Skip to content

Commit 2a35f55

Browse files
committed
refactor(sdk-coin-flrp): update transaction builder interfaces and improve error handling
Ticket: WIN-8145
1 parent 87df3db commit 2a35f55

File tree

13 files changed

+1594
-212
lines changed

13 files changed

+1594
-212
lines changed

modules/sdk-coin-flrp/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
".ts"
4343
]
4444
},
45+
"devDependencies": {
46+
"@bitgo/sdk-api": "^1.71.8",
47+
"@bitgo/sdk-test": "^9.1.16"
48+
},
4549
"dependencies": {
4650
"@bitgo/sdk-core": "^36.23.0",
4751
"@bitgo/secp256k1": "^1.7.0",
@@ -51,6 +55,7 @@
5155
"bignumber.js": "9.0.0",
5256
"bs58": "^6.0.0",
5357
"create-hash": "^1.2.0",
58+
"ethereumjs-util": "^7.1.5",
5459
"safe-buffer": "^5.2.1"
5560
},
5661
"gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c",

modules/sdk-coin-flrp/src/flrp.ts

Lines changed: 285 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BaseCoin as StaticsBaseCoin, CoinFamily } from '@bitgo/statics';
1+
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins, FlareNetwork } from '@bitgo/statics';
22
import {
33
AuditDecryptedKeyParams,
44
BaseCoin,
@@ -9,11 +9,27 @@ import {
99
ParsedTransaction,
1010
ParseTransactionOptions,
1111
SignedTransaction,
12-
SignTransactionOptions,
13-
TssVerifyAddressOptions,
1412
VerifyAddressOptions,
15-
VerifyTransactionOptions,
13+
TransactionType,
14+
ITransactionRecipient,
15+
InvalidAddressError,
16+
UnexpectedAddressError,
17+
InvalidTransactionError,
18+
BaseTransaction,
19+
SigningError,
20+
MethodNotImplementedError,
1621
} from '@bitgo/sdk-core';
22+
import * as FlrpLib from './lib';
23+
import {
24+
FlrpEntry,
25+
FlrpExplainTransactionOptions,
26+
FlrpSignTransactionOptions,
27+
FlrpTransactionParams,
28+
FlrpVerifyTransactionOptions,
29+
} from './lib/iface';
30+
import utils from './lib/utils';
31+
import BigNumber from 'bignumber.js';
32+
import { isValidAddress as isValidEthAddress } from 'ethereumjs-util';
1733

1834
export class Flrp extends BaseCoin {
1935
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -50,28 +66,280 @@ export class Flrp extends BaseCoin {
5066
return multisigTypes.onchain;
5167
}
5268

53-
verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
54-
throw new Error('Method not implemented.');
69+
async verifyTransaction(params: FlrpVerifyTransactionOptions): Promise<boolean> {
70+
const txHex = params.txPrebuild && params.txPrebuild.txHex;
71+
if (!txHex) {
72+
throw new Error('missing required tx prebuild property txHex');
73+
}
74+
let tx;
75+
try {
76+
const txBuilder = this.getBuilder().from(txHex);
77+
tx = await txBuilder.build();
78+
} catch (error) {
79+
throw new Error(`Invalid transaction: ${error.message}`);
80+
}
81+
const explainedTx = tx.explainTransaction();
82+
83+
const type = params.txParams.type;
84+
85+
if (!type || (type !== 'ImportToC' && explainedTx.type !== TransactionType[type])) {
86+
throw new Error('Tx type does not match with expected txParams type');
87+
}
88+
89+
switch (explainedTx.type) {
90+
case TransactionType.Export:
91+
if (!params.txParams.recipients || params.txParams.recipients?.length !== 1) {
92+
throw new Error('Export Tx requires a recipient');
93+
} else {
94+
this.validateExportTx(params.txParams.recipients, explainedTx);
95+
}
96+
break;
97+
case TransactionType.Import:
98+
if (tx.isTransactionForCChain) {
99+
// Import to C-chain
100+
if (explainedTx.outputs.length !== 1) {
101+
throw new Error('Expected 1 output in import transaction');
102+
}
103+
if (!params.txParams.recipients || params.txParams.recipients.length !== 1) {
104+
throw new Error('Expected 1 recipient in import transaction');
105+
}
106+
} else {
107+
// Import to P-chain
108+
if (explainedTx.outputs.length !== 1) {
109+
throw new Error('Expected 1 output in import transaction');
110+
}
111+
this.validateImportTx(explainedTx.inputs, params.txParams);
112+
}
113+
break;
114+
default:
115+
throw new Error('Tx type is not supported yet');
116+
}
117+
return true;
55118
}
56-
isWalletAddress(params: VerifyAddressOptions | TssVerifyAddressOptions): Promise<boolean> {
57-
throw new Error('Method not implemented.');
119+
120+
/**
121+
* Check if export txn is valid, based on expected tx params.
122+
*
123+
* @param {ITransactionRecipient[]} recipients expected recipients and info
124+
* @param {FlrpLib.TransactionExplanation} explainedTx explained export transaction
125+
*/
126+
validateExportTx(recipients: ITransactionRecipient[], explainedTx: FlrpLib.TransactionExplanation): void {
127+
if (recipients.length !== 1 || explainedTx.outputs.length !== 1) {
128+
throw new Error('Export Tx requires one recipient');
129+
}
130+
131+
const maxImportFee = (this._staticsCoin.network as FlareNetwork).maxImportFee;
132+
const recipientAmount = new BigNumber(recipients[0].amount);
133+
if (
134+
recipientAmount.isGreaterThan(explainedTx.outputAmount) ||
135+
recipientAmount.plus(maxImportFee).isLessThan(explainedTx.outputAmount)
136+
) {
137+
throw new Error(
138+
`Tx total amount ${explainedTx.outputAmount} does not match with expected total amount field ${recipientAmount} and max import fee ${maxImportFee}`
139+
);
140+
}
141+
142+
if (explainedTx.outputs && !utils.isValidAddress(explainedTx.outputs[0].address)) {
143+
throw new Error(`Invalid P-chain address ${explainedTx.outputs[0].address}`);
144+
}
58145
}
59-
parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
60-
throw new Error('Method not implemented.');
146+
147+
/**
148+
* Check if import txn into P is valid, based on expected tx params.
149+
*
150+
* @param {FlrpEntry[]} explainedTxInputs tx inputs (unspents to be imported)
151+
* @param {FlrpTransactionParams} txParams expected tx info to check against
152+
*/
153+
validateImportTx(explainedTxInputs: FlrpEntry[], txParams: FlrpTransactionParams): void {
154+
if (txParams.unspents) {
155+
if (explainedTxInputs.length !== txParams.unspents.length) {
156+
throw new Error(`Expected ${txParams.unspents.length} UTXOs, transaction had ${explainedTxInputs.length}`);
157+
}
158+
159+
const unspents = new Set(txParams.unspents);
160+
161+
for (const unspent of explainedTxInputs) {
162+
if (!unspents.has(unspent.id)) {
163+
throw new Error(`Transaction should not contain the UTXO: ${unspent.id}`);
164+
}
165+
}
166+
}
167+
}
168+
169+
private getBuilder(): FlrpLib.TransactionBuilderFactory {
170+
return new FlrpLib.TransactionBuilderFactory(coins.get(this.getChain()));
171+
}
172+
173+
/**
174+
* Check if address is valid, then make sure it matches the root address.
175+
*
176+
* @param params.address address to validate
177+
* @param params.keychains public keys to generate the wallet
178+
*/
179+
async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
180+
const { address, keychains } = params;
181+
182+
if (!this.isValidAddress(address)) {
183+
throw new InvalidAddressError(`invalid address: ${address}`);
184+
}
185+
if (!keychains || keychains.length !== 3) {
186+
throw new Error('Invalid keychains');
187+
}
188+
189+
// multisig addresses are separated by ~
190+
const splitAddresses = address.split('~');
191+
192+
// derive addresses from keychain
193+
const unlockAddresses = keychains.map((keychain) =>
194+
new FlrpLib.KeyPair({ pub: keychain.pub }).getAddress(this._staticsCoin.network.type)
195+
);
196+
197+
if (splitAddresses.length !== unlockAddresses.length) {
198+
throw new UnexpectedAddressError(`address validation failure: multisig address length does not match`);
199+
}
200+
201+
if (!this.adressesArraysMatch(splitAddresses, unlockAddresses)) {
202+
throw new UnexpectedAddressError(`address validation failure: ${address} is not of this wallet`);
203+
}
204+
205+
return true;
61206
}
207+
208+
/**
209+
* Validate that two multisig address arrays have the same elements, order doesnt matter
210+
* @param addressArray1
211+
* @param addressArray2
212+
* @returns true if address arrays have the same addresses
213+
* @private
214+
*/
215+
private adressesArraysMatch(addressArray1: string[], addressArray2: string[]) {
216+
return JSON.stringify(addressArray1.sort()) === JSON.stringify(addressArray2.sort());
217+
}
218+
219+
/**
220+
* Generate Flrp key pair
221+
*
222+
* @param {Buffer} seed - Seed from which the new keypair should be generated, otherwise a random seed is used
223+
* @returns {Object} object with generated pub and prv
224+
*/
62225
generateKeyPair(seed?: Buffer): KeyPair {
63-
throw new Error('Method not implemented.');
226+
const keyPair = seed ? new FlrpLib.KeyPair({ seed }) : new FlrpLib.KeyPair();
227+
const keys = keyPair.getKeys();
228+
229+
if (!keys.prv) {
230+
throw new Error('Missing prv in key generation.');
231+
}
232+
233+
return {
234+
pub: keys.pub,
235+
prv: keys.prv,
236+
};
64237
}
238+
239+
/**
240+
* Return boolean indicating whether input is valid public key for the coin
241+
*
242+
* @param {string} pub the prv to be checked
243+
* @returns is it valid?
244+
*/
65245
isValidPub(pub: string): boolean {
66-
throw new Error('Method not implemented.');
246+
try {
247+
new FlrpLib.KeyPair({ pub });
248+
return true;
249+
} catch (e) {
250+
return false;
251+
}
252+
}
253+
254+
/**
255+
* Return boolean indicating whether input is valid private key for the coin
256+
*
257+
* @param {string} prv the prv to be checked
258+
* @returns is it valid?
259+
*/
260+
isValidPrv(prv: string): boolean {
261+
try {
262+
new FlrpLib.KeyPair({ prv });
263+
return true;
264+
} catch (e) {
265+
return false;
266+
}
267+
}
268+
269+
isValidAddress(address: string | string[]): boolean {
270+
if (address === undefined) {
271+
return false;
272+
}
273+
274+
// validate eth address for cross-chain txs to c-chain
275+
if (typeof address === 'string' && isValidEthAddress(address)) {
276+
return true;
277+
}
278+
279+
return FlrpLib.Utils.isValidAddress(address);
67280
}
68-
isValidAddress(address: string): boolean {
69-
throw new Error('Method not implemented.');
281+
282+
/**
283+
* Signs Avaxp transaction
284+
*/
285+
async signTransaction(params: FlrpSignTransactionOptions): Promise<SignedTransaction> {
286+
// deserialize raw transaction (note: fromAddress has onchain order)
287+
const txBuilder = this.getBuilder().from(params.txPrebuild.txHex);
288+
const key = params.prv;
289+
290+
// push the keypair to signer array
291+
txBuilder.sign({ key });
292+
293+
// build the transaction
294+
const transaction: BaseTransaction = await txBuilder.build();
295+
if (!transaction) {
296+
throw new InvalidTransactionError('Error while trying to build transaction');
297+
}
298+
return transaction.signature.length >= 2
299+
? { txHex: transaction.toBroadcastFormat() }
300+
: { halfSigned: { txHex: transaction.toBroadcastFormat() } };
301+
}
302+
303+
async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
304+
return {};
305+
}
306+
307+
/**
308+
* Explain a Avaxp transaction from txHex
309+
* @param params
310+
* @param callback
311+
*/
312+
async explainTransaction(params: FlrpExplainTransactionOptions): Promise<FlrpLib.TransactionExplanation> {
313+
const txHex = params.txHex ?? params?.halfSigned?.txHex;
314+
if (!txHex) {
315+
throw new Error('missing transaction hex');
316+
}
317+
try {
318+
const txBuilder = this.getBuilder().from(txHex);
319+
const tx = await txBuilder.build();
320+
return tx.explainTransaction();
321+
} catch (e) {
322+
throw new Error(`Invalid transaction: ${e.message}`);
323+
}
324+
}
325+
326+
recoverySignature(message: Buffer, signature: Buffer): Buffer {
327+
return FlrpLib.Utils.recoverySignature(this._staticsCoin.network as FlareNetwork, message, signature);
70328
}
71-
signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
72-
throw new Error('Method not implemented.');
329+
330+
async signMessage(key: KeyPair, message: string | Buffer): Promise<Buffer> {
331+
const prv = new FlrpLib.KeyPair(key).getPrivateKey();
332+
if (!prv) {
333+
throw new SigningError('Invalid key pair options');
334+
}
335+
if (typeof message === 'string') {
336+
message = Buffer.from(message, 'hex');
337+
}
338+
return FlrpLib.Utils.createSignature(this._staticsCoin.network as FlareNetwork, message, prv);
73339
}
340+
341+
/** @inheritDoc */
74342
auditDecryptedKey(params: AuditDecryptedKeyParams): void {
75-
throw new Error('Method not implemented.');
343+
throw new MethodNotImplementedError();
76344
}
77345
}

modules/sdk-coin-flrp/src/lib/ExportInCTxBuilder.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ export class ExportInCTxBuilder extends AtomicInCTransactionBuilder {
8989
}
9090
const output = outputs[0];
9191

92-
// TODO validate assetId
92+
if (Buffer.from(output.assetId.toBytes()).toString('hex') !== this.transaction._assetId) {
93+
throw new BuildTransactionError('AssetID mismatch');
94+
}
9395

9496
// The inputs is not an utxo.
9597
// It's expected to have only one input from C-Chain address.

modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder {
166166
}
167167

168168
/**
169-
* Builds the avax transaction. transaction field is changed.
169+
* Builds the Flare transaction. Transaction field is changed.
170170
*/
171171
protected abstract buildFlareTransaction(): void;
172172

0 commit comments

Comments
 (0)