@@ -19,6 +19,7 @@ import {
19
19
IWallet ,
20
20
KeyPair ,
21
21
MPCSweepRecoveryOptions ,
22
+ MPCSweepTxs ,
22
23
MPCTx ,
23
24
MPCTxs ,
24
25
ParsedTransaction ,
@@ -57,7 +58,7 @@ import { BigNumber } from 'bignumber.js';
57
58
import BN from 'bn.js' ;
58
59
import { randomBytes } from 'crypto' ;
59
60
import debugLib from 'debug' ;
60
- import { addHexPrefix , bufArrToArr , stripHexPrefix } from 'ethereumjs-util' ;
61
+ import { addHexPrefix , bufArrToArr , stripHexPrefix , bufferToHex , setLengthLeft , toBuffer } from 'ethereumjs-util' ;
61
62
import Keccak from 'keccak' ;
62
63
import _ from 'lodash' ;
63
64
import secp256k1 from 'secp256k1' ;
@@ -70,6 +71,7 @@ import {
70
71
ERC721TransferBuilder ,
71
72
getBufferedByteCode ,
72
73
getCommon ,
74
+ getCreateForwarderParamsAndTypes ,
73
75
getProxyInitcode ,
74
76
getRawDecoded ,
75
77
getToken ,
@@ -224,6 +226,11 @@ export type UnsignedSweepTxMPCv2 = {
224
226
} [ ] ;
225
227
} ;
226
228
229
+ export type UnsignedBuilConsolidation = {
230
+ transactions : MPCSweepTxs [ ] | UnsignedSweepTxMPCv2 [ ] | RecoveryInfo [ ] | OfflineVaultTxInfo [ ] ;
231
+ lastScanIndex : number ;
232
+ } ;
233
+
227
234
export type RecoverOptionsWithBytes = {
228
235
isTss : true ;
229
236
/**
@@ -361,6 +368,33 @@ interface EthAddressCoinSpecifics extends AddressCoinSpecific {
361
368
salt ?: string ;
362
369
}
363
370
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
+
364
398
export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
365
399
baseAddress : string ;
366
400
coinSpecific : EthAddressCoinSpecifics ;
@@ -1192,6 +1226,161 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
1192
1226
return this . recoverEthLike ( params ) ;
1193
1227
}
1194
1228
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
+
1195
1384
/**
1196
1385
* Builds a funds recovery transaction without BitGo for non-TSS transaction
1197
1386
* @param params
0 commit comments