diff --git a/modules/sdk-coin-avaxc/package.json b/modules/sdk-coin-avaxc/package.json index 2d47eb642e..d31d269fcc 100644 --- a/modules/sdk-coin-avaxc/package.json +++ b/modules/sdk-coin-avaxc/package.json @@ -52,7 +52,9 @@ "keccak": "^3.0.3", "lodash": "^4.17.14", "secp256k1": "5.0.0", - "superagent": "^9.0.1" + "superagent": "^9.0.1", + "@bitgo/abstract-eth": "^22.1.1", + "bn.js": "^5.0.0" }, "devDependencies": { "@bitgo/sdk-api": "^1.54.2", diff --git a/modules/sdk-coin-avaxc/src/avaxc.ts b/modules/sdk-coin-avaxc/src/avaxc.ts index 8a0d012fa7..317e9c248d 100644 --- a/modules/sdk-coin-avaxc/src/avaxc.ts +++ b/modules/sdk-coin-avaxc/src/avaxc.ts @@ -62,11 +62,12 @@ import { VerifyAvaxcTransactionOptions, } from './iface'; import { AvaxpLib } from '@bitgo/sdk-coin-avaxp'; +import { AvaxcRecovery } from './avaxcRecovery'; export class AvaxC extends BaseCoin { static hopTransactionSalt = 'bitgoHopAddressRequestSalt'; - protected readonly _staticsCoin: Readonly; + private ethLikeAvaxc: any; protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { super(bitgo); @@ -74,8 +75,8 @@ export class AvaxC extends BaseCoin { if (!staticsCoin) { throw new Error('missing required constructor parameter staticsCoin'); } - this._staticsCoin = staticsCoin; + this.ethLikeAvaxc = AvaxcRecovery.createInstance(bitgo, staticsCoin); } static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { @@ -578,6 +579,9 @@ export class AvaxC extends BaseCoin { throw new Error('invalid recoveryDestination'); } + // COIN-1708 : Recover method to be invoked from WRW + if (params.bitgoFeeAddress) return this.ethLikeAvaxc.recoverEthLikeforEvmBasedRecovery(params); + // TODO (BG-56531): add support for krs const isUnsignedSweep = getIsUnsignedSweep(params); diff --git a/modules/sdk-coin-avaxc/src/avaxcRecovery.ts b/modules/sdk-coin-avaxc/src/avaxcRecovery.ts new file mode 100644 index 0000000000..e5737a648b --- /dev/null +++ b/modules/sdk-coin-avaxc/src/avaxcRecovery.ts @@ -0,0 +1,115 @@ +/** + * @prettier + */ +import { BaseCoin, BitGoBase, common } from '@bitgo/sdk-core'; +import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; +import { + AbstractEthLikeNewCoins, + optionalDeps, + TransactionBuilder as EthLikeTransactionBuilder, +} from '@bitgo/abstract-eth'; +import { TransactionBuilder } from './lib'; +import request from 'superagent'; +import BN from 'bn.js'; +import { Buffer } from 'buffer'; + +export class AvaxcRecovery extends AbstractEthLikeNewCoins { + protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly) { + super(bitgo, staticsCoin); + } + + static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly): BaseCoin { + return new AvaxcRecovery(bitgo, staticsCoin); + } + + protected getTransactionBuilder(): EthLikeTransactionBuilder { + return new TransactionBuilder(coins.get(this.getBaseChain())); + } + + async recoveryBlockchainExplorerQuery(query: Record): Promise { + console.log('Inside : recoveryBlockchainExplorerQuery \n'); + const env = this.bitgo.getEnv(); + const response = await request.post(common.Environments[env].avaxcNetworkBaseUrl + '/ext/bc/C/rpc').send(query); + console.log('Inside : recoveryBlockchainExplorerQuery : Got Response \n'); + + if (!response.ok) { + throw new Error('could not reach avax.network'); + } + + if (response.body.status === '0' && response.body.message === 'NOTOK') { + throw new Error('avax.network rate limit reached'); + } + return response.body; + } + + async getAddressNonce(address: string): Promise { + // Get nonce for backup key (should be 0) + const result = await this.recoveryBlockchainExplorerQuery({ + jsonrpc: '2.0', + method: 'eth_getTransactionCount', + params: [address, 'latest'], + id: 1, + }); + if (!result || isNaN(result.result)) { + throw new Error('Unable to find next nonce from avax.network, got: ' + JSON.stringify(result)); + } + const nonceHex = result.result; + return new optionalDeps.ethUtil.BN(nonceHex.slice(2), 16).toNumber(); + } + + async queryAddressBalance(address: string): Promise { + const result = await this.recoveryBlockchainExplorerQuery({ + jsonrpc: '2.0', + method: 'eth_getBalance', + params: [address, 'latest'], + id: 1, + }); + // throw if the result does not exist or the result is not a valid number + if (!result || !result.result || isNaN(result.result)) { + throw new Error(`Could not obtain address balance for ${address} from avax.network, got: ${result.result}`); + } + const nativeBalanceHex = result.result; + return new optionalDeps.ethUtil.BN(nativeBalanceHex.slice(2), 16); + } + + async querySequenceId(address: string): Promise { + // Get sequence ID using contract call + const sequenceIdMethodSignature = optionalDeps.ethAbi.methodID('getNextSequenceId', []); + const sequenceIdArgs = optionalDeps.ethAbi.rawEncode([], []); + const sequenceIdData = Buffer.concat([sequenceIdMethodSignature, sequenceIdArgs]).toString('hex'); + const sequenceIdDataHex = optionalDeps.ethUtil.addHexPrefix(sequenceIdData); + const result = await this.recoveryBlockchainExplorerQuery({ + jsonrpc: '2.0', + method: 'eth_call', + params: [{ to: address, data: sequenceIdDataHex }, 'latest'], + id: 1, + }); + if (!result || !result.result) { + throw new Error('Could not obtain sequence ID from avax.network, got: ' + result.result); + } + const sequenceIdHex = result.result; + return new optionalDeps.ethUtil.BN(sequenceIdHex.slice(2), 16).toNumber(); + } + + async getGasPriceFromExternalAPI(): Promise { + try { + // COIN -1708 : hardcoded for half signing + const gasPrice = new BN(250000); + console.log(` Got hardcoded gas price: ${gasPrice}`); + return gasPrice; + } catch (e) { + throw new Error('Failed to get gas price'); + } + } + + async getGasLimitFromExternalAPI(from: string, to: string, data: string): Promise { + try { + // COIN -1708 : hardcoded for half signing + const gasLimit = new BN(250000); + console.log(`Got hardcoded gas limit: ${gasLimit}`); + return gasLimit; + } catch (e) { + throw new Error('Failed to get gas limit: '); + } + } +}