1- import { BaseCoin as StaticsBaseCoin , CoinFamily } from '@bitgo/statics' ;
1+ import { BaseCoin as StaticsBaseCoin , CoinFamily , coins , FlareNetwork } from '@bitgo/statics' ;
22import {
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
1834export 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}
0 commit comments