@@ -14,13 +14,27 @@ import {
1414 TransactionExplanation ,
1515 TssVerifyAddressOptions ,
1616 VerifyTransactionOptions ,
17+ EDDSAMethodTypes ,
18+ MPCRecoveryOptions ,
19+ MPCTx ,
20+ MPCUnsignedTx ,
21+ RecoveryTxRequest ,
22+ OvcInput ,
23+ OvcOutput ,
24+ Environments ,
25+ MPCSweepTxs ,
26+ PublicKey ,
27+ MPCTxs ,
28+ MPCSweepRecoveryOptions ,
1729} from '@bitgo/sdk-core' ;
1830import { BaseCoin as StaticsBaseCoin , coins } from '@bitgo/statics' ;
1931import { KeyPair as TonKeyPair } from './lib/keyPair' ;
2032import BigNumber from 'bignumber.js' ;
2133import * as _ from 'lodash' ;
22- import { Transaction , TransactionBuilderFactory , Utils } from './lib' ;
34+ import { Transaction , TransactionBuilderFactory , Utils , TransferBuilder } from './lib' ;
2335import TonWeb from 'tonweb' ;
36+ import { getDerivationPath } from '@bitgo/sdk-lib-mpc' ;
37+ import { getFeeEstimate } from './lib/utils' ;
2438
2539export interface TonParseTransactionOptions extends ParseTransactionOptions {
2640 txHex : string ;
@@ -240,4 +254,240 @@ export class Ton extends BaseCoin {
240254 throw new Error ( 'Invalid transaction' ) ;
241255 }
242256 }
257+
258+ protected getPublicNodeUrl ( ) : string {
259+ return Environments [ this . bitgo . getEnv ( ) ] . tonNodeUrl ;
260+ }
261+
262+ private getBuilder ( ) : TransactionBuilderFactory {
263+ return new TransactionBuilderFactory ( coins . get ( this . getChain ( ) ) ) ;
264+ }
265+
266+ async recover ( params : MPCRecoveryOptions ) : Promise < MPCTx | MPCSweepTxs > {
267+ if ( ! params . bitgoKey ) {
268+ throw new Error ( 'missing bitgoKey' ) ;
269+ }
270+ if ( ! params . recoveryDestination || ! this . isValidAddress ( params . recoveryDestination ) ) {
271+ throw new Error ( 'invalid recoveryDestination' ) ;
272+ }
273+ if ( ! params . apiKey ) {
274+ throw new Error ( 'missing apiKey' ) ;
275+ }
276+ const bitgoKey = params . bitgoKey . replace ( / \s / g, '' ) ;
277+ const isUnsignedSweep = ! params . userKey && ! params . backupKey && ! params . walletPassphrase ;
278+
279+ // Build the transaction
280+ const tonweb = new TonWeb ( new TonWeb . HttpProvider ( this . getPublicNodeUrl ( ) , { apiKey : params . apiKey } ) ) ;
281+ const MPC = await EDDSAMethods . getInitializedMpcInstance ( ) ;
282+
283+ const index = params . index || 0 ;
284+ const currPath = params . seed ? getDerivationPath ( params . seed ) + `/${ index } ` : `m/${ index } ` ;
285+ const accountId = MPC . deriveUnhardened ( bitgoKey , currPath ) . slice ( 0 , 64 ) ;
286+ const senderAddr = await Utils . default . getAddressFromPublicKey ( accountId ) ;
287+ const balance = await tonweb . getBalance ( senderAddr ) ;
288+ if ( new BigNumber ( balance ) . isEqualTo ( 0 ) ) {
289+ throw Error ( 'Did not find address with funds to recover' ) ;
290+ }
291+
292+ const WalletClass = tonweb . wallet . all [ 'v4R2' ] ;
293+ const wallet = new WalletClass ( tonweb . provider , {
294+ publicKey : tonweb . utils . hexToBytes ( accountId ) ,
295+ wc : 0 ,
296+ } ) ;
297+ let seqno = await wallet . methods . seqno ( ) . call ( ) ;
298+ if ( seqno === null ) {
299+ seqno = 0 ;
300+ }
301+
302+ const feeEstimate = await getFeeEstimate ( wallet , params . recoveryDestination , balance , seqno as number ) ;
303+
304+ const totalFeeEstimate = Math . round (
305+ ( feeEstimate . source_fees . in_fwd_fee +
306+ feeEstimate . source_fees . storage_fee +
307+ feeEstimate . source_fees . gas_fee +
308+ feeEstimate . source_fees . fwd_fee ) *
309+ 1.5
310+ ) ;
311+
312+ if ( new BigNumber ( totalFeeEstimate ) . gt ( balance ) ) {
313+ throw Error ( 'Did not find address with funds to recover' ) ;
314+ }
315+
316+ const factory = this . getBuilder ( ) ;
317+ const expireAt = Math . floor ( Date . now ( ) / 1e3 ) + 60 * 60 * 24 * 7 ; // 7 days
318+
319+ const txBuilder = factory
320+ . getTransferBuilder ( )
321+ . sender ( senderAddr )
322+ . sequenceNumber ( seqno as number )
323+ . publicKey ( accountId )
324+ . expireTime ( expireAt ) ;
325+
326+ ( txBuilder as TransferBuilder ) . send ( {
327+ address : params . recoveryDestination ,
328+ amount : new BigNumber ( balance ) . minus ( new BigNumber ( totalFeeEstimate ) ) . toString ( ) ,
329+ } ) ;
330+
331+ const unsignedTransaction = await txBuilder . build ( ) ;
332+
333+ if ( ! isUnsignedSweep ) {
334+ if ( ! params . userKey ) {
335+ throw new Error ( 'missing userKey' ) ;
336+ }
337+ if ( ! params . backupKey ) {
338+ throw new Error ( 'missing backupKey' ) ;
339+ }
340+ if ( ! params . walletPassphrase ) {
341+ throw new Error ( 'missing wallet passphrase' ) ;
342+ }
343+
344+ // Clean up whitespace from entered values
345+ const userKey = params . userKey . replace ( / \s / g, '' ) ;
346+ const backupKey = params . backupKey . replace ( / \s / g, '' ) ;
347+
348+ let userPrv ;
349+
350+ try {
351+ userPrv = this . bitgo . decrypt ( {
352+ input : userKey ,
353+ password : params . walletPassphrase ,
354+ } ) ;
355+ } catch ( e ) {
356+ throw new Error ( `Error decrypting user keychain: ${ e . message } ` ) ;
357+ }
358+ const userSigningMaterial = JSON . parse ( userPrv ) as EDDSAMethodTypes . UserSigningMaterial ;
359+
360+ let backupPrv ;
361+ try {
362+ backupPrv = this . bitgo . decrypt ( {
363+ input : backupKey ,
364+ password : params . walletPassphrase ,
365+ } ) ;
366+ } catch ( e ) {
367+ throw new Error ( `Error decrypting backup keychain: ${ e . message } ` ) ;
368+ }
369+ const backupSigningMaterial = JSON . parse ( backupPrv ) as EDDSAMethodTypes . BackupSigningMaterial ;
370+
371+ const signatureHex = await EDDSAMethods . getTSSSignature (
372+ userSigningMaterial ,
373+ backupSigningMaterial ,
374+ currPath ,
375+ unsignedTransaction
376+ ) ;
377+
378+ const publicKeyObj = { pub : senderAddr } ;
379+ txBuilder . addSignature ( publicKeyObj as PublicKey , signatureHex ) ;
380+ }
381+
382+ const completedTransaction = await txBuilder . build ( ) ;
383+ const serializedTx = completedTransaction . toBroadcastFormat ( ) ;
384+ const walletCoin = this . getChain ( ) ;
385+
386+ const inputs : OvcInput [ ] = [ ] ;
387+ for ( const input of completedTransaction . inputs ) {
388+ inputs . push ( {
389+ address : input . address ,
390+ valueString : input . value ,
391+ value : new BigNumber ( input . value ) . toNumber ( ) ,
392+ } ) ;
393+ }
394+ const outputs : OvcOutput [ ] = [ ] ;
395+ for ( const output of completedTransaction . outputs ) {
396+ outputs . push ( {
397+ address : output . address ,
398+ valueString : output . value ,
399+ coinName : output . coin ,
400+ } ) ;
401+ }
402+ const spendAmount = completedTransaction . inputs . length === 1 ? completedTransaction . inputs [ 0 ] . value : 0 ;
403+ const parsedTx = { inputs : inputs , outputs : outputs , spendAmount : spendAmount , type : '' } ;
404+ const feeInfo = { fee : totalFeeEstimate , feeString : totalFeeEstimate . toString ( ) } ;
405+ const coinSpecific = { commonKeychain : bitgoKey } ;
406+ if ( isUnsignedSweep ) {
407+ const transaction : MPCTx = {
408+ serializedTx : serializedTx ,
409+ scanIndex : index ,
410+ coin : walletCoin ,
411+ signableHex : completedTransaction . signablePayload . toString ( 'hex' ) ,
412+ derivationPath : currPath ,
413+ parsedTx : parsedTx ,
414+ feeInfo : feeInfo ,
415+ coinSpecific : coinSpecific ,
416+ } ;
417+ const unsignedTx : MPCUnsignedTx = { unsignedTx : transaction , signatureShares : [ ] } ;
418+ const transactions : MPCUnsignedTx [ ] = [ unsignedTx ] ;
419+ const txRequest : RecoveryTxRequest = {
420+ transactions : transactions ,
421+ walletCoin : walletCoin ,
422+ } ;
423+ const txRequests : MPCSweepTxs = { txRequests : [ txRequest ] } ;
424+ return txRequests ;
425+ }
426+
427+ const transaction : MPCTx = {
428+ serializedTx : serializedTx ,
429+ scanIndex : index ,
430+ } ;
431+ return transaction ;
432+ }
433+
434+ /**
435+ * Creates funds sweep recovery transaction(s) without BitGo
436+ *
437+ * @param {MPCSweepRecoveryOptions } params parameters needed to combine the signatures
438+ * and transactions to create broadcastable transactions
439+ *
440+ * @returns {MPCTxs } array of the serialized transaction hex strings and indices
441+ * of the addresses being swept
442+ */
443+ async createBroadcastableSweepTransaction ( params : MPCSweepRecoveryOptions ) : Promise < MPCTxs > {
444+ const req = params . signatureShares ;
445+ const broadcastableTransactions : MPCTx [ ] = [ ] ;
446+ let lastScanIndex = 0 ;
447+
448+ for ( let i = 0 ; i < req . length ; i ++ ) {
449+ const MPC = await EDDSAMethods . getInitializedMpcInstance ( ) ;
450+ const transaction = req [ i ] . txRequest . transactions [ 0 ] . unsignedTx ;
451+ if ( ! req [ i ] . ovc || ! req [ i ] . ovc [ 0 ] . eddsaSignature ) {
452+ throw new Error ( 'Missing signature(s)' ) ;
453+ }
454+ const signature = req [ i ] . ovc [ 0 ] . eddsaSignature ;
455+ if ( ! transaction . signableHex ) {
456+ throw new Error ( 'Missing signable hex' ) ;
457+ }
458+ const messageBuffer = Buffer . from ( transaction . signableHex ! , 'hex' ) ;
459+ const result = MPC . verify ( messageBuffer , signature ) ;
460+ if ( ! result ) {
461+ throw new Error ( 'Invalid signature' ) ;
462+ }
463+ const signatureHex = Buffer . concat ( [ Buffer . from ( signature . R , 'hex' ) , Buffer . from ( signature . sigma , 'hex' ) ] ) ;
464+ const txBuilder = this . getBuilder ( ) . from ( transaction . serializedTx as string ) ;
465+ if ( ! transaction . coinSpecific ?. commonKeychain ) {
466+ throw new Error ( 'Missing common keychain' ) ;
467+ }
468+ const commonKeychain = transaction . coinSpecific ! . commonKeychain ! as string ;
469+ if ( ! transaction . derivationPath ) {
470+ throw new Error ( 'Missing derivation path' ) ;
471+ }
472+ const derivationPath = transaction . derivationPath as string ;
473+ const accountId = MPC . deriveUnhardened ( commonKeychain , derivationPath ) . slice ( 0 , 64 ) ;
474+ const tonKeyPair = new TonKeyPair ( { pub : accountId } ) ;
475+
476+ // add combined signature from ovc
477+ txBuilder . addSignature ( { pub : tonKeyPair . getKeys ( ) . pub } , signatureHex ) ;
478+ const signedTransaction = await txBuilder . build ( ) ;
479+ const serializedTx = signedTransaction . toBroadcastFormat ( ) ;
480+
481+ broadcastableTransactions . push ( {
482+ serializedTx : serializedTx ,
483+ scanIndex : transaction . scanIndex ,
484+ } ) ;
485+
486+ if ( i === req . length - 1 && transaction . coinSpecific ! . lastScanIndex ) {
487+ lastScanIndex = transaction . coinSpecific ! . lastScanIndex as number ;
488+ }
489+ }
490+
491+ return { transactions : broadcastableTransactions , lastScanIndex } ;
492+ }
243493}
0 commit comments