From 0c074a923125c0528d7327e22153a351e5950d30 Mon Sep 17 00:00:00 2001 From: Akash Gianchandani Date: Thu, 22 Feb 2024 20:34:47 +0530 Subject: [PATCH 1/5] fix(abstract-eth): fix recover token method for ethlike WIN-2117 This commit adds recover token method for ethlike coins TICKET: WIN-2117 --- modules/abstract-eth/package.json | 3 +- modules/abstract-eth/src/ethLikeToken.ts | 233 ++++++++++++++++++++++- modules/abstract-eth/src/lib/utils.ts | 28 +++ 3 files changed, 260 insertions(+), 4 deletions(-) diff --git a/modules/abstract-eth/package.json b/modules/abstract-eth/package.json index 96d7155648..533287efd6 100644 --- a/modules/abstract-eth/package.json +++ b/modules/abstract-eth/package.json @@ -55,6 +55,7 @@ "ethers": "^5.1.3", "keccak": "^3.0.3", "lodash": "4.17.21", - "secp256k1": "5.0.0" + "secp256k1": "5.0.0", + "superagent": "^3.8.3" } } diff --git a/modules/abstract-eth/src/ethLikeToken.ts b/modules/abstract-eth/src/ethLikeToken.ts index 53eeda6f76..94407ac50a 100644 --- a/modules/abstract-eth/src/ethLikeToken.ts +++ b/modules/abstract-eth/src/ethLikeToken.ts @@ -2,10 +2,23 @@ * @prettier */ import { coins, EthLikeTokenConfig, tokens, EthereumNetwork as EthLikeNetwork } from '@bitgo/statics'; +import _ from 'lodash'; +import { bip32 } from '@bitgo/utxo-lib'; -import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core'; -import { TransactionBuilder as EthLikeTransactionBuilder } from './lib'; -import { AbstractEthLikeNewCoins, optionalDeps, TransactionPrebuild } from './abstractEthLikeNewCoins'; +import { BitGoBase, CoinConstructor, NamedCoinConstructor, getIsUnsignedSweep, Util } from '@bitgo/sdk-core'; +import { + TransactionBuilder as EthLikeTransactionBuilder, + TransferBuilder as EthLikeTransferBuilder, + KeyPair as KeyPairLib, +} from './lib'; +import { + AbstractEthLikeNewCoins, + optionalDeps, + TransactionPrebuild, + RecoverOptions, + RecoveryInfo, + OfflineVaultTxInfo, +} from './abstractEthLikeNewCoins'; export type CoinNames = { [network: string]: string; @@ -149,6 +162,220 @@ export class EthLikeToken extends AbstractEthLikeNewCoins { ]; } + /** + * Builds a token recovery transaction without BitGo + * @param params + * @param params.userKey {String} [encrypted] xprv + * @param params.backupKey {String} [encrypted] xprv or xpub if the xprv is held by a KRS providers + * @param params.walletPassphrase {String} used to decrypt userKey and backupKey + * @param params.walletContractAddress {String} the ETH address of the wallet contract + * @param params.recoveryDestination {String} target address to send recovered funds to + * @param params.krsProvider {String} necessary if backup key is held by KRS + * @param params.tokenContractAddress {String} contract address for token to recover + */ + async recover(params: RecoverOptions): Promise { + if (_.isUndefined(params.userKey)) { + throw new Error('missing userKey'); + } + + if (_.isUndefined(params.backupKey)) { + throw new Error('missing backupKey'); + } + + if (_.isUndefined(params.walletPassphrase) && !params.userKey.startsWith('xpub')) { + throw new Error('missing wallet passphrase'); + } + + if (_.isUndefined(params.walletContractAddress) || !this.isValidAddress(params.walletContractAddress)) { + throw new Error('invalid walletContractAddress'); + } + + if (_.isUndefined(params.tokenContractAddress) || !this.isValidAddress(params.tokenContractAddress)) { + throw new Error('invalid tokenContractAddress'); + } + + if (_.isUndefined(params.recoveryDestination) || !this.isValidAddress(params.recoveryDestination)) { + throw new Error('invalid recoveryDestination'); + } + + const isUnsignedSweep = getIsUnsignedSweep(params); + + // Clean up whitespace from entered values + let userKey = params.userKey.replace(/\s/g, ''); + const backupKey = params.backupKey.replace(/\s/g, ''); + + const gasLimit = new optionalDeps.ethUtil.BN(this.setGasLimit(params.gasLimit)); + const gasPrice = params.eip1559 + ? new optionalDeps.ethUtil.BN(params.eip1559.maxFeePerGas) + : new optionalDeps.ethUtil.BN(this.setGasPrice(params.gasPrice)); + + // Decrypt private keys from KeyCard values + if (!userKey.startsWith('xpub') && !userKey.startsWith('xprv')) { + try { + userKey = this.bitgo.decrypt({ + input: userKey, + password: params.walletPassphrase, + }); + } catch (e) { + throw new Error(`Error decrypting user keychain: ${e.message}`); + } + } + + let backupKeyAddress; + let backupSigningKey; + + if (isUnsignedSweep) { + const backupHDNode = bip32.fromBase58(backupKey); + backupSigningKey = backupHDNode.publicKey; + backupKeyAddress = `0x${optionalDeps.ethUtil.publicToAddress(backupSigningKey, true).toString('hex')}`; + } else { + let backupPrv; + + try { + backupPrv = this.bitgo.decrypt({ + input: backupKey, + password: params.walletPassphrase, + }); + } catch (e) { + throw new Error(`Error decrypting backup keychain: ${e.message}`); + } + + const keyPair = new KeyPairLib({ prv: backupPrv }); + backupSigningKey = keyPair.getKeys().prv; + if (!backupSigningKey) { + throw new Error('no private key'); + } + backupKeyAddress = keyPair.getAddress(); + } + + // Get nonce for backup key (should be 0) + let backupKeyNonce = 0; + + const result = await this.recoveryBlockchainExplorerQuery({ + module: 'account', + action: 'txlist', + address: backupKeyAddress, + }); + + const backupKeyTxList = result.result; + if (backupKeyTxList.length > 0) { + // Calculate last nonce used + const outgoingTxs = backupKeyTxList.filter((tx) => tx.from === backupKeyAddress); + backupKeyNonce = outgoingTxs.length; + } + + // get balance of backup key and make sure we can afford gas + const backupKeyBalance = await this.queryAddressBalance(backupKeyAddress); + + if (backupKeyBalance.lt(gasPrice.mul(gasLimit))) { + throw new Error( + `Backup key address ${backupKeyAddress} has balance ${backupKeyBalance.toString( + 10 + )}. This address must have a balance of at least 0.01 ETH to perform recoveries` + ); + } + + // get token balance of wallet + const txAmount = await this.queryAddressTokenBalance( + params.tokenContractAddress as string, + params.walletContractAddress + ); + + // build recipients object + const recipients = [ + { + address: params.recoveryDestination, + amount: txAmount.toString(10), + }, + ]; + + // Get sequence ID using contract call + await new Promise((resolve) => setTimeout(resolve, 1000)); + const sequenceId = await this.querySequenceId(params.walletContractAddress); + + let operationHash, signature; + if (!isUnsignedSweep) { + // Get operation hash and sign it + operationHash = this.getOperationSha3ForExecuteAndConfirm(recipients, this.getDefaultExpireTime(), sequenceId); + signature = Util.ethSignMsgHash(operationHash, Util.xprvToEthPrivateKey(userKey)); + + try { + Util.ecRecoverEthAddress(operationHash, signature); + } catch (e) { + throw new Error('Invalid signature'); + } + } + + const txInfo = { + recipient: recipients[0], + expireTime: this.getDefaultExpireTime(), + contractSequenceId: sequenceId, + operationHash: operationHash, + signature: signature, + gasLimit: gasLimit.toString(10), + tokenContractAddress: params.tokenContractAddress, + }; + + const txBuilder = this.getTransactionBuilder() as EthLikeTransactionBuilder; + txBuilder.counter(backupKeyNonce); + txBuilder.contract(params.walletContractAddress); + let txFee; + if (params.eip1559) { + txFee = { + eip1559: { + maxPriorityFeePerGas: params.eip1559.maxPriorityFeePerGas, + maxFeePerGas: params.eip1559.maxFeePerGas, + }, + }; + } else { + txFee = { fee: gasPrice.toString() }; + } + txBuilder.fee({ + ...txFee, + gasLimit: gasLimit.toString(), + }); + const transferBuilder = txBuilder.transfer() as EthLikeTransferBuilder; + transferBuilder + .coin(this.tokenConfig.type) + .amount(recipients[0].amount) + .contractSequenceId(sequenceId) + .expirationTime(this.getDefaultExpireTime()) + .to(params.recoveryDestination); + + const tx = await txBuilder.build(); + + if (isUnsignedSweep) { + const response: OfflineVaultTxInfo = { + txHex: tx.toBroadcastFormat(), + userKey, + backupKey, + coin: this.getChain(), + gasPrice: optionalDeps.ethUtil.bufferToInt(gasPrice).toFixed(), + gasLimit, + recipients: [txInfo.recipient], + walletContractAddress: tx.toJson().to, + amount: txInfo.recipient.amount, + backupKeyNonce, + eip1559: params.eip1559, + }; + _.extend(response, txInfo); + response.nextContractSequenceId = response.contractSequenceId; + return response; + } + + txBuilder + .transfer() + .coin(this.tokenConfig.type) + .key(new KeyPairLib({ prv: userKey }).getKeys().prv as string); + txBuilder.sign({ key: backupSigningKey }); + + const signedTx = await txBuilder.build(); + return { + id: signedTx.toJson().id, + tx: signedTx.toBroadcastFormat(), + }; + } + verifyCoin(txPrebuild: TransactionPrebuild): boolean { return txPrebuild.coin === this.tokenConfig.coin && txPrebuild.token === this.tokenConfig.type; } diff --git a/modules/abstract-eth/src/lib/utils.ts b/modules/abstract-eth/src/lib/utils.ts index 6adbf38511..8753b38415 100644 --- a/modules/abstract-eth/src/lib/utils.ts +++ b/modules/abstract-eth/src/lib/utils.ts @@ -1,4 +1,5 @@ import { Buffer } from 'buffer'; +import request from 'superagent'; import assert from 'assert'; import { addHexPrefix, @@ -788,3 +789,30 @@ export function decodeForwarderCreationData(data: string): ForwarderInitializati } as const; } } + +/** + * Make a query to explorer for information such as balance, token balance, solidity calls + * @param {Object} query key-value pairs of parameters to append after /api + * @param {string} token the API token to use for the request + * @param {string} explorerUrl the URL of the explorer + * @returns {Promise} response from explorer + */ +export async function recoveryBlockchainExplorerQuery( + query: Record, + explorerUrl: string, + token?: string +): Promise> { + if (token) { + query.apikey = token; + } + const response = await request.get(`${explorerUrl}/api`).query(query); + + if (!response.ok) { + throw new Error('could not reach explorer'); + } + + if (response.body.status === '0' && response.body.message === 'NOTOK') { + throw new Error('Explorer rate limit reached'); + } + return response.body; +} From 547df19605a048d763a2856fa3d60e4fc35694e9 Mon Sep 17 00:00:00 2001 From: Akash Gianchandani Date: Thu, 22 Feb 2024 20:38:42 +0530 Subject: [PATCH 2/5] test(sdk-coin-arbeth): add recover token tests WIN-2117 This commit adds tests for recovering token using unsigned sweep and non bitgo recovery TICKET: WIN-2117 --- modules/sdk-coin-arbeth/package.json | 3 +- modules/sdk-coin-arbeth/src/arbeth.ts | 24 ++-- modules/sdk-coin-arbeth/src/arbethToken.ts | 15 ++- .../sdk-coin-arbeth/test/fixtures/arbeth.ts | 6 + .../sdk-coin-arbeth/test/unit/arbethToken.ts | 105 +++++++++++++++++- 5 files changed, 130 insertions(+), 23 deletions(-) diff --git a/modules/sdk-coin-arbeth/package.json b/modules/sdk-coin-arbeth/package.json index 7f051900c1..610f82517c 100644 --- a/modules/sdk-coin-arbeth/package.json +++ b/modules/sdk-coin-arbeth/package.json @@ -46,8 +46,7 @@ "@bitgo/utxo-lib": "^9.34.0", "@ethereumjs/common": "^2.6.5", "ethereumjs-abi": "^0.6.5", - "ethereumjs-util": "7.1.5", - "superagent": "^3.8.3" + "ethereumjs-util": "7.1.5" }, "devDependencies": { "@bitgo/sdk-api": "^1.43.0", diff --git a/modules/sdk-coin-arbeth/src/arbeth.ts b/modules/sdk-coin-arbeth/src/arbeth.ts index 5676f4cf89..541b1c17df 100644 --- a/modules/sdk-coin-arbeth/src/arbeth.ts +++ b/modules/sdk-coin-arbeth/src/arbeth.ts @@ -1,10 +1,13 @@ /** * @prettier */ -import request from 'superagent'; import { BaseCoin, BitGoBase, common } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; -import { AbstractEthLikeNewCoins, TransactionBuilder as EthLikeTransactionBuilder } from '@bitgo/abstract-eth'; +import { + AbstractEthLikeNewCoins, + TransactionBuilder as EthLikeTransactionBuilder, + recoveryBlockchainExplorerQuery, +} from '@bitgo/abstract-eth'; import { TransactionBuilder } from './lib'; export class Arbeth extends AbstractEthLikeNewCoins { @@ -26,19 +29,8 @@ export class Arbeth extends AbstractEthLikeNewCoins { * @returns {Promise} response from Arbiscan */ async recoveryBlockchainExplorerQuery(query: Record): Promise> { - const token = common.Environments[this.bitgo.getEnv()].arbiscanApiToken; - if (token) { - query.apikey = token; - } - const response = await request.get(common.Environments[this.bitgo.getEnv()].arbiscanBaseUrl + '/api').query(query); - - if (!response.ok) { - throw new Error('could not reach Arbiscan'); - } - - if (response.body.status === '0' && response.body.message === 'NOTOK') { - throw new Error('Arbiscan rate limit reached'); - } - return response.body; + const apiToken = common.Environments[this.bitgo.getEnv()].arbiscanApiToken; + const explorerUrl = common.Environments[this.bitgo.getEnv()].arbiscanBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); } } diff --git a/modules/sdk-coin-arbeth/src/arbethToken.ts b/modules/sdk-coin-arbeth/src/arbethToken.ts index 322dd3440b..fb57602a85 100644 --- a/modules/sdk-coin-arbeth/src/arbethToken.ts +++ b/modules/sdk-coin-arbeth/src/arbethToken.ts @@ -2,8 +2,8 @@ * @prettier */ import { EthLikeTokenConfig, coins } from '@bitgo/statics'; -import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core'; -import { CoinNames, EthLikeToken } from '@bitgo/abstract-eth'; +import { BitGoBase, CoinConstructor, NamedCoinConstructor, common } from '@bitgo/sdk-core'; +import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth'; import { TransactionBuilder } from './lib'; export { EthLikeTokenConfig }; @@ -29,6 +29,17 @@ export class ArbethToken extends EthLikeToken { return new TransactionBuilder(coins.get(this.getBaseChain())); } + /** + * Make a query to Arbiscan for information such as balance, token balance, solidity calls + * @param {Object} query key-value pairs of parameters to append after /api + * @returns {Promise} response from Arbiscan + */ + async recoveryBlockchainExplorerQuery(query: Record): Promise> { + const apiToken = common.Environments[this.bitgo.getEnv()].arbiscanApiToken; + const explorerUrl = common.Environments[this.bitgo.getEnv()].arbiscanBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); + } + getFullName(): string { return 'Arbeth Token'; } diff --git a/modules/sdk-coin-arbeth/test/fixtures/arbeth.ts b/modules/sdk-coin-arbeth/test/fixtures/arbeth.ts index 47fd0f29f1..556daff36f 100644 --- a/modules/sdk-coin-arbeth/test/fixtures/arbeth.ts +++ b/modules/sdk-coin-arbeth/test/fixtures/arbeth.ts @@ -30,6 +30,12 @@ export function getTokenBalanceRequest(tokenContractAddress: string, address: st }; } +export const getTokenBalanceResponse = { + status: '1', + message: 'OK', + result: '9999999999999999948', +}; + export const getBalanceResponse = { status: '1', message: 'OK', diff --git a/modules/sdk-coin-arbeth/test/unit/arbethToken.ts b/modules/sdk-coin-arbeth/test/unit/arbethToken.ts index f780171542..42392f350f 100644 --- a/modules/sdk-coin-arbeth/test/unit/arbethToken.ts +++ b/modules/sdk-coin-arbeth/test/unit/arbethToken.ts @@ -1,13 +1,20 @@ -import 'should'; - +import * as should from 'should'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; -import { ArbethToken } from '../../src'; +import nock from 'nock'; +import { OfflineVaultTxInfo, optionalDeps } from '@bitgo/abstract-eth'; import { BitGoAPI } from '@bitgo/sdk-api'; +import { ArbethToken } from '../../src'; +import * as mockData from '../fixtures/arbeth'; + describe('Arbeth Token:', function () { let bitgo: TestBitGoAPI; let arbethTokenCoin; + const baseUrl = 'https://api-sepolia.arbiscan.io'; const tokenName = 'tarbeth:link'; + const walletContractAddress = '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e'; + const tokenContractAddress = '0xe5b6c29411b3ad31c3613bba0145293fc9957256'; + const recipientAddress = '0xa9c34eb3d3631501de56d9cfc5363f9335cfcff6'; before(function () { bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' }); @@ -34,4 +41,96 @@ describe('Arbeth Token:', function () { const tokencoinBycontractAddress = bitgo.coin(arbethTokenCoin.tokenContractAddress); arbethTokenCoin.should.deepEqual(tokencoinBycontractAddress); }); + + it('should generate an unsigned sweep', async function () { + const userXpub = + 'xpub661MyMwAqRbcEeTc8789MK5PUGEYiPG4F4V17n2Rd2LoTATA1XoCnJT5FAYAShQxSxtFjpo5NHmcWwTp2LiWGBMwpUcAA3HywhxivgYfq7q'; + const backupXpub = + 'xpub661MyMwAqRbcFZX15xpZf4ERCGHiVSJm8r5C4yh1yXV2GrdZCUPYo4WQr6tN9oUywKXsgSHo7Risf9r22GH5joVD2hEEEhqnSCvK8qy11wW'; + + const backupKeyAddress = '0x4f2c4830cc37f2785c646f89ded8a919219fa0e9'; + nock(baseUrl) + .get('/api') + .twice() + .query(mockData.getTxListRequest(backupKeyAddress)) + .reply(200, mockData.getTxListResponse); + nock(baseUrl) + .get('/api') + .query(mockData.getTokenBalanceRequest(tokenContractAddress, walletContractAddress)) + .reply(200, mockData.getTokenBalanceResponse); + nock(baseUrl) + .get('/api') + .query(mockData.getBalanceRequest(backupKeyAddress)) + .reply(200, mockData.getBalanceResponse); + nock(baseUrl).get('/api').query(mockData.getContractCallRequest).reply(200, mockData.getContractCallResponse); + const transaction = (await arbethTokenCoin.recover({ + userKey: userXpub, + backupKey: backupXpub, + walletContractAddress: walletContractAddress, + tokenContractAddress, + recoveryDestination: recipientAddress, + eip1559: { maxFeePerGas: 20000000000, maxPriorityFeePerGas: 10000000000 }, + gasLimit: 8000000, + })) as OfflineVaultTxInfo; + should.exist(transaction); + transaction.should.have.property('txHex'); + transaction.should.have.property('contractSequenceId'); + transaction.should.have.property('expireTime'); + transaction.should.have.property('gasLimit'); + transaction.gasLimit.should.equal('8000000'); + transaction.should.have.property('walletContractAddress'); + transaction.walletContractAddress.should.equal(walletContractAddress); + transaction.should.have.property('recipients'); + const recipient = transaction.recipients[0]; + recipient.should.have.property('address'); + recipient.address.should.equal(recipientAddress); + recipient.should.have.property('amount'); + recipient.amount.should.equal('9999999999999999948'); + }); + + it('should construct a recovery transaction without BitGo', async function () { + const backupKeyAddress = '0x6d22efdd634996248170c948e5726007fc251bb3'; + nock(baseUrl).get('/api').query(mockData.getTxListRequest(backupKeyAddress)).reply(200, mockData.getTxListResponse); + nock(baseUrl) + .get('/api') + .query(mockData.getBalanceRequest(walletContractAddress)) + .reply(200, mockData.getBalanceResponse); + + nock(baseUrl) + .get('/api') + .query(mockData.getTokenBalanceRequest(tokenContractAddress, walletContractAddress)) + .reply(200, mockData.getTokenBalanceResponse); + nock(baseUrl) + .get('/api') + .query(mockData.getBalanceRequest(backupKeyAddress)) + .reply(200, mockData.getBalanceResponse); + nock(baseUrl).get('/api').query(mockData.getContractCallRequest).reply(200, mockData.getContractCallResponse); + + const transaction = (await arbethTokenCoin.recover({ + userKey: + '{"iv":"VFZ3jvXhxo1Z+Yaf2MtZnA==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' + + ':"ccm","adata":"","cipher":"aes","salt":"p+fkHuLa/8k=","ct":"hYG7pvljLIgCjZ\n' + + '53PBlCde5KZRmlUKKHLtDMk+HJfuU46hW+x+C9WsIAO4gFPnTCvFVmQ8x7czCtcNFub5AO2otOG\n' + + 'OsX4GE2gXOEmCl1TpWwwNhm7yMUjGJUpgW6ZZgXSXdDitSKi4V/hk78SGSzjFOBSPYRa6I="}\n', + backupKey: + '{"iv":"AbsCtv1qwPIhOgyrCpNagA==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' + + ':"ccm","adata":"","cipher":"aes","salt":"5vpUDBUlzm8=","ct":"PapYYCjBXRLUKA\n' + + 'JbOsB/EJ9B8fUmVQTxMPjUnQyAky12me9K66GiMEAxTD7kd6bYAQJuuTkATXKU7Bnf7vK9JxNOw\n' + + 'oji7HF9eFH0aD4/hX5SWFfHF2Qfi+TnXv6hVsMAoisDZs3/F67/ZUaDYR0ZsdrQ4Q/cLD0="}\n', + + walletContractAddress: walletContractAddress, + tokenContractAddress, + walletPassphrase: 'oPXkPN5Q0c8i44i0', + recoveryDestination: recipientAddress, + gasLimit: 500000, + })) as OfflineVaultTxInfo; + should.exist(transaction); + transaction.should.have.property('tx'); + transaction.should.have.property('id'); + const decodedTx = optionalDeps.EthTx.Transaction.fromSerializedTx(optionalDeps.ethUtil.toBuffer(transaction.tx)); + decodedTx.should.have.property('gasPrice'); + decodedTx.should.have.property('nonce'); + decodedTx.should.have.property('to'); + decodedTx.data.toString('hex').should.startWith('0dcd7a6c'); + }); }); From 0f630bce278d6a3230dc57ba5a67af1adea2ea99 Mon Sep 17 00:00:00 2001 From: Akash Gianchandani Date: Thu, 22 Feb 2024 20:40:45 +0530 Subject: [PATCH 3/5] refactor(sdk-coin-opeth): refactor recovery related method WIN-2117 This commit refactors the token recovery related method TICKET: WIN-2117 --- modules/sdk-coin-opeth/package.json | 3 +-- modules/sdk-coin-opeth/src/opeth.ts | 27 +++++++----------------- modules/sdk-coin-opeth/src/opethToken.ts | 15 +++++++++++-- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/modules/sdk-coin-opeth/package.json b/modules/sdk-coin-opeth/package.json index cd7219d6b2..efd40b2eeb 100644 --- a/modules/sdk-coin-opeth/package.json +++ b/modules/sdk-coin-opeth/package.json @@ -46,8 +46,7 @@ "@bitgo/utxo-lib": "^9.34.0", "@ethereumjs/common": "^2.6.5", "ethereumjs-abi": "^0.6.5", - "ethereumjs-util": "7.1.5", - "superagent": "^3.8.3" + "ethereumjs-util": "7.1.5" }, "devDependencies": { "@bitgo/sdk-api": "^1.43.0", diff --git a/modules/sdk-coin-opeth/src/opeth.ts b/modules/sdk-coin-opeth/src/opeth.ts index 4b8bbe1307..710579b191 100644 --- a/modules/sdk-coin-opeth/src/opeth.ts +++ b/modules/sdk-coin-opeth/src/opeth.ts @@ -1,11 +1,13 @@ /** * @prettier */ - -import request from 'superagent'; import { BaseCoin, BitGoBase, common } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; -import { AbstractEthLikeNewCoins, TransactionBuilder as EthLikeTransactionBuilder } from '@bitgo/abstract-eth'; +import { + AbstractEthLikeNewCoins, + TransactionBuilder as EthLikeTransactionBuilder, + recoveryBlockchainExplorerQuery, +} from '@bitgo/abstract-eth'; import { TransactionBuilder } from './lib'; export class Opeth extends AbstractEthLikeNewCoins { @@ -27,21 +29,8 @@ export class Opeth extends AbstractEthLikeNewCoins { * @returns {Promise} response from Optimism Etherscan */ async recoveryBlockchainExplorerQuery(query: Record): Promise> { - const token = common.Environments[this.bitgo.getEnv()].optimisticEtherscanApiToken; - if (token) { - query.apikey = token; - } - const response = await request - .get(common.Environments[this.bitgo.getEnv()].optimisticEtherscanBaseUrl + '/api') - .query(query); - - if (!response.ok) { - throw new Error('could not reach Optimism Etherscan'); - } - - if (response.body.status === '0' && response.body.message === 'NOTOK') { - throw new Error('Optimism Etherscan rate limit reached'); - } - return response.body; + const apiToken = common.Environments[this.bitgo.getEnv()].optimisticEtherscanApiToken; + const explorerUrl = common.Environments[this.bitgo.getEnv()].optimisticEtherscanBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); } } diff --git a/modules/sdk-coin-opeth/src/opethToken.ts b/modules/sdk-coin-opeth/src/opethToken.ts index ae61475355..54612875fe 100644 --- a/modules/sdk-coin-opeth/src/opethToken.ts +++ b/modules/sdk-coin-opeth/src/opethToken.ts @@ -2,8 +2,8 @@ * @prettier */ import { EthLikeTokenConfig, coins } from '@bitgo/statics'; -import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core'; -import { CoinNames, EthLikeToken } from '@bitgo/abstract-eth'; +import { BitGoBase, CoinConstructor, NamedCoinConstructor, common } from '@bitgo/sdk-core'; +import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth'; import { TransactionBuilder } from './lib'; export { EthLikeTokenConfig }; @@ -29,6 +29,17 @@ export class OpethToken extends EthLikeToken { return new TransactionBuilder(coins.get(this.getBaseChain())); } + /** + * Make a query to Optimism Etherscan for information such as balance, token balance, solidity calls + * @param {Object} query key-value pairs of parameters to append after /api + * @returns {Promise} response from Optimism Etherscan + */ + async recoveryBlockchainExplorerQuery(query: Record): Promise> { + const apiToken = common.Environments[this.bitgo.getEnv()].optimisticEtherscanApiToken; + const explorerUrl = common.Environments[this.bitgo.getEnv()].optimisticEtherscanBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); + } + getFullName(): string { return 'Opeth Token'; } From 806c8747ce4df0c21c11bfb6e5a310e133f5861a Mon Sep 17 00:00:00 2001 From: Akash Gianchandani Date: Thu, 22 Feb 2024 20:45:59 +0530 Subject: [PATCH 4/5] refactor(sdk-coin-polygon): refactor recover token related methods WIN-2117 This commit refactors token recovery related methods for polygon TICKET: WIN-2117 --- modules/sdk-coin-polygon/package.json | 3 +-- modules/sdk-coin-polygon/src/polygon.ts | 28 +++++--------------- modules/sdk-coin-polygon/src/polygonToken.ts | 15 +++++++++-- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/modules/sdk-coin-polygon/package.json b/modules/sdk-coin-polygon/package.json index 91b56e4272..e4bb955b8b 100644 --- a/modules/sdk-coin-polygon/package.json +++ b/modules/sdk-coin-polygon/package.json @@ -45,8 +45,7 @@ "@bitgo/sjcl": "^1.0.1", "@bitgo/statics": "^47.0.0", "@bitgo/utxo-lib": "^9.34.0", - "@ethereumjs/common": "^2.6.5", - "superagent": "^3.8.3" + "@ethereumjs/common": "^2.6.5" }, "devDependencies": { "@bitgo/sdk-api": "^1.43.0", diff --git a/modules/sdk-coin-polygon/src/polygon.ts b/modules/sdk-coin-polygon/src/polygon.ts index 5dd4f96e12..1296f333ff 100644 --- a/modules/sdk-coin-polygon/src/polygon.ts +++ b/modules/sdk-coin-polygon/src/polygon.ts @@ -1,8 +1,7 @@ /** * @prettier */ -import request from 'superagent'; -import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth'; +import { AbstractEthLikeNewCoins, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth'; import { BaseCoin, BitGoBase, common, MPCAlgorithm } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; import { TransactionBuilder } from './lib'; @@ -25,27 +24,14 @@ export class Polygon extends AbstractEthLikeNewCoins { } /** - * Make a query to Polygonscan for information such as balance, token balance, solidity calls + * Make a query to Polygon explorer for information such as balance, token balance, solidity calls * @param {Object} query key-value pairs of parameters to append after /api - * @returns {Promise} response from Polygonscan + * @returns {Promise} response from Polygon */ - async recoveryBlockchainExplorerQuery(query: Record): Promise { - const token = common.Environments[this.bitgo.getEnv()].polygonscanApiToken; - if (token) { - query.apikey = token; - } - const response = await request - .get(common.Environments[this.bitgo.getEnv()].polygonscanBaseUrl + '/api') - .query(query); - - if (!response.ok) { - throw new Error('could not reach Polygonscan'); - } - - if (response.body.status === '0' && response.body.message === 'NOTOK') { - throw new Error('Polygonscan rate limit reached'); - } - return response.body; + async recoveryBlockchainExplorerQuery(query: Record): Promise> { + const apiToken = common.Environments[this.bitgo.getEnv()].polygonscanApiToken; + const explorerUrl = common.Environments[this.bitgo.getEnv()].polygonscanBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); } /** @inheritDoc */ diff --git a/modules/sdk-coin-polygon/src/polygonToken.ts b/modules/sdk-coin-polygon/src/polygonToken.ts index b38fd05c0b..006022ca4a 100644 --- a/modules/sdk-coin-polygon/src/polygonToken.ts +++ b/modules/sdk-coin-polygon/src/polygonToken.ts @@ -1,9 +1,9 @@ /** * @prettier */ -import { EthLikeToken, CoinNames } from '@bitgo/abstract-eth'; +import { EthLikeToken, CoinNames, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth'; import { EthLikeTokenConfig, coins } from '@bitgo/statics'; -import { BitGoBase, CoinConstructor, MPCAlgorithm, NamedCoinConstructor } from '@bitgo/sdk-core'; +import { BitGoBase, CoinConstructor, MPCAlgorithm, NamedCoinConstructor, common } from '@bitgo/sdk-core'; import { TransactionBuilder } from './lib'; export { EthLikeTokenConfig }; @@ -29,6 +29,17 @@ export class PolygonToken extends EthLikeToken { return new TransactionBuilder(coins.get(this.getBaseChain())); } + /** + * Make a query to Polygonscan for information such as balance, token balance, solidity calls + * @param {Object} query key-value pairs of parameters to append after /api + * @returns {Promise} response from Polygonscan + */ + async recoveryBlockchainExplorerQuery(query: Record): Promise> { + const apiToken = common.Environments[this.bitgo.getEnv()].polygonscanApiToken; + const explorerUrl = common.Environments[this.bitgo.getEnv()].polygonscanBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken); + } + /** @inheritDoc */ supportsTss(): boolean { return true; From 412e67b87d2c8819d6bd3e45af71afbf5853edf3 Mon Sep 17 00:00:00 2001 From: Akash Gianchandani Date: Thu, 22 Feb 2024 20:47:00 +0530 Subject: [PATCH 5/5] refactor(sdk-coin-zketh): refactor recover token related methods WIN-2117 TICKET: WIN-2117 --- modules/sdk-coin-zketh/package.json | 3 +-- modules/sdk-coin-zketh/src/zketh.ts | 25 +++++++++--------------- modules/sdk-coin-zketh/src/zkethToken.ts | 14 +++++++++++-- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/modules/sdk-coin-zketh/package.json b/modules/sdk-coin-zketh/package.json index 36d0ba3b90..231bff14e4 100644 --- a/modules/sdk-coin-zketh/package.json +++ b/modules/sdk-coin-zketh/package.json @@ -44,8 +44,7 @@ "@bitgo/sdk-core": "^26.0.0", "@bitgo/statics": "^47.0.0", "@bitgo/utxo-lib": "^9.34.0", - "@ethereumjs/common": "^2.6.5", - "superagent": "^3.8.3" + "@ethereumjs/common": "^2.6.5" }, "devDependencies": { "@bitgo/sdk-api": "^1.43.0", diff --git a/modules/sdk-coin-zketh/src/zketh.ts b/modules/sdk-coin-zketh/src/zketh.ts index 976044fd65..060f15ab3f 100644 --- a/modules/sdk-coin-zketh/src/zketh.ts +++ b/modules/sdk-coin-zketh/src/zketh.ts @@ -1,10 +1,13 @@ /** * @prettier */ -import request from 'superagent'; import { BaseCoin, BitGoBase, common } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; -import { AbstractEthLikeNewCoins, TransactionBuilder as EthLikeTransactionBuilder } from '@bitgo/abstract-eth'; +import { + AbstractEthLikeNewCoins, + TransactionBuilder as EthLikeTransactionBuilder, + recoveryBlockchainExplorerQuery, +} from '@bitgo/abstract-eth'; import { TransactionBuilder } from './lib'; export class Zketh extends AbstractEthLikeNewCoins { @@ -21,22 +24,12 @@ export class Zketh extends AbstractEthLikeNewCoins { } /** - * Make a query to zkSync explorer for information such as balance, token balance, solidity calls + * Make a query to Zksync explorer for information such as balance, token balance, solidity calls * @param {Object} query key-value pairs of parameters to append after /api - * @returns {Promise} response from zkSync explorer + * @returns {Promise} response from Zksync explorer */ async recoveryBlockchainExplorerQuery(query: Record): Promise> { - const response = await request - .get(common.Environments[this.bitgo.getEnv()].zksyncExplorerBaseUrl + '/api') - .query(query); - - if (!response.ok) { - throw new Error('could not reach zkSync explorer'); - } - - if (response.body.status === '0' && response.body.message === 'NOTOK') { - throw new Error('zkSync explorer rate limit reached'); - } - return response.body; + const explorerUrl = common.Environments[this.bitgo.getEnv()].zksyncExplorerBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string); } } diff --git a/modules/sdk-coin-zketh/src/zkethToken.ts b/modules/sdk-coin-zketh/src/zkethToken.ts index e0a1f426df..3d92c02ea0 100644 --- a/modules/sdk-coin-zketh/src/zkethToken.ts +++ b/modules/sdk-coin-zketh/src/zkethToken.ts @@ -2,8 +2,8 @@ * @prettier */ import { EthLikeTokenConfig, coins } from '@bitgo/statics'; -import { BitGoBase, CoinConstructor, NamedCoinConstructor } from '@bitgo/sdk-core'; -import { CoinNames, EthLikeToken } from '@bitgo/abstract-eth'; +import { BitGoBase, CoinConstructor, NamedCoinConstructor, common } from '@bitgo/sdk-core'; +import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth'; import { TransactionBuilder } from './lib'; export { EthLikeTokenConfig }; @@ -29,6 +29,16 @@ export class ZkethToken extends EthLikeToken { return new TransactionBuilder(coins.get(this.getBaseChain())); } + /** + * Make a query to Zksync explorer for information such as balance, token balance, solidity calls + * @param {Object} query key-value pairs of parameters to append after /api + * @returns {Promise} response from Zksync explorer + */ + async recoveryBlockchainExplorerQuery(query: Record): Promise> { + const explorerUrl = common.Environments[this.bitgo.getEnv()].zksyncExplorerBaseUrl; + return await recoveryBlockchainExplorerQuery(query, explorerUrl as string); + } + getFullName(): string { return 'Zketh Token'; }