@@ -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 ,
@@ -55,7 +56,7 @@ import { BigNumber } from 'bignumber.js';
55
56
import BN from 'bn.js' ;
56
57
import { randomBytes } from 'crypto' ;
57
58
import debugLib from 'debug' ;
58
- import { addHexPrefix , stripHexPrefix } from 'ethereumjs-util' ;
59
+ import { addHexPrefix , stripHexPrefix , bufferToHex , setLengthLeft , toBuffer } from 'ethereumjs-util' ;
59
60
import Keccak from 'keccak' ;
60
61
import _ from 'lodash' ;
61
62
import secp256k1 from 'secp256k1' ;
@@ -68,6 +69,7 @@ import {
68
69
ERC721TransferBuilder ,
69
70
getBufferedByteCode ,
70
71
getCommon ,
72
+ getCreateForwarderParamsAndTypes ,
71
73
getProxyInitcode ,
72
74
getRawDecoded ,
73
75
getToken ,
@@ -359,6 +361,33 @@ interface EthAddressCoinSpecifics extends AddressCoinSpecific {
359
361
salt ?: string ;
360
362
}
361
363
364
+ export const DEFAULT_SCAN_FACTOR = 20 ;
365
+ export interface EthConsolidationRecoveryOptions {
366
+ coinName : string ;
367
+ walletContractAddress ?: string ;
368
+ apiKey : string ;
369
+ isTss : boolean ;
370
+ userKey : string ;
371
+ backupKey : string ;
372
+ walletPassphrase ?: string ;
373
+ recoveryDestination : string ;
374
+ krsProvider ?: string ;
375
+ gasPrice ?: number ;
376
+ gasLimit ?: number ;
377
+ eip1559 ?: EIP1559 ;
378
+ replayProtectionOptions ?: ReplayProtectionOptions ;
379
+ bitgoFeeAddress ?: string ;
380
+ bitgoDestinationAddress ?: string ;
381
+ tokenContractAddress ?: string ;
382
+ intendedChain ?: string ;
383
+ common ?: EthLikeCommon . default ;
384
+ derivationSeed ?: string ;
385
+ bitgoKey : string ;
386
+ startingScanIndex : number ;
387
+ endingScanIndex : number ;
388
+ ignoreAddressTypes ?: unknown ;
389
+ }
390
+
362
391
export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
363
392
baseAddress : string ;
364
393
coinSpecific : EthAddressCoinSpecifics ;
@@ -1181,6 +1210,144 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
1181
1210
return this . recoverEthLike ( params ) ;
1182
1211
}
1183
1212
1213
+ generateForwarderAddress (
1214
+ baseAddress : string ,
1215
+ feeAddress : string ,
1216
+ forwarderFactoryAddress : string ,
1217
+ forwarderImplementationAddress : string ,
1218
+ index : number
1219
+ ) : string {
1220
+ const salt = addHexPrefix ( index . toString ( 16 ) ) ;
1221
+ const saltBuffer = setLengthLeft ( toBuffer ( salt ) , 32 ) ;
1222
+
1223
+ const { createForwarderParams, createForwarderTypes } = getCreateForwarderParamsAndTypes (
1224
+ baseAddress ,
1225
+ saltBuffer ,
1226
+ feeAddress
1227
+ ) ;
1228
+
1229
+ const calculationSalt = bufferToHex ( optionalDeps . ethAbi . soliditySHA3 ( createForwarderTypes , createForwarderParams ) ) ;
1230
+
1231
+ const initCode = getProxyInitcode ( forwarderImplementationAddress ) ;
1232
+ return calculateForwarderV1Address ( forwarderFactoryAddress , calculationSalt , initCode ) ;
1233
+ }
1234
+
1235
+ deriveAddressFromPublicKey ( publicKey : string ) : string {
1236
+ const keyPair = new KeyPairLib ( { pub : publicKey } ) ;
1237
+ const address = keyPair . getAddress ( ) ;
1238
+ return address ;
1239
+ }
1240
+
1241
+ getConsolidationAddress ( params : EthConsolidationRecoveryOptions , index : number ) : string {
1242
+ if ( params . walletContractAddress && params . bitgoFeeAddress ) {
1243
+ const ethNetwork = this . getNetwork ( ) ;
1244
+ const forwarderFactoryAddress = ethNetwork ?. walletV4ForwarderFactoryAddress as string ;
1245
+ const forwarderImplementationAddress = ethNetwork ?. walletV4ForwarderImplementationAddress as string ;
1246
+ try {
1247
+ return this . generateForwarderAddress (
1248
+ params . walletContractAddress ,
1249
+ params . bitgoFeeAddress ,
1250
+ forwarderFactoryAddress ,
1251
+ forwarderImplementationAddress ,
1252
+ index
1253
+ ) ;
1254
+ } catch ( e ) {
1255
+ console . log ( `Failed to generate forwarder address: ${ e . message } ` ) ;
1256
+ }
1257
+ }
1258
+
1259
+ if ( params . userKey ) {
1260
+ try {
1261
+ return this . deriveAddressFromPublicKey ( params . userKey ) ;
1262
+ } catch ( e ) {
1263
+ console . log ( `Failed to generate derived address: ${ e . message } ` ) ;
1264
+ }
1265
+ }
1266
+ throw new Error (
1267
+ 'Unable to generate consolidation address. Check that wallet contract address, fee address, or user key is valid.'
1268
+ ) ;
1269
+ }
1270
+
1271
+ async recoverConsolidations ( params : EthConsolidationRecoveryOptions ) : Promise < Record < string , unknown > | undefined > {
1272
+ const isUnsignedSweep = ! params . userKey && ! params . backupKey && ! params . walletPassphrase ;
1273
+ const startIdx = params . startingScanIndex || 1 ;
1274
+ const endIdx = params . endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR ;
1275
+
1276
+ if ( ! params . walletContractAddress || params . walletContractAddress === '' ) {
1277
+ throw new Error ( `Invalid wallet contract address ${ params . walletContractAddress } ` ) ;
1278
+ }
1279
+
1280
+ if ( ! params . bitgoFeeAddress || params . bitgoFeeAddress === '' ) {
1281
+ throw new Error ( `Invalid fee address ${ params . bitgoFeeAddress } ` ) ;
1282
+ }
1283
+
1284
+ if ( startIdx < 1 || endIdx <= startIdx || endIdx - startIdx > 10 * DEFAULT_SCAN_FACTOR ) {
1285
+ throw new Error (
1286
+ `Invalid starting or ending index to scan for addresses. startingScanIndex: ${ startIdx } , endingScanIndex: ${ endIdx } .`
1287
+ ) ;
1288
+ }
1289
+
1290
+ const consolidationTransactions : any [ ] = [ ] ;
1291
+ let lastScanIndex = startIdx ;
1292
+
1293
+ for ( let i = startIdx ; i < endIdx ; i ++ ) {
1294
+ const consolidationAddress = this . getConsolidationAddress ( params , i ) ;
1295
+
1296
+ const recoverParams = {
1297
+ apiKey : params . apiKey ,
1298
+ backupKey : params . backupKey ,
1299
+ gasLimit : params . gasLimit ,
1300
+ recoveryDestination : params . recoveryDestination ,
1301
+ userKey : params . userKey ,
1302
+ walletContractAddress : consolidationAddress ,
1303
+ derivationSeed : '' ,
1304
+ isTss : params . isTss ,
1305
+ eip1559 : {
1306
+ maxFeePerGas : params . eip1559 ?. maxFeePerGas || 20 ,
1307
+ maxPriorityFeePerGas : params . eip1559 ?. maxPriorityFeePerGas || 200000 ,
1308
+ } ,
1309
+ replayProtectionOptions : {
1310
+ chain : params . replayProtectionOptions ?. chain || 0 ,
1311
+ hardfork : params . replayProtectionOptions ?. hardfork || 'london' ,
1312
+ } ,
1313
+ bitgoKey : '' ,
1314
+ ignoreAddressTypes : [ ] ,
1315
+ } ;
1316
+
1317
+ let recoveryTransaction ;
1318
+ try {
1319
+ recoveryTransaction = await this . recover ( recoverParams ) ;
1320
+ } catch ( e ) {
1321
+ if (
1322
+ e . message === 'Did not find address with funds to recover' ||
1323
+ e . message === 'Did not find token account to recover tokens, please check token account' ||
1324
+ e . message === 'Not enough token funds to recover'
1325
+ ) {
1326
+ lastScanIndex = i ;
1327
+ continue ;
1328
+ }
1329
+ throw e ;
1330
+ }
1331
+
1332
+ if ( isUnsignedSweep ) {
1333
+ consolidationTransactions . push ( ( recoveryTransaction as MPCSweepTxs ) . txRequests [ 0 ] ) ;
1334
+ } else {
1335
+ consolidationTransactions . push ( recoveryTransaction ) ;
1336
+ }
1337
+
1338
+ lastScanIndex = i ;
1339
+ }
1340
+
1341
+ if ( consolidationTransactions . length === 0 ) {
1342
+ throw new Error (
1343
+ `Did not find an address with sufficient funds to recover. Please start the next scan at address index ${
1344
+ lastScanIndex + 1
1345
+ } .`
1346
+ ) ;
1347
+ }
1348
+ return { transactions : consolidationTransactions , lastScanIndex } ;
1349
+ }
1350
+
1184
1351
/**
1185
1352
* Builds a funds recovery transaction without BitGo for non-TSS transaction
1186
1353
* @param params
0 commit comments