33 */
44import {
55 AddressCoinSpecific ,
6+ PresignTransactionOptions as BasePresignTransactionOptions ,
7+ SignTransactionOptions as BaseSignTransactionOptions ,
8+ TransactionPrebuild as BaseTransactionPrebuild ,
9+ VerifyAddressOptions as BaseVerifyAddressOptions ,
610 BitGoBase ,
711 BuildNftTransferDataOptions ,
812 common ,
913 Ecdsa ,
1014 ECDSAMethodTypes ,
15+ ECDSAUtils ,
1116 EthereumLibraryUnavailableError ,
1217 FeeEstimateOptions ,
1318 FullySignedTransaction ,
@@ -17,31 +22,31 @@ import {
1722 InvalidAddressVerificationObjectPropertyError ,
1823 IWallet ,
1924 KeyPair ,
25+ MPCSweepRecoveryOptions ,
26+ MPCTx ,
27+ MPCTxs ,
2028 ParsedTransaction ,
2129 ParseTransactionOptions ,
2230 PrebuildTransactionResult ,
23- PresignTransactionOptions as BasePresignTransactionOptions ,
2431 Recipient ,
25- SignTransactionOptions as BaseSignTransactionOptions ,
2632 TransactionParams ,
27- TransactionPrebuild as BaseTransactionPrebuild ,
2833 TransactionRecipient ,
2934 TypedData ,
3035 UnexpectedAddressError ,
36+ UnsignedTransactionTss ,
3137 Util ,
32- VerifyAddressOptions as BaseVerifyAddressOptions ,
3338 VerifyTransactionOptions ,
3439 Wallet ,
35- ECDSAUtils ,
3640} from '@bitgo/sdk-core' ;
41+ import { getDerivationPath } from '@bitgo/sdk-lib-mpc' ;
42+ import { bip32 } from '@bitgo/secp256k1' ;
3743import {
38- BaseCoin as StaticsBaseCoin ,
3944 CoinMap ,
4045 coins ,
41- EthereumNetwork as EthLikeNetwork ,
4246 ethGasConfigs ,
47+ EthereumNetwork as EthLikeNetwork ,
48+ BaseCoin as StaticsBaseCoin ,
4349} from '@bitgo/statics' ;
44- import { bip32 } from '@bitgo/secp256k1' ;
4550import type * as EthLikeCommon from '@ethereumjs/common' ;
4651import type * as EthLikeTxLib from '@ethereumjs/tx' ;
4752import { FeeMarketEIP1559Transaction , Transaction as LegacyTransaction } from '@ethereumjs/tx' ;
@@ -202,6 +207,19 @@ interface UnformattedTxInfo {
202207 recipient : Recipient ;
203208}
204209
210+ export type UnsignedSweepTxMPCv2 = {
211+ txRequests : {
212+ transactions : [
213+ {
214+ unsignedTx : UnsignedTransactionTss ;
215+ nonce : number ;
216+ signatureShares : [ ] ;
217+ }
218+ ] ;
219+ walletCoin : string ;
220+ } [ ] ;
221+ } ;
222+
205223export type RecoverOptionsWithBytes = {
206224 isTss : true ;
207225 /**
@@ -232,6 +250,7 @@ export type RecoverOptions = {
232250 tokenContractAddress ?: string ;
233251 intendedChain ?: string ;
234252 common ?: EthLikeCommon . default ;
253+ derivationSeed ?: string ;
235254} & TSSRecoverOptions ;
236255
237256export type GetBatchExecutionInfoRT = {
@@ -1127,7 +1146,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
11271146 * @param {string } params.bitgoFeeAddress - wrong chain wallet fee address for evm based cross chain recovery txn
11281147 * @param {string } params.bitgoDestinationAddress - target bitgo address where fee will be sent for evm based cross chain recovery txn
11291148 */
1130- async recover ( params : RecoverOptions ) : Promise < RecoveryInfo | OfflineVaultTxInfo > {
1149+ async recover ( params : RecoverOptions ) : Promise < RecoveryInfo | OfflineVaultTxInfo | UnsignedSweepTxMPCv2 > {
11311150 if ( params . isTss === true ) {
11321151 return this . recoverTSS ( params ) ;
11331152 }
@@ -1868,45 +1887,31 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
18681887 * Recovers a tx with TSS key shares
18691888 * same expected arguments as recover method, but with TSS key shares
18701889 */
1871- protected async recoverTSS ( params : RecoverOptions ) : Promise < RecoveryInfo | OfflineVaultTxInfo > {
1890+ protected async recoverTSS (
1891+ params : RecoverOptions
1892+ ) : Promise < RecoveryInfo | OfflineVaultTxInfo | UnsignedSweepTxMPCv2 > {
18721893 this . validateRecoveryParams ( params ) ;
18731894 // Clean up whitespace from entered values
18741895 const userPublicOrPrivateKeyShare = params . userKey . replace ( / \s / g, '' ) ;
18751896 const backupPrivateOrPublicKeyShare = params . backupKey . replace ( / \s / g, '' ) ;
18761897
1877- const gasLimit = new optionalDeps . ethUtil . BN ( this . setGasLimit ( params . gasLimit ) ) ;
1878- const gasPrice = params . eip1559
1879- ? new optionalDeps . ethUtil . BN ( params . eip1559 . maxFeePerGas )
1880- : new optionalDeps . ethUtil . BN ( this . setGasPrice ( params . gasPrice ) ) ;
1881-
18821898 if (
18831899 getIsUnsignedSweep ( {
18841900 userKey : userPublicOrPrivateKeyShare ,
18851901 backupKey : backupPrivateOrPublicKeyShare ,
18861902 isTss : params . isTss ,
18871903 } )
18881904 ) {
1889- const backupKeyPair = new KeyPairLib ( { pub : backupPrivateOrPublicKeyShare } ) ;
1890- const baseAddress = backupKeyPair . getAddress ( ) ;
1891- const { txInfo, tx, nonce } = await this . buildTssRecoveryTxn ( baseAddress , gasPrice , gasLimit , params ) ;
1892- return this . formatForOfflineVaultTSS (
1893- txInfo ,
1894- tx ,
1895- userPublicOrPrivateKeyShare ,
1896- backupPrivateOrPublicKeyShare ,
1897- gasPrice ,
1898- gasLimit ,
1899- nonce ,
1900- params . eip1559 ,
1901- params . replayProtectionOptions
1902- ) ;
1905+ return this . buildUnsignedSweepTxnTSS ( params ) ;
19031906 } else {
19041907 const { userKeyShare, backupKeyShare, commonKeyChain } = await ECDSAUtils . getMpcV2RecoveryKeyShares (
19051908 userPublicOrPrivateKeyShare ,
19061909 backupPrivateOrPublicKeyShare ,
19071910 params . walletPassphrase
19081911 ) ;
19091912
1913+ const { gasLimit, gasPrice } = await this . getGasValues ( params ) ;
1914+
19101915 const MPC = new Ecdsa ( ) ;
19111916 const derivedCommonKeyChain = MPC . deriveUnhardened ( commonKeyChain , 'm/0' ) ;
19121917 const backupKeyPair = new KeyPairLib ( { pub : derivedCommonKeyChain . slice ( 0 , 66 ) } ) ;
@@ -1924,6 +1929,226 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
19241929 }
19251930 }
19261931
1932+ private async getGasValues ( params : RecoverOptions ) : Promise < { gasLimit : number ; gasPrice : Buffer } > {
1933+ const gasLimit = new optionalDeps . ethUtil . BN ( this . setGasLimit ( params . gasLimit ) ) ;
1934+ const gasPrice = params . eip1559
1935+ ? new optionalDeps . ethUtil . BN ( params . eip1559 . maxFeePerGas )
1936+ : new optionalDeps . ethUtil . BN ( this . setGasPrice ( params . gasPrice ) ) ;
1937+ return { gasLimit, gasPrice } ;
1938+ }
1939+
1940+ protected async buildUnsignedSweepTxnTSS ( params : RecoverOptions ) : Promise < OfflineVaultTxInfo | UnsignedSweepTxMPCv2 > {
1941+ const userPublicOrPrivateKeyShare = params . userKey . replace ( / \s / g, '' ) ;
1942+ const backupPrivateOrPublicKeyShare = params . backupKey . replace ( / \s / g, '' ) ;
1943+
1944+ const { gasLimit, gasPrice } = await this . getGasValues ( params ) ;
1945+
1946+ const backupKeyPair = new KeyPairLib ( { pub : backupPrivateOrPublicKeyShare } ) ;
1947+ const baseAddress = backupKeyPair . getAddress ( ) ;
1948+ const { txInfo, tx, nonce } = await this . buildTssRecoveryTxn ( baseAddress , gasPrice , gasLimit , params ) ;
1949+ return this . formatForOfflineVaultTSS (
1950+ txInfo ,
1951+ tx ,
1952+ userPublicOrPrivateKeyShare ,
1953+ backupPrivateOrPublicKeyShare ,
1954+ gasPrice ,
1955+ gasLimit ,
1956+ nonce ,
1957+ params . eip1559 ,
1958+ params . replayProtectionOptions
1959+ ) ;
1960+ }
1961+
1962+ protected async buildUnsignedSweepTxnMPCv2 ( params : RecoverOptions ) : Promise < UnsignedSweepTxMPCv2 > {
1963+ const { gasLimit, gasPrice } = await this . getGasValues ( params ) ;
1964+
1965+ const recoverParams = params as RecoverOptions ;
1966+ this . validateUnsignedSweepTSSParams ( recoverParams ) ;
1967+
1968+ const derivationPath = recoverParams . derivationSeed ? getDerivationPath ( recoverParams . derivationSeed ) : 'm/0' ;
1969+ const MPC = new Ecdsa ( ) ;
1970+ const derivedCommonKeyChain = MPC . deriveUnhardened ( recoverParams . backupKey as string , derivationPath ) ;
1971+ const backupKeyPair = new KeyPairLib ( { pub : derivedCommonKeyChain . slice ( 0 , 66 ) } ) ;
1972+ const baseAddress = backupKeyPair . getAddress ( ) ;
1973+ const { txInfo, tx, nonce } = await this . buildTssRecoveryTxn ( baseAddress , gasPrice , gasLimit , params ) ;
1974+ return this . buildTxRequestForOfflineVaultMPCv2 (
1975+ txInfo ,
1976+ tx ,
1977+ derivationPath ,
1978+ nonce ,
1979+ gasPrice ,
1980+ gasLimit ,
1981+ params . eip1559 ,
1982+ params . replayProtectionOptions ,
1983+ recoverParams . backupKey as string
1984+ ) ;
1985+ }
1986+
1987+ async createBroadcastableSweepTransaction ( params : MPCSweepRecoveryOptions ) : Promise < MPCTxs > {
1988+ const req = params . signatureShares ;
1989+ const broadcastableTransactions : MPCTx [ ] = [ ] ;
1990+ let lastScanIndex = 0 ;
1991+
1992+ for ( let i = 0 ; i < req . length ; i ++ ) {
1993+ const MPC = new Ecdsa ( ) ;
1994+ const transaction = req [ i ] ?. txRequest ?. transactions ?. [ 0 ] ?. unsignedTx as unknown as UnsignedTransactionTss ;
1995+ if ( ! req [ i ] . ovc || ! req [ i ] . ovc [ 0 ] . ecdsaSignature ) {
1996+ throw new Error ( 'Missing signature(s)' ) ;
1997+ }
1998+ if ( ! transaction . signableHex ) {
1999+ throw new Error ( 'Missing signable hex' ) ;
2000+ }
2001+ const signature = req [ i ] . ovc [ 0 ] . ecdsaSignature ;
2002+ if ( ! signature ) {
2003+ throw new Error ( 'Signature is undefined' ) ;
2004+ }
2005+ const shares : string [ ] = signature . toString ( ) . split ( ':' ) ;
2006+ if ( shares . length !== 4 ) {
2007+ throw new Error ( 'Invalid signature' ) ;
2008+ }
2009+ const finalSignature : ECDSAMethodTypes . Signature = {
2010+ recid : Number ( shares [ 0 ] ) ,
2011+ r : shares [ 1 ] ,
2012+ s : shares [ 2 ] ,
2013+ y : shares [ 3 ] ,
2014+ } as unknown as ECDSAMethodTypes . Signature ;
2015+ const signatureHex = Buffer . from ( signature . toString ( ) , 'hex' ) ;
2016+ const txBuilder = this . getTransactionBuilder ( getCommon ( this . getNetwork ( ) as EthLikeNetwork ) ) ;
2017+ txBuilder . from ( transaction . serializedTxHex as string ) ;
2018+
2019+ if ( ! transaction . coinSpecific ?. commonKeyChain ) {
2020+ throw new Error ( `Missing common keychain for transaction at index ${ i } ` ) ;
2021+ }
2022+ const commonKeyChain = transaction . coinSpecific . commonKeyChain ;
2023+ if ( ! transaction . derivationPath ) {
2024+ throw new Error ( `Missing derivation path for transaction at index ${ i } ` ) ;
2025+ }
2026+ if ( ! commonKeyChain ) {
2027+ throw new Error ( `Missing common key chain for transaction at index ${ i } ` ) ;
2028+ }
2029+
2030+ const derivationPath = transaction . derivationPath ?? 'm/0' ;
2031+ const derivedCommonKeyChain = MPC . deriveUnhardened ( String ( commonKeyChain ) , String ( derivationPath ) ) ;
2032+ const derivedPublicKey = new KeyPairLib ( { pub : derivedCommonKeyChain . slice ( 0 , 66 ) } ) ;
2033+ txBuilder . addSignature ( { pub : derivedPublicKey . getKeys ( ) . pub } , signatureHex ) ;
2034+ const ethCommmon = AbstractEthLikeNewCoins . getEthLikeCommon (
2035+ transaction . eip1559 ,
2036+ transaction . replayProtectionOptions
2037+ ) ;
2038+ let unsignedTx ;
2039+ if ( transaction . eip1559 ) {
2040+ unsignedTx = await FeeMarketEIP1559Transaction . fromSerializedTx (
2041+ Buffer . from ( transaction . serializedTxHex , 'hex' )
2042+ ) ;
2043+ } else {
2044+ unsignedTx = await LegacyTransaction . fromSerializedTx ( Buffer . from ( transaction . serializedTxHex , 'hex' ) ) ;
2045+ }
2046+ const signedTx = this . getSignedTxFromSignature ( ethCommmon , unsignedTx , finalSignature ) ;
2047+ broadcastableTransactions . push ( {
2048+ serializedTx : addHexPrefix ( signedTx . serialize ( ) . toString ( 'hex' ) ) ,
2049+ } ) ;
2050+
2051+ if ( i === req . length - 1 && transaction . coinSpecific ! . lastScanIndex ) {
2052+ lastScanIndex = transaction . coinSpecific ! . lastScanIndex as number ;
2053+ }
2054+ }
2055+
2056+ return { transactions : broadcastableTransactions , lastScanIndex } ;
2057+ }
2058+
2059+ /**
2060+ * Method to validate recovery params
2061+ * @param {RecoverOptions } params
2062+ * @returns {void }
2063+ */
2064+ private async validateUnsignedSweepTSSParams ( params : RecoverOptions ) : Promise < void > {
2065+ if ( _ . isUndefined ( params . backupKey ) && params . backupKey === '' ) {
2066+ throw new Error ( 'missing commonKeyChain' ) ;
2067+ }
2068+ if ( ! _ . isUndefined ( params . derivationSeed ) && typeof params . derivationSeed !== 'string' ) {
2069+ throw new Error ( 'invalid derivationSeed' ) ;
2070+ }
2071+ if (
2072+ _ . isUndefined ( params . bitgoDestinationAddress ) ||
2073+ typeof params . bitgoDestinationAddress !== 'string' ||
2074+ ! this . isValidAddress ( params . bitgoDestinationAddress )
2075+ ) {
2076+ throw new Error ( 'missing or invalid destinationAddress' ) ;
2077+ }
2078+ }
2079+
2080+ /**
2081+ * Helper function for recover()
2082+ * This transforms the unsigned transaction information into a format the BitGo offline vault expects
2083+ * @param {UnformattedTxInfo } txInfo - tx info
2084+ * @param {EthLikeTxLib.Transaction | EthLikeTxLib.FeeMarketEIP1559Transaction } ethTx - the ethereumjs tx object
2085+ * @param {string } derivationPath - the derivationPath
2086+ * @param {number } nonce - the nonce of the backup key address
2087+ * @param {Buffer } gasPrice - gas price for the tx
2088+ * @param {number } gasLimit - gas limit for the tx
2089+ * @param {EIP1559 } eip1559 - eip1559 params
2090+ * @returns {Promise<OfflineVaultTxInfo> }
2091+ */
2092+ private buildTxRequestForOfflineVaultMPCv2 (
2093+ txInfo : UnformattedTxInfo ,
2094+ ethTx : EthLikeTxLib . Transaction | EthLikeTxLib . FeeMarketEIP1559Transaction ,
2095+ derivationPath : string ,
2096+ nonce : number ,
2097+ gasPrice : Buffer ,
2098+ gasLimit : number ,
2099+ eip1559 ?: EIP1559 ,
2100+ replayProtectionOptions ?: ReplayProtectionOptions ,
2101+ commonKeyChain ?: string
2102+ ) : UnsignedSweepTxMPCv2 {
2103+ if ( ! ethTx . to ) {
2104+ throw new Error ( 'Eth tx must have a `to` address' ) ;
2105+ }
2106+
2107+ const fee = eip1559
2108+ ? gasLimit * eip1559 . maxFeePerGas
2109+ : gasLimit * optionalDeps . ethUtil . bufferToInt ( gasPrice ) . toFixed ( ) ;
2110+
2111+ const unsignedTx : UnsignedTransactionTss = {
2112+ serializedTxHex : ethTx . serialize ( ) . toString ( 'hex' ) ,
2113+ signableHex : ethTx . getMessageToSign ( false ) . toString ( 'hex' ) ,
2114+ derivationPath : derivationPath ,
2115+ feeInfo : {
2116+ fee : fee ,
2117+ feeString : fee . toString ( ) ,
2118+ } ,
2119+ parsedTx : {
2120+ spendAmount : txInfo . recipient . amount ,
2121+ outputs : [
2122+ {
2123+ coinName : this . getChain ( ) ,
2124+ address : txInfo . recipient . address ,
2125+ valueString : txInfo . recipient . amount ,
2126+ } ,
2127+ ] ,
2128+ } ,
2129+ coinSpecific : {
2130+ commonKeyChain : commonKeyChain ,
2131+ } ,
2132+ eip1559 : eip1559 ,
2133+ replayProtectionOptions : replayProtectionOptions ,
2134+ } ;
2135+
2136+ return {
2137+ txRequests : [
2138+ {
2139+ walletCoin : this . getChain ( ) ,
2140+ transactions : [
2141+ {
2142+ unsignedTx : unsignedTx ,
2143+ nonce : nonce ,
2144+ signatureShares : [ ] ,
2145+ } ,
2146+ ] ,
2147+ } ,
2148+ ] ,
2149+ } ;
2150+ }
2151+
19272152 private async buildTssRecoveryTxn ( baseAddress : string , gasPrice : any , gasLimit : any , params : RecoverOptions ) {
19282153 const nonce = await this . getAddressNonce ( baseAddress ) ;
19292154 const txAmount = await this . validateBalanceAndGetTxAmount ( baseAddress , gasPrice , gasLimit ) ;
@@ -1957,7 +2182,6 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
19572182
19582183 async validateBalanceAndGetTxAmount ( baseAddress : string , gasPrice : BN , gasLimit : BN ) {
19592184 const baseAddressBalance = await this . queryAddressBalance ( baseAddress ) ;
1960-
19612185 const totalGasNeeded = gasPrice . mul ( gasLimit ) ;
19622186 const weiToGwei = new BN ( 10 ** 9 ) ;
19632187 if ( baseAddressBalance . lt ( totalGasNeeded ) ) {
@@ -1967,7 +2191,6 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
19672191 ` Gwei to perform recoveries. Try sending some ETH to this address then retry.`
19682192 ) ;
19692193 }
1970-
19712194 const txAmount = baseAddressBalance . sub ( totalGasNeeded ) ;
19722195 return txAmount ;
19732196 }
0 commit comments