diff --git a/src/__tests__/api/master/musigRecovery.test.ts b/src/__tests__/api/master/musigRecovery.test.ts index 70647e7f..1a8aaf5f 100644 --- a/src/__tests__/api/master/musigRecovery.test.ts +++ b/src/__tests__/api/master/musigRecovery.test.ts @@ -1,12 +1,12 @@ import 'should'; import sinon from 'sinon'; -import { AbstractEthLikeNewCoins } from '@bitgo-beta/abstract-eth'; import nock from 'nock'; import * as request from 'supertest'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; import { data as ethRecoveryData } from '../../mocks/ethRecoveryMusigMockData'; +import { BitGoAPITestHarness } from './testUtils'; describe('POST /api/v1/:coin/advancedwallet/recovery', () => { let agent: request.SuperAgentTest; @@ -41,23 +41,51 @@ describe('POST /api/v1/:coin/advancedwallet/recovery', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); it('should get the tx hex for broadcasting from eve on musig recovery ', async () => { - // sdk call mock on mbe - const recoverStub = sinon - .stub(AbstractEthLikeNewCoins.prototype, 'recover') - .resolves(ethRecoveryData.unsignedSweepPrebuildTx); + const backupKeyAddress = '0x30edc88a77598833f58947638b2ac3d5713d9845'; + const apiKey = 'etherscan-api-token'; + const etherscanBase = 'https://api.etherscan.io'; + const chainid = '560048'; + + // Etherscan calls to get the nonce, balance, and sequence ID for the backup key and wallet contract + const txlistNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=account&action=txlist&address=${backupKeyAddress}&apikey=${apiKey}`, + ) + .twice() + .reply(200, { result: [] }); + + const backupBalanceNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=account&action=balance&address=${backupKeyAddress}&apikey=${apiKey}`, + ) + .reply(200, { result: '10000000000000000' }); + + const walletBalanceNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=account&action=balance&address=${ethRecoveryData.walletContractAddress}&apikey=${apiKey}`, + ) + .reply(200, { result: '1000000000000000000' }); + + const sequenceIdNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=proxy&action=eth_call&to=${ethRecoveryData.walletContractAddress}&data=a0b7967b&tag=latest&apikey=${apiKey}`, + ) + .reply(200, { + result: '0x0000000000000000000000000000000000000000000000000000000000000001', + }); - // the call to eve.recoverWallet(...) - // that contains the calls to sdk.signTransaction const eveRecoverWalletNock = nock(advancedWalletManagerUrl) - .post(`/api/${coin}/multisig/recovery`, { - userPub: ethRecoveryData.userKey, - backupPub: ethRecoveryData.backupKey, - unsignedSweepPrebuildTx: ethRecoveryData.unsignedSweepPrebuildTx, - coinSpecificParams: undefined, - walletContractAddress: ethRecoveryData.walletContractAddress, + .post(`/api/${coin}/multisig/recovery`, (body) => { + return ( + body.userPub === ethRecoveryData.userKey && + body.backupPub === ethRecoveryData.backupKey && + body.walletContractAddress === ethRecoveryData.walletContractAddress && + body.unsignedSweepPrebuildTx !== undefined + ); }) .reply(200, { txHex: ethRecoveryData.txHexFullSigned, @@ -74,13 +102,16 @@ describe('POST /api/v1/:coin/advancedwallet/recovery', () => { walletContractAddress: ethRecoveryData.walletContractAddress, bitgoPub: '', }, - apiKey: 'etherscan-api-token', + apiKey, recoveryDestinationAddress: ethRecoveryData.recoveryDestinationAddress, }); response.status.should.equal(200); response.body.should.have.property('txHex', ethRecoveryData.txHexFullSigned); - sinon.assert.calledOnce(recoverStub); + txlistNock.isDone().should.be.true(); + backupBalanceNock.isDone().should.be.true(); + walletBalanceNock.isDone().should.be.true(); + sequenceIdNock.isDone().should.be.true(); eveRecoverWalletNock.done(); }); diff --git a/src/__tests__/api/master/nonRecovery.test.ts b/src/__tests__/api/master/nonRecovery.test.ts index 0814299e..2a553720 100644 --- a/src/__tests__/api/master/nonRecovery.test.ts +++ b/src/__tests__/api/master/nonRecovery.test.ts @@ -1,18 +1,13 @@ import 'should'; import * as request from 'supertest'; import nock from 'nock'; +import sinon from 'sinon'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; -import sinon from 'sinon'; -import * as middleware from '../../../shared/middleware'; -import * as masterMiddleware from '../../../masterBitgoExpress/middleware/middleware'; -import { BitGoRequest } from '../../../types/request'; -import { BitGoAPI } from '@bitgo-beta/sdk-api'; -import { AdvancedWalletManagerClient } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; +import { BitGoAPITestHarness } from './testUtils'; describe('Non Recovery Tests', () => { let agent: request.SuperAgentTest; - let mockBitgo: BitGoAPI; const advancedWalletManagerUrl = 'http://advancedwalletmanager.invalid'; const accessToken = 'test-token'; const config: MasterExpressConfig = { @@ -31,21 +26,10 @@ describe('Non Recovery Tests', () => { recoveryMode: false, }; - beforeEach(() => { + before(() => { nock.disableNetConnect(); nock.enableNetConnect('127.0.0.1'); - // Create mock BitGo instance with base functionality - mockBitgo = new BitGoAPI({ env: 'test' }); - - // Setup middleware stubs before creating app - sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { - (req as BitGoRequest).bitgo = mockBitgo; - (req as BitGoRequest).config = config; - next(); - }); - - // Create app after middleware is stubbed const app = expressApp(config); agent = request.agent(app); }); @@ -53,23 +37,10 @@ describe('Non Recovery Tests', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); describe('Recovery', () => { - const coin = 'tbtc'; - - beforeEach(() => { - sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => { - (req as BitGoRequest).params = { coin }; - (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient( - config, - coin, - ); - next(); - return undefined; - }); - }); - it('should fail to run mbe recovery if not in recovery mode', async () => { const coin = 'tbtc'; const userPub = 'xpub_user'; diff --git a/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts b/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts index 3f9b4c5d..73bfda09 100644 --- a/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts +++ b/src/__tests__/api/master/recoveryConsolidationsWallet.test.ts @@ -5,8 +5,7 @@ import nock from 'nock'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; import { Trx } from '@bitgo-beta/sdk-coin-trx'; -import { Sol } from '@bitgo-beta/sdk-coin-sol'; -import { Sui } from '@bitgo-beta/sdk-coin-sui'; +import { BitGoAPITestHarness } from './testUtils'; describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { let agent: request.SuperAgentTest; @@ -14,12 +13,118 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { const accessToken = 'test-access-token'; const mockUserPub = - 'xpub661MyMwAqRbcFkPHucMnrGNzDwb6teAX1RbKQmqtEF8kK3Z7LZ59qafCjB9eCWzSgHCZkdXgp'; + 'xpub661MyMwAqRbcEtjU21VjQhGDdg5noG6kCGjcpc4EZwnLUxr9Pi56i14Eek8CQqcuGVnXQf3Zy47Uizr5WHDbZ3GumXEFXpwFLHWGbKrWWcg'; const mockBackupPub = - 'xpub661MyMwAqRbcGaZrYqfYmaTRzQxM9PKEZ7GRb6DKfghkzgjk2dKT4qBXfz6WzpT4N5fXJhFW'; + 'xpub661MyMwAqRbcEnTrcp222pRm7G1ZAbDD3KxXT2XEKRe3jnnvydqnyssewd2eUxgeWr1c1ffHcqqRKB8j3Lw9VR4dvrAhTov4kPKZF5rs6Vr'; const mockBitgoPub = - 'xpub661MyMwAqRbcF1cvdJUvQ8MV6a7R5hF5cBmVxA1zS1k7RH7NKj3X7K8fgR4kS2qY6jW9cF7L'; - const mockCommonKeychain = 'common-keychain-123'; + 'xpub661MyMwAqRbcFNUFGFmDcC3Frgtz4FnJqFdCGbzLva2hf5i3ZJuQdsGc3z5FXCVqR9NQ6h2zTyGcQkfFtsLT5St621Fcu1C22kCKhbo4kQy'; + + // ── SOL-specific constants ──────────────────────────────────────────────── + + const solRpcBase = 'https://api.devnet.solana.com'; + + const solBitgoKey = + '125746de1919236bd30a4809d718b1c161ab8f7674fe506bed438fa860adcfcc' + + '256f3721062dfeaea177c38c467a24228b9acf1a9f92fc2f5d0177bbbf218eb8'; + + const solWalletAddress2 = '22USpDwmubAoY5uws4hp4YhJZwt4eoumeLrGGx5z7DWV'; + + const solDurableNoncePubKey = '6LqY5ncj7s4b1c3YJV1hsn2hVPNhEfvDCNYMaCc1jJhX'; + const solDurableNoncePubKey2 = '4Y3kQtmVUfF7nimtABPpCwjihmLgJUgm8eZTAo44c4u9'; + const solDurableNoncePubKey3 = '6UW2N7eynvw1zjULpGDxPorJHj6wpvVgiFUcjzwoY6fg'; + const solDurableNoncePrivKey = + '447272d65cc8b39f88ea23b5f16859bd84b3ecfd6176ef99535efab37541c83b' + + '051a34bc8acd438763976f96876115050f73828553566d111d7ac8bffebf587c'; + + const solDurableNonceAccountInfo = { + jsonrpc: '2.0', + result: { + context: { apiVersion: '1.10.39', slot: 163846900 }, + value: { + data: { + parsed: { + info: { + authority: 'LvDUy1MovMeusYaL8ErQAqL4PeD8H9W1RALJU3twUGj', + blockhash: 'MeM29wJ8Kai1SyV5Xz8fHQhTygPs4Eka7UTgZH3LsEm', + feeCalculator: { lamportsPerSignature: '5000' }, + }, + type: 'initialized', + }, + program: 'nonce', + space: 80, + }, + executable: false, + lamports: 1447680, + owner: '11111111111111111111111111111111', + rentEpoch: 0, + }, + }, + id: 1, + }; + + // ── SUI-specific constants ──────────────────────────────────────────────── + + const suiRpcBase = 'https://fullnode.testnet.sui.io'; + + const suiBitgoKey = + '3b89eec9d2d2f3b049ecda2e7b5f47827f7927fe6618d6e8b13f64e7c95f4b0' + + '0b9577ab01395ecf8eeb804b590cedae14ff5fd3947bf3b7a95b9327c49e27c54'; + + const suiReceiveAddress1 = '0x32d8e57ee6d91e5558da0677154c2f085795348e317f95acc9efade1b4112fcc'; + + const suiCoinsAtAddr1 = [ + { + coinType: '0x2::sui::SUI', + coinObjectId: '0x996aab365d4551b6d1274f520bbfa7b0a566d548b2d590b5565c623812e7e76d', + version: '201', + digest: 'HXpNTfx9TBdxFcXHi4RziZsQuDAHavRasK6Ri15rVwuA', + balance: '200000000', + }, + { + coinType: '0x2::sui::SUI', + coinObjectId: '0xb39c5f380208cce7fe1ba1258c8d19befb02a80f14952617ed37098dbd4d2df0', + version: '199', + digest: 'mqk37hXLkiUYgkYxk2MyqNykCkCXwe97uMus7bDPhe2', + balance: '101976', + }, + ]; + + // ── TRX-specific constants ──────────────────────────────────────────────── + + const trxTokenContractAddress = 'TARsLWnWXyxDLzXpZLt8PKQjx7kqcPkbDx'; + + const tronBase = 'https://api.shasta.trongrid.io'; + + const TRX_ADDR_1 = 'TGAsEaxULesgHpKw39zrf4pWg3pbYTTKx8'; + const TRX_ADDR_2 = 'TWWPHrDJUVJMv21hkjkLP3ksVctpLDHpce'; + const TRX_ADDR_3 = 'TGp8qBtnJkhK1xRtuSgW5cgus9bADDXwUL'; + + // A minimal structurally-valid TRON transaction taken from the SDK's own test fixtures + const TRON_MOCK_TX = { + visible: false, + txID: 'ee0bbf72b238361577a9dc41d79f7a74f6ba9efe472c21bfd3e7dc850c9e9020', + raw_data: { + contract: [ + { + parameter: { + value: { + amount: 10, + owner_address: '41e5e00fc1cdb3921b8340c20b2b65b543c84aa1dd', + to_address: '412c2ba4a9ff6c53207dc5b686bfecf75ea7b80577', + }, + type_url: 'type.googleapis.com/protocol.TransferContract', + }, + type: 'TransferContract', + }, + ], + ref_block_bytes: '5123', + ref_block_hash: '52a26dea963a47bc', + expiration: 1569463320000, + timestamp: 1569463261623, + }, + raw_data_hex: + '0a025123220852a26dea963a47bc40c0fbb6dad62d5a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a1541e5e00fc1cdb3921b8340c20b2b65b543c84aa1dd1215412c2ba4a9ff6c53207dc5b686bfecf75ea7b80577180a70b7b3b3dad62d', + }; before(() => { nock.disableNetConnect(); @@ -46,119 +151,143 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); it('should succeed in handling TRON consolidation recovery for onchain wallet', async () => { - const mockTransactions = [ - { txHex: 'unsigned-tx-1', serializedTx: 'serialized-unsigned-tx-1' }, - { txHex: 'unsigned-tx-2', serializedTx: 'serialized-unsigned-tx-2' }, - ]; + const tronBalanceWithToken = { + data: [ + { + balance: 200_000_000, + trc20: [{ [trxTokenContractAddress]: '1000000' }], + }, + ], + }; - const recoverConsolidationsStub = sinon.stub(Trx.prototype, 'recoverConsolidations').resolves({ - transactions: mockTransactions, - }); + const balanceNock1 = nock(tronBase) + .get(`/v1/accounts/${TRX_ADDR_1}`) + .reply(200, tronBalanceWithToken); + const triggerNock1 = nock(tronBase) + .post('/wallet/triggersmartcontract') + .reply(200, { transaction: TRON_MOCK_TX }); + + const balanceNock2 = nock(tronBase) + .get(`/v1/accounts/${TRX_ADDR_2}`) + .reply(200, tronBalanceWithToken); + const triggerNock2 = nock(tronBase) + .post('/wallet/triggersmartcontract') + .reply(200, { transaction: TRON_MOCK_TX }); const recoveryNock = nock(advancedWalletManagerUrl) .post('/api/trx/multisig/recovery') .twice() .reply(200, { txHex: 'signed-tx' }); - const requestPayload = { - multisigType: 'onchain' as const, - userPub: mockUserPub, - backupPub: mockBackupPub, - bitgoPub: mockBitgoPub, - tokenContractAddress: 'tron-token-address', - startingScanIndex: 1, - endingScanIndex: 3, - }; - const response = await agent .post(`/api/v1/trx/advancedwallet/recoveryconsolidations`) .set('Authorization', `Bearer ${accessToken}`) - .send(requestPayload); + .send({ + multisigType: 'onchain' as const, + userPub: mockUserPub, + backupPub: mockBackupPub, + bitgoPub: mockBitgoPub, + tokenContractAddress: trxTokenContractAddress, + startingScanIndex: 1, + endingScanIndex: 3, + }); response.status.should.equal(200); response.body.should.have.property('signedTxs'); response.body.signedTxs.should.have.length(2); - sinon.assert.calledOnce(recoverConsolidationsStub); + balanceNock1.isDone().should.be.true(); + triggerNock1.isDone().should.be.true(); + balanceNock2.isDone().should.be.true(); + triggerNock2.isDone().should.be.true(); recoveryNock.done(); - - const callArgs = recoverConsolidationsStub.firstCall.args[0]; - callArgs.should.have.property('tokenContractAddress', 'tron-token-address'); - callArgs.should.have.property('userKey', mockUserPub); - callArgs.should.have.property('backupKey', mockBackupPub); - callArgs.should.have.property('bitgoKey', mockBitgoPub); }); it('should succeed in handling Solana consolidation recovery for onchain wallet', async () => { - const mockTransactions = [{ txHex: 'unsigned-tx-1', serializedTx: 'serialized-unsigned-tx-1' }]; + // Uses real SOL SDK fixture keys so Sol.recoverConsolidations runs end-to-end. + nock(solRpcBase) + .post('/', (b) => b.method === 'getBalance' && b.params[0] === solWalletAddress2) + .reply(200, { jsonrpc: '2.0', result: { context: { slot: 1 }, value: 1000000000 }, id: 1 }); + + nock(solRpcBase) + .post('/', (b) => b.method === 'getLatestBlockhash') + .reply(200, { + jsonrpc: '2.0', + result: { + context: { slot: 2792 }, + value: { + blockhash: 'EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N', + lastValidBlockHeight: 3090, + }, + }, + id: 1, + }); - const recoverConsolidationsStub = sinon.stub(Sol.prototype, 'recoverConsolidations').resolves({ - transactions: mockTransactions, - }); + nock(solRpcBase) + .post('/', (b) => b.method === 'getAccountInfo' && b.params[0] === solDurableNoncePubKey) + .reply(200, solDurableNonceAccountInfo); + + nock(solRpcBase) + .post('/', (b) => b.method === 'getFeeForMessage') + .reply(200, { jsonrpc: '2.0', result: { context: { slot: 1 }, value: 5000 }, id: 1 }); const recoveryNock = nock(advancedWalletManagerUrl) .post('/api/sol/multisig/recovery') .reply(200, { txHex: 'signed-tx' }); - const requestPayload = { - multisigType: 'onchain' as const, - userPub: mockUserPub, - backupPub: mockBackupPub, - bitgoPub: mockBitgoPub, - durableNonces: { - publicKeys: ['sol-pubkey-1', 'sol-pubkey-2'], - secretKey: 'sol-secret-key', - }, - }; - const response = await agent .post(`/api/v1/sol/advancedwallet/recoveryconsolidations`) .set('Authorization', `Bearer ${accessToken}`) - .send(requestPayload); + .send({ + multisigType: 'onchain' as const, + userPub: solBitgoKey, + backupPub: solBitgoKey, + bitgoPub: solBitgoKey, + startingScanIndex: 2, + endingScanIndex: 3, + durableNonces: { + publicKeys: [solDurableNoncePubKey, solDurableNoncePubKey2, solDurableNoncePubKey3], + secretKey: solDurableNoncePrivKey, + }, + }); response.status.should.equal(200); response.body.should.have.property('signedTxs'); response.body.signedTxs.should.have.length(1); - - sinon.assert.calledOnce(recoverConsolidationsStub); recoveryNock.done(); - - const callArgs = recoverConsolidationsStub.firstCall.args[0]; - callArgs.should.have.property('durableNonces'); - callArgs.durableNonces.should.have.property('publicKeys').which.is.an.Array(); - callArgs.durableNonces.should.have.property('secretKey', 'sol-secret-key'); - callArgs.should.have.property('userKey', mockUserPub); - callArgs.should.have.property('backupKey', mockBackupPub); - callArgs.should.have.property('bitgoKey', mockBitgoPub); }); it('should succeed in handling MPC consolidation recovery with commonKeychain', async () => { - const mockTxRequests = [ - { - walletCoin: 'tsui', - transactions: [ - { - unsignedTx: { - txHex: 'unsigned-mpc-tx-1', - serializedTx: 'serialized-unsigned-mpc-tx-1', - /** - * signableHex is required by the isRecoveryTxRequest type guard in the handler — - * without it the guard returns false and the AWM signing call is never reached. - */ - signableHex: 'signable-mpc-tx-1', + // Uses real SUI SDK fixture keys so Sui.recoverConsolidations runs end-to-end. + nock(suiRpcBase) + .post('/', (b) => b.method === 'suix_getBalance' && b.params[0] === suiReceiveAddress1) + .reply(200, { result: { totalBalance: '200101976', fundsInAddressBalance: '0' } }); + + nock(suiRpcBase) + .post('/', (b) => b.method === 'suix_getCoins' && b.params[0] === suiReceiveAddress1) + .reply(200, { + result: { data: suiCoinsAtAddr1, hasNextPage: false, nextCursor: null }, + }); + + nock(suiRpcBase) + .post('/', (b) => b.method === 'sui_dryRunTransactionBlock') + .reply(200, { + result: { + effects: { + status: { status: 'success' }, + gasUsed: { + computationCost: '1000000', + storageCost: '976000', + storageRebate: '978120', + nonRefundableStorageFee: '9880', }, - signatureShares: [], }, - ], - }, - ] as any; - - const recoverConsolidationsStub = sinon.stub(Sui.prototype, 'recoverConsolidations').resolves({ - txRequests: mockTxRequests, - }); + }, + }); let capturedAwmBody: any; const recoveryNock = nock(advancedWalletManagerUrl) @@ -168,46 +297,50 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { }) .reply(200, { txHex: 'signed-mpc-tx' }); - const requestPayload = { - multisigType: 'tss' as const, - commonKeychain: mockCommonKeychain, - apiKey: 'test-api-key', - startingScanIndex: 0, - endingScanIndex: 5, - }; - const response = await agent .post(`/api/v1/tsui/advancedwallet/recoveryconsolidations`) .set('Authorization', `Bearer ${accessToken}`) - .send(requestPayload); + .send({ + multisigType: 'tss' as const, + commonKeychain: suiBitgoKey, + startingScanIndex: 1, + endingScanIndex: 2, + }); response.status.should.equal(200); response.body.should.have.property('signedTxs'); response.body.signedTxs.should.have.length(1); - - sinon.assert.calledOnce(recoverConsolidationsStub); recoveryNock.done(); - - const callArgs = recoverConsolidationsStub.firstCall.args[0]; - callArgs.should.have.property('userKey', ''); - callArgs.should.have.property('backupKey', ''); - callArgs.should.have.property('bitgoKey', mockCommonKeychain); - - capturedAwmBody.should.have.property('commonKeychain', mockCommonKeychain); + capturedAwmBody.should.have.property('commonKeychain', suiBitgoKey); }); it('should succeed in handling SOL MPC consolidation recovery', async () => { - const mockTransactions = [ - { - txHex: 'unsigned-mpc-tx-1', - serializedTx: 'serialized-mpc-tx-1', - signableHex: 'signable-mpc-tx-1', - }, - ]; + // Uses real SOL SDK fixture keys so Sol.recoverConsolidations runs end-to-end. + nock(solRpcBase) + .post('/', (b) => b.method === 'getBalance' && b.params[0] === solWalletAddress2) + .reply(200, { jsonrpc: '2.0', result: { context: { slot: 1 }, value: 1000000000 }, id: 1 }); + + nock(solRpcBase) + .post('/', (b) => b.method === 'getLatestBlockhash') + .reply(200, { + jsonrpc: '2.0', + result: { + context: { slot: 2792 }, + value: { + blockhash: 'EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N', + lastValidBlockHeight: 3090, + }, + }, + id: 1, + }); - const recoverConsolidationsStub = sinon.stub(Sol.prototype, 'recoverConsolidations').resolves({ - transactions: mockTransactions, - }); + nock(solRpcBase) + .post('/', (b) => b.method === 'getAccountInfo' && b.params[0] === solDurableNoncePubKey) + .reply(200, solDurableNonceAccountInfo); + + nock(solRpcBase) + .post('/', (b) => b.method === 'getFeeForMessage') + .reply(200, { jsonrpc: '2.0', result: { context: { slot: 1 }, value: 5000 }, id: 1 }); let capturedAwmBody: any; const recoveryNock = nock(advancedWalletManagerUrl) @@ -217,65 +350,80 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { }) .reply(200, { txHex: 'signed-mpc-tx' }); - const requestPayload = { - multisigType: 'tss' as const, - commonKeychain: mockCommonKeychain, - apiKey: 'sol-api-key', - durableNonces: { - publicKeys: ['sol-pubkey-1'], - secretKey: 'sol-secret', - }, - }; - const response = await agent .post(`/api/v1/sol/advancedwallet/recoveryconsolidations`) .set('Authorization', `Bearer ${accessToken}`) - .send(requestPayload); + .send({ + multisigType: 'tss' as const, + commonKeychain: solBitgoKey, + startingScanIndex: 2, + endingScanIndex: 3, + durableNonces: { + publicKeys: [solDurableNoncePubKey, solDurableNoncePubKey2, solDurableNoncePubKey3], + secretKey: solDurableNoncePrivKey, + }, + }); response.status.should.equal(200); response.body.should.have.property('signedTxs'); response.body.signedTxs.should.have.length(1); - - sinon.assert.calledOnce(recoverConsolidationsStub); recoveryNock.done(); - capturedAwmBody.should.have.property('commonKeychain', mockCommonKeychain); + capturedAwmBody.should.have.property('commonKeychain', solBitgoKey); }); it('should succeed in handling multiple recovery consolidations', async () => { - const mockTransactions = [ - { txHex: 'unsigned-tx-1', serializedTx: 'serialized-unsigned-tx-1' }, - { txHex: 'unsigned-tx-2', serializedTx: 'serialized-unsigned-tx-2' }, - { txHex: 'unsigned-tx-3', serializedTx: 'serialized-unsigned-tx-3' }, - ]; - - const recoverConsolidationsStub = sinon.stub(Trx.prototype, 'recoverConsolidations').resolves({ - transactions: mockTransactions, - }); + // Scan range: startingScanIndex=0 (falsy → defaults to 1), endingScanIndex=10 + // → scans indices 1-9. Indices 1-3 have funds; 4-9 return empty data (no balance). + const tronNativeBalance = { data: [{ balance: 10_000_000 }] }; + + const balanceNock1 = nock(tronBase) + .get(`/v1/accounts/${TRX_ADDR_1}`) + .reply(200, tronNativeBalance); + const createTxNock1 = nock(tronBase).post('/wallet/createtransaction').reply(200, TRON_MOCK_TX); + + const balanceNock2 = nock(tronBase) + .get(`/v1/accounts/${TRX_ADDR_2}`) + .reply(200, tronNativeBalance); + const createTxNock2 = nock(tronBase).post('/wallet/createtransaction').reply(200, TRON_MOCK_TX); + + const balanceNock3 = nock(tronBase) + .get(`/v1/accounts/${TRX_ADDR_3}`) + .reply(200, tronNativeBalance); + const createTxNock3 = nock(tronBase).post('/wallet/createtransaction').reply(200, TRON_MOCK_TX); + + // Indices 4-9 return no balance – use a persistent regex nock consumed by remaining calls + nock(tronBase) + .persist() + .get(/\/v1\/accounts\/T.*/) + .reply(200, { data: [] }); const recoveryNock = nock(advancedWalletManagerUrl) .post('/api/trx/multisig/recovery') .thrice() .reply(200, { txHex: 'signed-tx' }); - const requestPayload = { - multisigType: 'onchain' as const, - userPub: mockUserPub, - backupPub: mockBackupPub, - bitgoPub: mockBitgoPub, - startingScanIndex: 0, - endingScanIndex: 10, - }; - const response = await agent .post(`/api/v1/trx/advancedwallet/recoveryconsolidations`) .set('Authorization', `Bearer ${accessToken}`) - .send(requestPayload); + .send({ + multisigType: 'onchain' as const, + userPub: mockUserPub, + backupPub: mockBackupPub, + bitgoPub: mockBitgoPub, + startingScanIndex: 0, + endingScanIndex: 10, + }); response.status.should.equal(200); response.body.should.have.property('signedTxs'); response.body.signedTxs.should.have.length(3); - sinon.assert.calledOnce(recoverConsolidationsStub); + balanceNock1.isDone().should.be.true(); + createTxNock1.isDone().should.be.true(); + balanceNock2.isDone().should.be.true(); + createTxNock2.isDone().should.be.true(); + balanceNock3.isDone().should.be.true(); + createTxNock3.isDone().should.be.true(); recoveryNock.done(); }); @@ -353,9 +501,12 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { }); it('should succeed in handling empty recovery consolidations result', async () => { - const recoverConsolidationsStub = sinon.stub(Trx.prototype, 'recoverConsolidations').resolves({ - transactions: [], - } as any); + // All scanned addresses return no balance → recoverConsolidations returns { transactions: [] } + // Scan range: 1-20 (defaults). Regex nock handles all 20 balance lookups. + nock(tronBase) + .persist() + .get(/\/v1\/accounts\/T.*/) + .reply(200, { data: [] }); const response = await agent .post(`/api/v1/trx/advancedwallet/recoveryconsolidations`) @@ -370,11 +521,12 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { response.status.should.equal(200); response.body.should.have.property('signedTxs'); response.body.signedTxs.should.have.length(0); - - sinon.assert.calledOnce(recoverConsolidationsStub); }); it('should fail when recoverConsolidations returns unexpected result structure', async () => { + // This test verifies the handler's defensive check: + // if (!result.transactions && !result.txRequests) throw 'recoverConsolidations did not …' + // The real SDK always returns one of those two shapes; this prototype stub exercises this error path const recoverConsolidationsStub = sinon.stub(Trx.prototype, 'recoverConsolidations').resolves({ someOtherProperty: 'value', } as any); @@ -400,9 +552,9 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { }); it('should fail when recoverConsolidations throws an error', async () => { - const recoverConsolidationsStub = sinon - .stub(Trx.prototype, 'recoverConsolidations') - .rejects(new Error('Failed to recover consolidations')); + nock(tronBase) + .get(/\/v1\/accounts\/T.*/) + .reply(500, { error: 'Node error' }); const response = await agent .post(`/api/v1/trx/advancedwallet/recoveryconsolidations`) @@ -416,17 +568,20 @@ describe('POST /api/v1/:coin/advancedwallet/recoveryconsolidations', () => { response.status.should.equal(500); response.body.should.have.property('error', 'Internal Server Error'); - response.body.should.have.property('details', 'Failed to recover consolidations'); - - sinon.assert.calledOnce(recoverConsolidationsStub); + response.body.should.have.property('details'); }); it('should fail when awmClient throws an error', async () => { - const mockTransactions = [{ txHex: 'unsigned-tx-1', serializedTx: 'serialized-unsigned-tx-1' }]; - - sinon.stub(Trx.prototype, 'recoverConsolidations').resolves({ - transactions: mockTransactions, - }); + // Index 1 has a recoverable balance; the AWM signing endpoint returns 500. + nock(tronBase) + .get(`/v1/accounts/${TRX_ADDR_1}`) + .reply(200, { data: [{ balance: 10_000_000 }] }); + nock(tronBase).post('/wallet/createtransaction').reply(200, TRON_MOCK_TX); + // Remaining indices in default scan (2-20) return no balance. + nock(tronBase) + .persist() + .get(/\/v1\/accounts\/T.*/) + .reply(200, { data: [] }); nock(advancedWalletManagerUrl).post('/api/trx/multisig/recovery').reply(500, { error: 'Internal Server Error', diff --git a/src/__tests__/api/master/recoveryWallet.test.ts b/src/__tests__/api/master/recoveryWallet.test.ts index c6a84215..32e7ab26 100644 --- a/src/__tests__/api/master/recoveryWallet.test.ts +++ b/src/__tests__/api/master/recoveryWallet.test.ts @@ -1,16 +1,10 @@ import 'should'; import * as request from 'supertest'; import nock from 'nock'; +import sinon from 'sinon'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; -import sinon from 'sinon'; -import * as middleware from '../../../shared/middleware'; -import * as masterMiddleware from '../../../masterBitgoExpress/middleware/middleware'; -import { BitGoRequest } from '../../../types/request'; -import { BitGoAPI } from '@bitgo-beta/sdk-api'; -import { AdvancedWalletManagerClient } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; -import { CoinFamily } from '@bitgo-beta/statics'; -import coinFactory from '../../../shared/coinFactory'; +import { BitGoAPITestHarness } from './testUtils'; describe('Recovery Tests', () => { let agent: request.SuperAgentTest; @@ -32,20 +26,10 @@ describe('Recovery Tests', () => { recoveryMode: true, }; - beforeEach(() => { + before(() => { nock.disableNetConnect(); nock.enableNetConnect('127.0.0.1'); - const bitgo = new BitGoAPI({ env: 'test' }); - - // Setup middleware stubs before creating app - sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { - (req as BitGoRequest).bitgo = bitgo; - (req as BitGoRequest).config = config; - next(); - }); - - // Create app after middleware is stubbed const app = expressApp(config); agent = request.agent(app); }); @@ -53,75 +37,77 @@ describe('Recovery Tests', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); describe('UTXO coin recovery', () => { - let mockRecover: sinon.SinonStub; - let mockIsValidPub: sinon.SinonStub; - let mockRecoverResponse: any; const coin = 'tbtc'; + const userPub = + 'xpub661MyMwAqRbcEtjU21VjQhGDdg5noG6kCGjcpc4EZwnLUxr9Pi56i14Eek8CQqcuGVnXQf3Zy47Uizr5WHDbZ3GumXEFXpwFLHWGbKrWWcg'; + const backupPub = + 'xpub661MyMwAqRbcEnTrcp222pRm7G1ZAbDD3KxXT2XEKRe3jnnvydqnyssewd2eUxgeWr1c1ffHcqqRKB8j3Lw9VR4dvrAhTov4kPKZF5rs6Vr'; + const bitgoPub = + 'xpub661MyMwAqRbcFNUFGFmDcC3Frgtz4FnJqFdCGbzLva2hf5i3ZJuQdsGc3z5FXCVqR9NQ6h2zTyGcQkfFtsLT5St621Fcu1C22kCKhbo4kQy'; - beforeEach(() => { - // Setup mock response for UTXO recovery - mockRecoverResponse = { - txHex: - '0100000001edd7a583fef5aabf265e6dca24452581a3cca2671a1fa6b4e404bccb6ff4c83b0000000000ffffffff01780f0000000000002200202120dcf53e62a4cc9d3843993aa2258bd14fbf911a4ea4cf4f3ac840f417027900000000', - txInfo: { - unspents: [ - { - id: '3bc8f46fcbbc04e4b4a61f1a67a2cca381254524ca6d5e26bfaaf5fe83a5d7ed:0', - address: 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu', - value: 4000, - chain: 20, - index: 0, - valueString: '4000', - }, - ], - }, - feeInfo: {}, - coin: 'tbtc', - }; - - // Create mock methods - mockRecover = sinon.stub().resolves(mockRecoverResponse); - mockIsValidPub = sinon.stub().returns(true); - const mockCoin = { - recover: mockRecover, - isValidPub: mockIsValidPub, - getFamily: sinon.stub().returns(CoinFamily.BTC), - }; - // coinStub.withArgs(coin).returns(mockCoin); - sinon - .stub(coinFactory, 'getCoin') - .withArgs(coin) - .returns(mockCoin as any); - - // Setup coin middleware - sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => { - (req as BitGoRequest).params = { coin }; - (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient( - config, - coin, - ); - next(); - return undefined; - }); - }); + const addrWithFunds = 'tb1qs5efv9zqhrc4sne7zphmsxea3cg9m262v6phsqn5dfdwed8ykx4s4wj67d'; it('should recover a UTXO wallet by calling the advanced wallet manager service', async () => { - const userPub = 'xpub_user'; - const backupPub = 'xpub_backup'; - const bitgoPub = 'xpub_bitgo'; const recoveryDestination = 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu'; + const blockchairBase = 'https://api.blockchair.com'; + + const balanceNock = nock(blockchairBase) + .get(`/bitcoin/testnet/dashboards/address/${addrWithFunds}?key=key`) + .reply(200, { + data: { [addrWithFunds]: { address: { transaction_count: 1, balance: 4000 } } }, + }); - // Mock the advanced wallet manager recovery call + const unspentsNock = nock(blockchairBase) + .get(`/bitcoin/testnet/dashboards/addresses/${addrWithFunds}?key=key`) + .reply(200, { + data: { + utxo: [ + { + transaction_hash: + '3bc8f46fcbbc04e4b4a61f1a67a2cca381254524ca6d5e26bfaaf5fe83a5d7ed', + index: 0, + recipient: addrWithFunds, + value: 4000, + block_id: 100, + spending_transaction_hash: null, + spending_index: null, + address: addrWithFunds, + }, + ], + }, + }); + + // All other address lookups return empty (persistent regex fallback). + // Handles all chains at index 0 with no balance, plus chain-20 index 1. + nock(blockchairBase) + .persist() + .get(/\/bitcoin\/testnet\/dashboards\/address\/[^?]+\?key=key/) + .reply(function (uri) { + const match = uri.match(/\/dashboards\/address\/([^?]+)\?/); + const addr = match ? decodeURIComponent(match[1]) : 'unknown'; + return [200, { data: { [addr]: { address: { transaction_count: 0, balance: 0 } } } }]; + }); + + // mempool.space fee rate (called when feeRate param is undefined) + const feeNock = nock('https://mempool.space') + .get('/api/v1/fees/recommended') + .reply(200, { fastestFee: 20, halfHourFee: 10, hourFee: 5 }); + + // The real SDK builds a dynamic PSBT; body matcher const recoveryNock = nock(advancedWalletManagerUrl) - .post(`/api/${coin}/multisig/recovery`, { - userPub, - backupPub, - bitgoPub, - unsignedSweepPrebuildTx: mockRecoverResponse, - walletContractAddress: '', + .post(`/api/${coin}/multisig/recovery`, (body) => { + return ( + body.userPub === userPub && + body.backupPub === backupPub && + body.bitgoPub === bitgoPub && + body.walletContractAddress === '' && + body.unsignedSweepPrebuildTx !== undefined && + body.unsignedSweepPrebuildTx.txHex !== undefined + ); }) .reply(200, { txHex: @@ -154,31 +140,13 @@ describe('Recovery Tests', () => { '01000000000101edd7a583fef5aabf265e6dca24452581a3cca2671a1fa6b4e404bccb6ff4c83b0000000000ffffffff01780f0000000000002200202120dcf53e62a4cc9d3843993aa2258bd14fbf911a4ea4cf4f3ac840f41702790400473044022043a9256810ef47ce36a092305c0b1ef675bce53e46418eea8cacbf1643e541d90220450766e048b841dac658d0a2ba992628bfe131dff078c3a574cadf67b4946647014730440220360045a15e459ed44aa3e52b86dd6a16dddaf319821f4dcc15627686f377edd102205cb3d5feab1a773c518d43422801e01dd1bc586bb09f6a9ed23a1fc0cfeeb5310169522103a1c425fd9b169e6ab5ed3de596acb777ccae0cda3d91256238b5e739a3f14aae210222a76697605c890dc4365132f9ae0d351952a1aad7eecf78d9923766dbe74a1e21033b21c0758ffbd446204914fa1d1c5921e9f82c2671dac89737666aa9375973e953ae00000000', ); - // Verify SDK coin method calls - // coinStub.calledWith(coin).should.be.true(); - mockIsValidPub.calledWith(userPub).should.be.true(); - mockIsValidPub.calledWith(backupPub).should.be.true(); - mockRecover - .calledWith({ - userKey: userPub, - backupKey: backupPub, - bitgoKey: bitgoPub, - recoveryDestination: recoveryDestination, - apiKey: 'key', - ignoreAddressTypes: [], - scan: 1, - feeRate: undefined, - }) - .should.be.true(); - - // Verify advanced wallet manager call + balanceNock.isDone().should.be.true(); + unspentsNock.isDone().should.be.true(); + feeNock.isDone().should.be.true(); recoveryNock.done(); }); it('should reject incorrect EVM parameters for a UTXO coin', async () => { - const userPub = 'xpub_user'; - const backupPub = 'xpub_backup'; - const bitgoPub = 'xpub_bitgo'; const recoveryDestination = 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu'; const response = await agent @@ -211,9 +179,6 @@ describe('Recovery Tests', () => { }); it('should reject incorrect Solana parameters for a UTXO coin', async () => { - const userPub = 'xpub_user'; - const backupPub = 'xpub_backup'; - const bitgoPub = 'xpub_bitgo'; const recoveryDestination = 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu'; const response = await agent @@ -248,9 +213,6 @@ describe('Recovery Tests', () => { }); it('should reject using legacy coinSpecificParams format', async () => { - const userPub = 'xpub_user'; - const backupPub = 'xpub_backup'; - const bitgoPub = 'xpub_bitgo'; const recoveryDestination = 'tb1qprdy6jwxrrr2qrwgd2tzl8z99hqp29jn6f3sguxulqm448myj6jsy2nwsu'; const response = await agent @@ -281,52 +243,74 @@ describe('Recovery Tests', () => { }); describe('EVM coin recovery', () => { - // Setup mocks for ETH const ethCoinId = 'hteth'; - beforeEach(() => { - const mockRecover = sinon.stub().resolves({ txHex: 'eth-signed-tx-hex' }); - const mockIsValidPub = sinon.stub().returns(true); - sinon - .stub(coinFactory, 'getCoin') - .withArgs(ethCoinId) - .returns({ - /** Mock the recover() method on EVM coins */ - recover: mockRecover, - isValidPub: mockIsValidPub, - getFamily: sinon.stub().returns(CoinFamily.ETH), - } as any); - - sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => { - (req as BitGoRequest).params = { coin: ethCoinId }; - (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient( - config, - ethCoinId, - ); - next(); - return undefined; - }); - }); + const ethUserPub = + 'xpub661MyMwAqRbcFigezGWEYSbCPVuaUmvnp1u7iEpH9YsKU6uYQtPANvudjgAo82QRHXsUieMqKeB1xEj89VUKU1ugtmyAZ3xzNEbHPexxgKK'; + const ethBackupPub = + 'xpub661MyMwAqRbcGbCirzmQsUJT2eidt9tFLw2m77w6FiKco6TKu49CP3GkHF88xGCpvqkP93SYMAarfyWAn8UWevQtNT6pDo8xH7xmf6GqK6e'; it('should recover an EVM wallet by calling the advanced wallet manager service', async () => { - const userPub = 'xpub_user'; - const backupPub = 'xpub_backup'; - const bitgoPub = 'xpub_bitgo'; const recoveryDestination = '0x1234567890123456789012345678901234567890'; const walletContractAddress = '0x0987654321098765432109876543210987654321'; + const backupKeyAddress = '0x30edc88a77598833f58947638b2ac3d5713d9845'; + const etherscanBase = 'https://api.etherscan.io'; + const chainid = '560048'; // Holesky testnet (hteth) + const apiKey = 'key'; + + // Etherscan txlist for backup key nonce (called twice: recoverEthLike + formatForOfflineVault) + const txlistNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=account&action=txlist&address=${backupKeyAddress}&apikey=${apiKey}`, + ) + .twice() + .reply(200, { result: [] }); + + const backupBalanceNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=account&action=balance&address=${backupKeyAddress}&apikey=${apiKey}`, + ) + .reply(200, { result: '10000000000000000' }); + + const walletBalanceNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=account&action=balance&address=${walletContractAddress}&apikey=${apiKey}`, + ) + .reply(200, { result: '1000000000000000000' }); + + const sequenceIdNock = nock(etherscanBase) + .get( + `/v2/api?chainid=${chainid}&module=proxy&action=eth_call&to=${walletContractAddress}&data=a0b7967b&tag=latest&apikey=${apiKey}`, + ) + .reply(200, { + result: '0x0000000000000000000000000000000000000000000000000000000000000001', + }); + // The real SDK builds a dynamic unsignedSweepPrebuildTx; body matcher const recoveryNock = nock(advancedWalletManagerUrl) - .post(`/api/${ethCoinId}/multisig/recovery`) + .post(`/api/${ethCoinId}/multisig/recovery`, (body) => { + return ( + body.userPub === ethUserPub && + body.backupPub === ethBackupPub && + body.walletContractAddress === walletContractAddress && + body.unsignedSweepPrebuildTx !== undefined + ); + }) .reply(200, { txHex: 'eth-signed-tx-hex' }); const response = await agent .post(`/api/v1/${ethCoinId}/advancedwallet/recovery`) .set('Authorization', `Bearer ${accessToken}`) .send({ - multiSigRecoveryParams: { userPub, backupPub, bitgoPub, walletContractAddress }, + multiSigRecoveryParams: { + userPub: ethUserPub, + backupPub: ethBackupPub, + bitgoPub: '', + walletContractAddress, + }, recoveryDestinationAddress: recoveryDestination, coin: ethCoinId, - apiKey: 'key', + apiKey, coinSpecificParams: { evmRecoveryOptions: {}, }, @@ -334,13 +318,14 @@ describe('Recovery Tests', () => { response.status.should.equal(200); response.body.should.have.property('txHex', 'eth-signed-tx-hex'); + txlistNock.isDone().should.be.true(); + backupBalanceNock.isDone().should.be.true(); + walletBalanceNock.isDone().should.be.true(); + sequenceIdNock.isDone().should.be.true(); recoveryNock.done(); }); it('should reject incorrect UTXO parameters for an ETH coin', async () => { - const userPub = 'xpub_user'; - const backupPub = 'xpub_backup'; - const bitgoPub = 'xpub_bitgo'; const recoveryDestination = '0x1234567890123456789012345678901234567890'; const walletContractAddress = '0x0987654321098765432109876543210987654321'; @@ -349,9 +334,9 @@ describe('Recovery Tests', () => { .set('Authorization', `Bearer ${accessToken}`) .send({ multiSigRecoveryParams: { - userPub, - backupPub, - bitgoPub, + userPub: ethUserPub, + backupPub: ethBackupPub, + bitgoPub: '', walletContractAddress, }, recoveryDestinationAddress: recoveryDestination, @@ -374,9 +359,6 @@ describe('Recovery Tests', () => { }); it('should reject incorrect Solana parameters for an ETH coin', async () => { - const userPub = 'xpub_user'; - const backupPub = 'xpub_backup'; - const bitgoPub = 'xpub_bitgo'; const recoveryDestination = '0x1234567890123456789012345678901234567890'; const walletContractAddress = '0x0987654321098765432109876543210987654321'; @@ -385,9 +367,9 @@ describe('Recovery Tests', () => { .set('Authorization', `Bearer ${accessToken}`) .send({ multiSigRecoveryParams: { - userPub, - backupPub, - bitgoPub, + userPub: ethUserPub, + backupPub: ethBackupPub, + bitgoPub: '', walletContractAddress, }, recoveryDestinationAddress: recoveryDestination, @@ -417,23 +399,6 @@ describe('Recovery Tests', () => { const solCoinId = 'tsol'; const solExplorerUrl = 'https://api.devnet.solana.com'; - beforeEach(() => { - // Setup coin middleware for Solana coin - sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => { - (req as BitGoRequest).params = { coin: solCoinId }; - (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient( - config, - solCoinId, - ); - next(); - return undefined; - }); - }); - - afterEach(() => { - nock.cleanAll(); - }); - it('should sign a solana recovery successfully', async () => { const solAccountBalanceNock = nock(solExplorerUrl) .post('/') @@ -563,23 +528,6 @@ describe('Recovery Tests', () => { const suiCoinId = 'tsui'; const suiExplorerUrl = 'https://fullnode.testnet.sui.io'; - beforeEach(() => { - // Setup coin middleware for Sui coin - sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => { - (req as BitGoRequest).params = { coin: suiCoinId }; - (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient( - config, - suiCoinId, - ); - next(); - return undefined; - }); - }); - - afterEach(() => { - nock.cleanAll(); - }); - it('should sign a sui recovery successfully', async () => { const suiAccountBalanceNock = nock(suiExplorerUrl) .post('/') diff --git a/src/__tests__/api/master/recoveryWalletMpcV2.test.ts b/src/__tests__/api/master/recoveryWalletMpcV2.test.ts index 057b9cde..e249762b 100644 --- a/src/__tests__/api/master/recoveryWalletMpcV2.test.ts +++ b/src/__tests__/api/master/recoveryWalletMpcV2.test.ts @@ -1,12 +1,10 @@ import 'should'; import * as request from 'supertest'; import nock from 'nock'; +import sinon from 'sinon'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; -import sinon from 'sinon'; -import * as middleware from '../../../shared/middleware'; -import { BitGoRequest } from '../../../types/request'; -import { BitGoAPI } from '@bitgo-beta/sdk-api'; +import { BitGoAPITestHarness } from './testUtils'; describe('MBE mpcv2 recovery', () => { let agent: request.SuperAgentTest; @@ -15,15 +13,10 @@ describe('MBE mpcv2 recovery', () => { const cosmosLikeCoin = 'tsei'; const accessToken = 'test-token'; - let bitgo: BitGoAPI; - before(() => { nock.disableNetConnect(); nock.enableNetConnect('127.0.0.1'); - // Create a BitGo instance that we'll use for stubbing - bitgo = new BitGoAPI({ env: 'test' }); - const config: MasterExpressConfig = { appMode: AppMode.MASTER_EXPRESS, port: 0, // Let OS assign a free port @@ -40,19 +33,14 @@ describe('MBE mpcv2 recovery', () => { recoveryMode: true, }; - // Setup middleware stubs before creating app - sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { - (req as BitGoRequest).bitgo = bitgo; - (req as BitGoRequest).config = config; - next(); - }); - const app = expressApp(config); agent = request.agent(app); }); afterEach(() => { nock.cleanAll(); + sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); it('should recover a HETH (an eth-like) wallet by calling the advanced wallet manager service', async () => {