@@ -19,6 +19,7 @@ import {
1919 IWallet ,
2020 KeyPair ,
2121 MPCSweepRecoveryOptions ,
22+ MPCSweepTxs ,
2223 MPCTx ,
2324 MPCTxs ,
2425 ParsedTransaction ,
@@ -57,7 +58,7 @@ import { BigNumber } from 'bignumber.js';
5758import BN from 'bn.js' ;
5859import { randomBytes } from 'crypto' ;
5960import debugLib from 'debug' ;
60- import { addHexPrefix , bufArrToArr , stripHexPrefix } from 'ethereumjs-util' ;
61+ import { addHexPrefix , bufArrToArr , stripHexPrefix , bufferToHex , setLengthLeft , toBuffer } from 'ethereumjs-util' ;
6162import Keccak from 'keccak' ;
6263import _ from 'lodash' ;
6364import secp256k1 from 'secp256k1' ;
@@ -70,6 +71,7 @@ import {
7071 ERC721TransferBuilder ,
7172 getBufferedByteCode ,
7273 getCommon ,
74+ getCreateForwarderParamsAndTypes ,
7375 getProxyInitcode ,
7476 getRawDecoded ,
7577 getToken ,
@@ -224,6 +226,11 @@ export type UnsignedSweepTxMPCv2 = {
224226 } [ ] ;
225227} ;
226228
229+ export type UnsignedBuilConsolidation = {
230+ transactions : MPCSweepTxs [ ] | UnsignedSweepTxMPCv2 [ ] | RecoveryInfo [ ] | OfflineVaultTxInfo [ ] ;
231+ lastScanIndex : number ;
232+ } ;
233+
227234export type RecoverOptionsWithBytes = {
228235 isTss : true ;
229236 /**
@@ -361,6 +368,33 @@ interface EthAddressCoinSpecifics extends AddressCoinSpecific {
361368 salt ?: string ;
362369}
363370
371+ export const DEFAULT_SCAN_FACTOR = 20 ;
372+ export interface EthConsolidationRecoveryOptions {
373+ coinName ?: string ;
374+ walletContractAddress ?: string ;
375+ apiKey ?: string ;
376+ isTss ?: boolean ;
377+ userKey ?: string ;
378+ backupKey ?: string ;
379+ walletPassphrase ?: string ;
380+ recoveryDestination ?: string ;
381+ krsProvider ?: string ;
382+ gasPrice ?: number ;
383+ gasLimit ?: number ;
384+ eip1559 ?: EIP1559 ;
385+ replayProtectionOptions ?: ReplayProtectionOptions ;
386+ bitgoFeeAddress ?: string ;
387+ bitgoDestinationAddress ?: string ;
388+ tokenContractAddress ?: string ;
389+ intendedChain ?: string ;
390+ common ?: EthLikeCommon . default ;
391+ derivationSeed ?: string ;
392+ bitgoKey ?: string ;
393+ startingScanIndex ?: number ;
394+ endingScanIndex ?: number ;
395+ ignoreAddressTypes ?: unknown ;
396+ }
397+
364398export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
365399 baseAddress : string ;
366400 coinSpecific : EthAddressCoinSpecifics ;
@@ -1192,6 +1226,161 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
11921226 return this . recoverEthLike ( params ) ;
11931227 }
11941228
1229+ generateForwarderAddress (
1230+ baseAddress : string ,
1231+ feeAddress : string ,
1232+ forwarderFactoryAddress : string ,
1233+ forwarderImplementationAddress : string ,
1234+ index : number
1235+ ) : string {
1236+ const salt = addHexPrefix ( index . toString ( 16 ) ) ;
1237+ const saltBuffer = setLengthLeft ( toBuffer ( salt ) , 32 ) ;
1238+
1239+ const { createForwarderParams, createForwarderTypes } = getCreateForwarderParamsAndTypes (
1240+ baseAddress ,
1241+ saltBuffer ,
1242+ feeAddress
1243+ ) ;
1244+
1245+ const calculationSalt = bufferToHex ( optionalDeps . ethAbi . soliditySHA3 ( createForwarderTypes , createForwarderParams ) ) ;
1246+
1247+ const initCode = getProxyInitcode ( forwarderImplementationAddress ) ;
1248+ return calculateForwarderV1Address ( forwarderFactoryAddress , calculationSalt , initCode ) ;
1249+ }
1250+
1251+ deriveAddressFromPublicKey ( commonKeychain : string , index : number ) : string {
1252+ const derivationPath = `m/${ index } ` ;
1253+ const pubkeySize = 33 ;
1254+
1255+ const ecdsaMpc = new Ecdsa ( ) ;
1256+ const derivedPublicKey = Buffer . from ( ecdsaMpc . deriveUnhardened ( commonKeychain , derivationPath ) , 'hex' )
1257+ . subarray ( 0 , pubkeySize )
1258+ . toString ( 'hex' ) ;
1259+
1260+ const publicKey = Buffer . from ( derivedPublicKey , 'hex' ) . slice ( 0 , 66 ) . toString ( 'hex' ) ;
1261+
1262+ const keyPair = new KeyPairLib ( { pub : publicKey } ) ;
1263+ const address = keyPair . getAddress ( ) ;
1264+ return address ;
1265+ }
1266+
1267+ getConsolidationAddress ( params : EthConsolidationRecoveryOptions , index : number ) : string [ ] {
1268+ const possibleConsolidationAddresses : string [ ] = [ ] ;
1269+ if ( params . walletContractAddress && params . bitgoFeeAddress ) {
1270+ const ethNetwork = this . getNetwork ( ) ;
1271+ const forwarderFactoryAddress = ethNetwork ?. walletV4ForwarderFactoryAddress as string ;
1272+ const forwarderImplementationAddress = ethNetwork ?. walletV4ForwarderImplementationAddress as string ;
1273+ try {
1274+ const forwarderAddress = this . generateForwarderAddress (
1275+ params . walletContractAddress ,
1276+ params . bitgoFeeAddress ,
1277+ forwarderFactoryAddress ,
1278+ forwarderImplementationAddress ,
1279+ index
1280+ ) ;
1281+ possibleConsolidationAddresses . push ( forwarderAddress ) ;
1282+ } catch ( e ) {
1283+ console . log ( `Failed to generate forwarder address: ${ e . message } ` ) ;
1284+ }
1285+ }
1286+
1287+ if ( params . userKey ) {
1288+ try {
1289+ const derivedAddress = this . deriveAddressFromPublicKey ( params . userKey , index ) ;
1290+ possibleConsolidationAddresses . push ( derivedAddress ) ;
1291+ } catch ( e ) {
1292+ console . log ( `Failed to generate derived address: ${ e } ` ) ;
1293+ }
1294+ }
1295+
1296+ if ( possibleConsolidationAddresses . length === 0 ) {
1297+ throw new Error (
1298+ 'Unable to generate consolidation address. Check that wallet contract address, fee address, or user key is valid.'
1299+ ) ;
1300+ }
1301+ return possibleConsolidationAddresses ;
1302+ }
1303+
1304+ async recoverConsolidations ( params : EthConsolidationRecoveryOptions ) : Promise < UnsignedBuilConsolidation > {
1305+ const isUnsignedSweep = ! params . userKey && ! params . backupKey && ! params . walletPassphrase ;
1306+ const startIdx = params . startingScanIndex || 1 ;
1307+ const endIdx = params . endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR ;
1308+
1309+ if ( ! params . walletContractAddress || params . walletContractAddress === '' ) {
1310+ throw new Error ( `Invalid wallet contract address ${ params . walletContractAddress } ` ) ;
1311+ }
1312+
1313+ if ( ! params . bitgoFeeAddress || params . bitgoFeeAddress === '' ) {
1314+ throw new Error ( `Invalid fee address ${ params . bitgoFeeAddress } ` ) ;
1315+ }
1316+
1317+ if ( startIdx < 1 || endIdx <= startIdx || endIdx - startIdx > 10 * DEFAULT_SCAN_FACTOR ) {
1318+ throw new Error (
1319+ `Invalid starting or ending index to scan for addresses. startingScanIndex: ${ startIdx } , endingScanIndex: ${ endIdx } .`
1320+ ) ;
1321+ }
1322+
1323+ const consolidatedTransactions : any [ ] = [ ] ;
1324+ let lastScanIndex = startIdx ;
1325+
1326+ for ( let i = startIdx ; i < endIdx ; i ++ ) {
1327+ const consolidationAddress = this . getConsolidationAddress ( params , i ) ;
1328+ for ( const address of consolidationAddress ) {
1329+ const recoverParams = {
1330+ apiKey : params . apiKey ,
1331+ backupKey : params . backupKey || '' ,
1332+ gasLimit : params . gasLimit ,
1333+ recoveryDestination : params . recoveryDestination || '' ,
1334+ userKey : params . userKey || '' ,
1335+ walletContractAddress : address ,
1336+ derivationSeed : '' ,
1337+ isTss : params . isTss ,
1338+ eip1559 : {
1339+ maxFeePerGas : params . eip1559 ?. maxFeePerGas || 20 ,
1340+ maxPriorityFeePerGas : params . eip1559 ?. maxPriorityFeePerGas || 200000 ,
1341+ } ,
1342+ replayProtectionOptions : {
1343+ chain : params . replayProtectionOptions ?. chain || 0 ,
1344+ hardfork : params . replayProtectionOptions ?. hardfork || 'london' ,
1345+ } ,
1346+ bitgoKey : '' ,
1347+ ignoreAddressTypes : [ ] ,
1348+ } ;
1349+ let recoveryTransaction ;
1350+ try {
1351+ recoveryTransaction = await this . recover ( recoverParams ) ;
1352+ } catch ( e ) {
1353+ if (
1354+ e . message === 'Did not find address with funds to recover' ||
1355+ e . message === 'Did not find token account to recover tokens, please check token account' ||
1356+ e . message === 'Not enough token funds to recover'
1357+ ) {
1358+ lastScanIndex = i ;
1359+ continue ;
1360+ }
1361+ throw e ;
1362+ }
1363+ if ( isUnsignedSweep ) {
1364+ consolidatedTransactions . push ( ( recoveryTransaction as MPCSweepTxs ) . txRequests [ 0 ] ) ;
1365+ } else {
1366+ consolidatedTransactions . push ( recoveryTransaction ) ;
1367+ }
1368+ }
1369+ // To avoid rate limit for etherscan
1370+ await new Promise ( ( resolve ) => setTimeout ( resolve , 1000 ) ) ;
1371+ // lastScanIndex = i;
1372+ }
1373+
1374+ if ( consolidatedTransactions . length === 0 ) {
1375+ throw new Error (
1376+ `Did not find an address with sufficient funds to recover. Please start the next scan at address index ${
1377+ lastScanIndex + 1
1378+ } .`
1379+ ) ;
1380+ }
1381+ return { transactions : consolidatedTransactions , lastScanIndex } ;
1382+ }
1383+
11951384 /**
11961385 * Builds a funds recovery transaction without BitGo for non-TSS transaction
11971386 * @param params
0 commit comments