From 53279bcb92a6ac2bd17380c87b3f653216884010 Mon Sep 17 00:00:00 2001 From: Daniel Peng Date: Fri, 29 May 2026 09:21:42 -0400 Subject: [PATCH 1/2] refactor: use sdk functions instead of fake mocks for tests Ticket: WAL-1489 --- src/__tests__/api/master/ecdsa.test.ts | 25 +- src/__tests__/api/master/eddsa.test.ts | 18 +- .../api/master/generateWallet.test.ts | 62 +- src/__tests__/api/master/sendMany.test.ts | 692 ++++++++++++------ 4 files changed, 541 insertions(+), 256 deletions(-) diff --git a/src/__tests__/api/master/ecdsa.test.ts b/src/__tests__/api/master/ecdsa.test.ts index a112d73d..7b48bc20 100644 --- a/src/__tests__/api/master/ecdsa.test.ts +++ b/src/__tests__/api/master/ecdsa.test.ts @@ -2,7 +2,6 @@ import 'should'; import nock from 'nock'; import * as sinon from 'sinon'; import { - EcdsaMPCv2Utils, Environments, IRequestTracer, openpgpUtils, @@ -16,7 +15,13 @@ import { import { AdvancedWalletManagerClient } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; import { signAndSendEcdsaMPCv2FromTxRequest } from '../../../masterBitgoExpress/handlers/ecdsa'; import { BitGoAPI } from '@bitgo-beta/sdk-api'; -import { readKey } from 'openpgp'; + +class BitGoAPITestHarness extends BitGoAPI { + static clearConstantsCache(): void { + BitGoAPI._constants = {}; + BitGoAPI._constantsExpire = {}; + } +} describe('Ecdsa Signing Handler', () => { let bitgo: BitGoAPI; @@ -57,6 +62,7 @@ describe('Ecdsa Signing Handler', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); after(() => { @@ -84,8 +90,19 @@ describe('Ecdsa Signing Handler', () => { const userPubKey = 'test-user-pub-key'; const bitgoGpgKey = await openpgpUtils.generateGPGKeyPair('secp256k1'); - const pgpKey = await readKey({ armoredKey: bitgoGpgKey.publicKey }); - sinon.stub(EcdsaMPCv2Utils.prototype, 'getBitgoMpcv2PublicGpgKey').resolves(pgpKey); + const bitgoEd25519Key = await openpgpUtils.generateGPGKeyPair('ed25519'); + + nock(bitgoApiUrl) + .persist() + .get('/api/v1/client/constants') + .reply(200, { + constants: { + mpc: { + bitgoMPCv2PublicKey: bitgoGpgKey.publicKey, + bitgoPublicKey: bitgoEd25519Key.publicKey, + }, + }, + }); // Mock sendSignatureShareV2 calls for each round const round1SignatureShare: SignatureShareRecord = { diff --git a/src/__tests__/api/master/eddsa.test.ts b/src/__tests__/api/master/eddsa.test.ts index 565ce8a3..3059927f 100644 --- a/src/__tests__/api/master/eddsa.test.ts +++ b/src/__tests__/api/master/eddsa.test.ts @@ -3,7 +3,6 @@ import nock from 'nock'; import * as sinon from 'sinon'; import { BitGoBase, - EddsaUtils, Environments, IRequestTracer, openpgpUtils, @@ -14,7 +13,13 @@ import { import { BitGoAPI } from '@bitgo-beta/sdk-api'; import { AdvancedWalletManagerClient as AdvancedWalletManagerClient } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; import { handleEddsaSigning } from '../../../masterBitgoExpress/handlers/eddsa'; -import { readKey } from 'openpgp'; + +class BitGoAPITestHarness extends BitGoAPI { + static clearConstantsCache(): void { + BitGoAPI._constants = {}; + BitGoAPI._constantsExpire = {}; + } +} describe('Eddsa Signing Handler', () => { let bitgo: BitGoBase; @@ -52,6 +57,7 @@ describe('Eddsa Signing Handler', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); after(() => { @@ -100,10 +106,10 @@ describe('Eddsa Signing Handler', () => { const userPubKey = 'test-user-pub-key'; const bitgoGpgKey = await openpgpUtils.generateGPGKeyPair('ed25519'); - const getGPGKeysStub = sinon.stub().resolves([{ pub: bitgoGpgKey.publicKey }]); - const pgpKey = await readKey({ armoredKey: bitgoGpgKey.publicKey }); - sinon.stub(EddsaUtils.prototype, 'getBitgoPublicGpgKey').resolves(pgpKey); + nock(bitgoApiUrl) + .get('/api/v1/client/constants') + .reply(200, { constants: { mpc: { bitgoPublicKey: bitgoGpgKey.publicKey } } }); // Mock exchangeEddsaCommitments call const exchangeCommitmentsNock = nock(bitgoApiUrl) @@ -226,8 +232,6 @@ describe('Eddsa Signing Handler', () => { }, }); - (bitgo as any).getGPGKeys = getGPGKeysStub; - const result = await handleEddsaSigning(bitgo, wallet, txRequest, awmClient, userPubKey, reqId); result.should.eql({ diff --git a/src/__tests__/api/master/generateWallet.test.ts b/src/__tests__/api/master/generateWallet.test.ts index 5e5aba07..7a8513e6 100644 --- a/src/__tests__/api/master/generateWallet.test.ts +++ b/src/__tests__/api/master/generateWallet.test.ts @@ -11,14 +11,12 @@ import { BitGoAPI } from '@bitgo-beta/sdk-api'; import * as middleware from '../../../shared/middleware'; import { BitGoRequest } from '../../../types/request'; -/** - * This test suite demonstrates how to mock the BitGo SDK's fetchConstants method - * instead of using nock to intercept HTTP requests to the constants endpoint. - * - * By using sinon to stub the fetchConstants method directly, we make the tests more - * focused on behavior rather than implementation details, and less brittle to changes - * in how the constants are fetched. - */ +class BitGoAPITestHarness extends BitGoAPI { + static clearConstantsCache(): void { + BitGoAPI._constants = {}; + BitGoAPI._constantsExpire = {}; + } +} function mockWalletResponse(id: string, coinName: string, overrides: Record = {}) { return { @@ -110,6 +108,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); it('should generate an onchain wallet with separate backup AWM (separate-HSM mode)', async () => { @@ -352,6 +351,12 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const backupAwmUrl = 'http://backup-awm.invalid'; sinon.restore(); + // Register before new BitGoAPI() so the constructor's background fetchConstants() hits the nock; + // use persist() since the background fetch and the handler may both call the endpoint + nock(bitgoApiUrl) + .persist() + .get('/api/v1/client/constants') + .reply(200, { constants: { mpc: { bitgoPublicKey: 'test-bitgo-public-key' } } }); const backupBitgo = new BitGoAPI({ env: 'test' }); const configWithBackup: MasterExpressConfig = { appMode: AppMode.MASTER_EXPRESS, @@ -378,12 +383,6 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const app = expressApp(configWithBackup); const backupAgent = request.agent(app); - sinon.stub(backupBitgo, 'fetchConstants').resolves({ - mpc: { - bitgoPublicKey: 'test-bitgo-public-key', - }, - }); - // User init goes to primary AWM const userInitNock = nock(advancedWalletManagerUrl) .post(`/api/${eddsaCoin}/mpc/key/initialize`, { @@ -667,12 +666,10 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { }); it('should generate a TSS MPC v1 wallet by calling the advanced wallet manager service', async () => { - // Mock fetchConstants instead of using nock for URL mocking - sinon.stub(bitgo, 'fetchConstants').resolves({ - mpc: { - bitgoPublicKey: 'test-bitgo-public-key', - }, - }); + nock(bitgoApiUrl) + .persist() + .get('/api/v1/client/constants') + .reply(200, { constants: { mpc: { bitgoPublicKey: 'test-bitgo-public-key' } } }); const userInitNock = nock(advancedWalletManagerUrl) .post(`/api/${eddsaCoin}/mpc/key/initialize`, { @@ -991,7 +988,6 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { multisigType: 'tss', }); - // No need to check constantsNock since we're using sinon stub userInitNock.done(); backupInitNock.done(); bitgoAddKeychainNock.done(); @@ -1007,6 +1003,12 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const backupAwmUrl = 'http://backup-awm.invalid'; sinon.restore(); + // Register before new BitGoAPI() so the constructor's background fetchConstants() hits the nock; + // use persist() since the background fetch and the handler may both call the endpoint + nock(bitgoApiUrl) + .persist() + .get('/api/v1/client/constants') + .reply(200, { constants: { mpc: { bitgoMPCv2PublicKey: 'test-bitgo-public-key' } } }); const backupBitgo = new BitGoAPI({ env: 'test' }); const configWithBackup: MasterExpressConfig = { appMode: AppMode.MASTER_EXPRESS, @@ -1033,12 +1035,6 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { const app = expressApp(configWithBackup); const backupAgent = request.agent(app); - sinon.stub(backupBitgo, 'fetchConstants').resolves({ - mpc: { - bitgoMPCv2PublicKey: 'test-bitgo-public-key', - }, - }); - // Init: user goes to primary AWM, backup goes to backup AWM const userInitNock = nock(advancedWalletManagerUrl) .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { source: 'user' }) @@ -1641,12 +1637,11 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { }); it('should generate a TSS MPC v2 wallet by calling the advanced wallet manager service', async () => { - // Mock fetchConstants instead of using nock for URL mocking - sinon.stub(bitgo, 'fetchConstants').resolves({ - mpc: { - bitgoMPCv2PublicKey: 'test-bitgo-public-key', - }, - }); + nock(bitgoApiUrl) + .persist() + .get('/api/v1/client/constants') + .reply(200, { constants: { mpc: { bitgoMPCv2PublicKey: 'test-bitgo-public-key' } } }); + // init round const userInitNock = nock(advancedWalletManagerUrl) .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { @@ -2287,7 +2282,6 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { type: 'advanced', }); - // No need to check constantsNock since we're using sinon stub userInitNock.done(); backupInitNock.done(); userRound1Nock.done(); diff --git a/src/__tests__/api/master/sendMany.test.ts b/src/__tests__/api/master/sendMany.test.ts index 1c726b05..111a516b 100644 --- a/src/__tests__/api/master/sendMany.test.ts +++ b/src/__tests__/api/master/sendMany.test.ts @@ -5,16 +5,343 @@ import * as request from 'supertest'; import nock from 'nock'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; -import { Environments, Wallet } from '@bitgo-beta/sdk-core'; +import { + Environments, + openpgpUtils, + SignatureShareRecord, + SignatureShareType, + Wallet, +} from '@bitgo-beta/sdk-core'; +import { BitGoAPI } from '@bitgo-beta/sdk-api'; import { Tbtc } from '@bitgo-beta/sdk-coin-btc'; +import { Tsol } from '@bitgo-beta/sdk-coin-sol'; import assert from 'assert'; +class BitGoAPITestHarness extends BitGoAPI { + static clearConstantsCache(): void { + BitGoAPI._constants = {}; + BitGoAPI._constantsExpire = {}; + } +} + +const testWalletId = 'test-wallet-id'; +const testBitgoApiUrl = Environments.test.uri; +const tssTxRequestId = 'test-tx-request-id'; + +function mockMultisigPrebuildResponse(walletIdParam: string) { + return { + txHex: 'prebuilt-tx-hex', + txInfo: { + nP2SHInputs: 1, + nSegwitInputs: 0, + nOutputs: 2, + }, + walletId: walletIdParam, + }; +} + +function buildPendingEdDsaTxRequest(walletIdParam: string) { + return { + txRequestId: tssTxRequestId, + apiVersion: 'full', + enterpriseId: 'test-enterprise-id', + transactions: [ + { + state: 'pendingSignature', + unsignedTx: { + derivationPath: 'm/0', + signableHex: 'testMessage', + serializedTxHex: 'testSerializedTxHex', + }, + signatureShares: [ + { share: 'bitgo-to-user-r-share', from: 'bitgo', to: 'user' }, + { share: 'user-to-bitgo-r-share', from: 'user', to: 'bitgo' }, + ], + }, + ], + state: 'pendingUserSignature', + walletId: walletIdParam, + walletType: 'hot', + version: 2, + date: new Date().toISOString(), + userId: 'test-user-id', + intent: {}, + policiesChecked: true, + unsignedTxs: [], + latest: true, + }; +} + +function buildSignedEdDsaTxRequest(walletIdParam: string) { + const pending = buildPendingEdDsaTxRequest(walletIdParam); + return { + ...pending, + state: 'signed', + transactions: [ + { + ...pending.transactions[0], + state: 'signed', + signedTx: { id: 'test-tx-id', tx: 'signed-transaction' }, + }, + ], + }; +} + +function nockTssWalletKeychains(coinName: string) { + nock(testBitgoApiUrl) + .get(`/api/v2/${coinName}/key/user-key-id`) + .matchHeader('any', () => true) + .times(10) + .reply(200, { + id: 'user-key-id', + pub: 'xpub_user', + commonKeychain: 'test-common-keychain', + source: 'user', + type: 'tss', + }); + nock(testBitgoApiUrl) + .get(`/api/v2/${coinName}/key/backup-key-id`) + .matchHeader('any', () => true) + .times(10) + .reply(200, { + id: 'backup-key-id', + pub: 'xpub_backup', + commonKeychain: 'test-common-keychain', + source: 'backup', + type: 'tss', + }); + nock(testBitgoApiUrl) + .get(`/api/v2/${coinName}/key/bitgo-key-id`) + .matchHeader('any', () => true) + .times(10) + .reply(200, { + id: 'bitgo-key-id', + pub: 'xpub_bitgo', + commonKeychain: 'test-common-keychain', + source: 'bitgo', + type: 'tss', + hsmType: 'institutional', + }); +} + +function buildPendingEcdsaMPCv2TxRequest(walletIdParam: string) { + return { + txRequestId: tssTxRequestId, + apiVersion: 'full', + enterpriseId: 'test-enterprise-id', + transactions: [ + { + state: 'pendingSignature', + unsignedTx: { + derivationPath: 'm/0', + signableHex: 'testMessage', + serializedTxHex: 'testSerializedTxHex', + }, + signatureShares: [] as SignatureShareRecord[], + }, + ], + state: 'pendingUserSignature', + walletId: walletIdParam, + walletType: 'hot', + version: 2, + date: new Date().toISOString(), + userId: 'test-user-id', + intent: {}, + policiesChecked: true, + unsignedTxs: [], + latest: true, + }; +} + +function buildSignedEcdsaMPCv2TxRequest(walletIdParam: string) { + const pending = buildPendingEcdsaMPCv2TxRequest(walletIdParam); + return { + ...pending, + state: 'signed', + transactions: [ + { + ...pending.transactions[0], + state: 'signed', + signedTx: { id: 'test-tx-id', tx: 'signed-transaction' }, + }, + ], + }; +} + +function nockEcdsaMPCv2SigningFlow( + coin: string, + walletIdParam: string, + bitgoApiUrlParam: string, + advancedWalletManagerUrlParam: string, +) { + const round1SignatureShare: SignatureShareRecord = { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: JSON.stringify({ + type: 'round1Input', + data: { msg1: { from: 1, message: 'round1-message' } }, + }), + }; + const round2SignatureShare: SignatureShareRecord = { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: JSON.stringify({ + type: 'round2Input', + data: { + msg2: { from: 1, to: 3, encryptedMessage: 'round2-message', signature: 'round2-signature' }, + msg3: { from: 1, to: 3, encryptedMessage: 'round3-message', signature: 'round3-signature' }, + }, + }), + }; + const round3SignatureShare: SignatureShareRecord = { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: JSON.stringify({ + type: 'round3Input', + data: { + msg4: { + from: 1, + message: 'round4-message', + signature: 'round4-signature', + signatureR: 'round4-signature-r', + }, + }, + }), + }; + + const pendingTxRequest = buildPendingEcdsaMPCv2TxRequest(walletIdParam); + const signedTxRequest = buildSignedEcdsaMPCv2TxRequest(walletIdParam); + + // The SDK fetches the user keychain in handleSendMany (validation) and again inside + // prebuildAndSignTransaction → getKeysForSigning, so use persist(). + nock(bitgoApiUrlParam) + .persist() + .get(`/api/v2/${coin}/key/user-key-id`) + .matchHeader('any', () => true) + .reply(200, { + id: 'user-key-id', + pub: 'xpub_user', + commonKeychain: 'test-common-keychain', + source: 'user', + type: 'tss', + }); + + // pickBitgoPubGpgKeyForSigning fetches the BitGo keychain to resolve the GPG key via + // hsmType → getBitgoMpcGpgPubKey. env:'test' requires this path (no constants fallback). + nock(bitgoApiUrlParam) + .get(`/api/v2/${coin}/key/bitgo-key-id`) + .matchHeader('any', () => true) + .reply(200, { + id: 'bitgo-key-id', + pub: 'xpub_bitgo', + commonKeychain: 'test-common-keychain', + source: 'bitgo', + type: 'tss', + hsmType: 'institutional', + }); + + const createTxRequestNock = nock(bitgoApiUrlParam) + .post(`/api/v2/wallet/${walletIdParam}/txrequests`) + .matchHeader('any', () => true) + .reply(200, pendingTxRequest); + + // getTxRequest is called three times: in prebuildAndSignTransaction, in + // signEcdsaMPCv2TssUsingExternalSigner, and in sendManyTxRequests. + nock(bitgoApiUrlParam) + .persist() + .get(`/api/v2/wallet/${walletIdParam}/txrequests`) + .query(true) + .matchHeader('any', () => true) + .reply(200, { txRequests: [signedTxRequest] }); + + const round1SignNock = nock(bitgoApiUrlParam) + .post(`/api/v2/wallet/${walletIdParam}/txrequests/${tssTxRequestId}/transactions/0/sign`) + .matchHeader('any', () => true) + .reply(200, { + ...pendingTxRequest, + transactions: [ + { ...pendingTxRequest.transactions[0], signatureShares: [round1SignatureShare] }, + ], + }); + + const round2SignNock = nock(bitgoApiUrlParam) + .post(`/api/v2/wallet/${walletIdParam}/txrequests/${tssTxRequestId}/transactions/0/sign`) + .matchHeader('any', () => true) + .reply(200, { + ...pendingTxRequest, + transactions: [ + { + ...pendingTxRequest.transactions[0], + signatureShares: [round1SignatureShare, round2SignatureShare], + }, + ], + }); + + const round3SignNock = nock(bitgoApiUrlParam) + .post(`/api/v2/wallet/${walletIdParam}/txrequests/${tssTxRequestId}/transactions/0/sign`) + .matchHeader('any', () => true) + .reply(200, { + ...pendingTxRequest, + transactions: [ + { + ...pendingTxRequest.transactions[0], + signatureShares: [round1SignatureShare, round2SignatureShare, round3SignatureShare], + }, + ], + }); + + const sendTxNock = nock(bitgoApiUrlParam) + .post(`/api/v2/wallet/${walletIdParam}/txrequests/${tssTxRequestId}/transactions/0/send`) + .matchHeader('any', () => true) + .reply(200, pendingTxRequest); + + const transferNock = nock(bitgoApiUrlParam) + .post(`/api/v2/wallet/${walletIdParam}/txrequests/${tssTxRequestId}/transfers`) + .matchHeader('any', () => true) + .reply(200, { state: 'signed' }); + + const awmRound1Nock = nock(advancedWalletManagerUrlParam) + .post(`/api/${coin}/mpc/sign/mpcv2round1`) + .reply(200, { + signatureShareRound1: round1SignatureShare, + userGpgPubKey: 'user-gpg-pub-key', + encryptedRound1Session: 'encrypted-round1-session', + encryptedUserGpgPrvKey: 'encrypted-user-gpg-prv-key', + encryptedDataKey: 'test-encrypted-data-key', + }); + + const awmRound2Nock = nock(advancedWalletManagerUrlParam) + .post(`/api/${coin}/mpc/sign/mpcv2round2`) + .reply(200, { + signatureShareRound2: round2SignatureShare, + encryptedRound2Session: 'encrypted-round2-session', + }); + + const awmRound3Nock = nock(advancedWalletManagerUrlParam) + .post(`/api/${coin}/mpc/sign/mpcv2round3`) + .reply(200, { + signatureShareRound3: round3SignatureShare, + }); + + return { + createTxRequestNock, + round1SignNock, + round2SignNock, + round3SignNock, + sendTxNock, + transferNock, + awmRound1Nock, + awmRound2Nock, + awmRound3Nock, + }; +} + describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { let agent: request.SuperAgentTest; const advancedWalletManagerUrl = 'http://advancedwalletmanager.invalid'; const bitgoApiUrl = Environments.test.uri; const accessToken = 'test-token'; - const walletId = 'test-wallet-id'; + const walletId = testWalletId; const coin = 'tbtc'; before(() => { @@ -43,6 +370,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { afterEach(() => { nock.cleanAll(); sinon.restore(); + BitGoAPITestHarness.clearConstantsCache(); }); describe('SendMany Multisig:', () => { @@ -76,15 +404,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { - nP2SHInputs: 1, - nSegwitInputs: 0, - nOutputs: 2, - }, - walletId, - }); + const prebuildStub = sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -171,11 +493,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, - walletId, - }); + sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -239,11 +559,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, - walletId, - }); + sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -307,15 +625,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { - nP2SHInputs: 1, - nSegwitInputs: 0, - nOutputs: 2, - }, - walletId, - }); + const prebuildStub = sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -375,7 +687,15 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { describe('SendMany TSS EDDSA:', () => { const coin = 'tsol'; it('should send many transactions using EDDSA TSS signing', async () => { - // Mock wallet get request for TSS wallet + const bitgoGpgKey = await openpgpUtils.generateGPGKeyPair('ed25519'); + const pendingTxRequest = buildPendingEdDsaTxRequest(walletId); + const signedTxRequest = buildSignedEdDsaTxRequest(walletId); + + nock(bitgoApiUrl) + .persist() + .get('/api/v1/client/constants') + .reply(200, { constants: { mpc: { bitgoPublicKey: bitgoGpgKey.publicKey } } }); + const walletGetNock = nock(bitgoApiUrl) .get(`/api/v2/${coin}/wallet/${walletId}`) .matchHeader('any', () => true) @@ -386,45 +706,81 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { multisigType: 'tss', }); - // Mock keychain get request for TSS keychain - const keychainGetNock = nock(bitgoApiUrl) - .get(`/api/v2/${coin}/key/user-key-id`) + nockTssWalletKeychains(coin); + sinon.stub(Tsol.prototype, 'verifyTransaction').resolves(true); + + let capturedTxRequestBody: Record | undefined; + const createTxRequestNock = nock(bitgoApiUrl) + .post(`/api/v2/wallet/${walletId}/txrequests`, (body) => { + capturedTxRequestBody = body; + return true; + }) .matchHeader('any', () => true) + .reply(200, pendingTxRequest); + + const deleteSigSharesNock = nock(bitgoApiUrl) + .delete(`/api/v2/wallet/${walletId}/txrequests/${tssTxRequestId}/signatureshares`) + .matchHeader('any', () => true) + .reply(200, []); + + const exchangeCommitmentsNock = nock(bitgoApiUrl) + .post(`/api/v2/wallet/${walletId}/txrequests/${tssTxRequestId}/transactions/0/commit`) + .matchHeader('any', () => true) + .reply(200, { commitmentShare: { share: 'bitgo-commitment-share' } }); + + const offerRShareNock = nock(bitgoApiUrl) + .post( + `/api/v2/wallet/${walletId}/txrequests/${tssTxRequestId}/transactions/0/signatureshares`, + ) + .matchHeader('any', () => true) + .reply(200, { share: 'user-to-bitgo-r-share', from: 'bitgo', to: 'user' }); + + nock(bitgoApiUrl) + .persist() + .get(`/api/v2/wallet/${walletId}/txrequests`) + .query(true) + .matchHeader('any', () => true) + .reply(200, { txRequests: [signedTxRequest] }); + + const sendGShareNock = nock(bitgoApiUrl) + .post( + `/api/v2/wallet/${walletId}/txrequests/${tssTxRequestId}/transactions/0/signatureshares`, + ) + .matchHeader('any', () => true) + .reply(200, { share: 'user-to-bitgo-g-share', from: 'bitgo', to: 'user' }); + + const transferNock = nock(bitgoApiUrl) + .post(`/api/v2/wallet/${walletId}/txrequests/${tssTxRequestId}/transfers`) + .matchHeader('any', () => true) + .reply(200, { state: 'signed' }); + + const signMpcCommitmentNockAwm = nock(advancedWalletManagerUrl) + .post(`/api/${coin}/mpc/sign/commitment`) .reply(200, { - id: 'user-key-id', - pub: 'xpub_user', - commonKeychain: 'test-common-keychain', - source: 'user', - type: 'tss', + userToBitgoCommitment: { share: 'user-commitment-share' }, + encryptedSignerShare: { share: 'encrypted-signer-share' }, + encryptedUserToBitgoRShare: { share: 'encrypted-user-to-bitgo-r-share' }, + encryptedDataKey: 'test-encrypted-data-key', }); - const sendManyStub = sinon.stub(Wallet.prototype, 'sendMany').resolves({ - txRequest: { - txRequestId: 'test-tx-request-id', - state: 'signed', - apiVersion: 'full', - pendingApprovalId: 'test-pending-approval-id', - transactions: [ - { - state: 'signed', - unsignedTx: { - derivationPath: 'm/0', - signableHex: 'testMessage', - serializedTxHex: 'testSerializedTxHex', - }, - signatureShares: [], - signedTx: { - id: 'test-tx-id', - tx: 'signed-transaction', - }, - }, - ], - }, - txid: 'test-tx-id', - tx: 'signed-transaction', - }); - // Mock multisigType to return 'tss' - const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss'); + const signMpcRShareNockAwm = nock(advancedWalletManagerUrl) + .post(`/api/${coin}/mpc/sign/r`) + .reply(200, { + rShare: { + rShares: [ + { r: 'r-share', R: 'R-share' }, + { r: 'r-share-2', R: 'R-share-2' }, + { r: 'r-share-3', R: 'R-share-3' }, + { r: 'r-share-4', R: 'R-share-4', i: 3, j: 1 }, + ], + }, + }); + + const signMpcGShareNockAwm = nock(advancedWalletManagerUrl) + .post(`/api/${coin}/mpc/sign/g`) + .reply(200, { + gShare: { r: 'r', gamma: 'gamma', i: 1, j: 3, n: 4 }, + }); const response = await agent .post(`/api/v1/${coin}/advancedwallet/${walletId}/sendMany`) @@ -449,21 +805,28 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { response.body.should.have.property('txid', 'test-tx-id'); response.body.should.have.property('tx', 'signed-transaction'); - // Verify that type defaults to 'transfer' for TSS wallets when not provided - const sendManyArgs = sendManyStub.firstCall.args[0] as Record; - sendManyArgs.should.have.property('type', 'transfer'); + capturedTxRequestBody!.should.have.property('intent'); + (capturedTxRequestBody!.intent as Record).should.have.property( + 'intentType', + 'payment', + ); walletGetNock.done(); - keychainGetNock.done(); - sinon.assert.calledOnce(sendManyStub); - sinon.assert.calledOnce(multisigTypeStub); + createTxRequestNock.done(); + deleteSigSharesNock.done(); + exchangeCommitmentsNock.done(); + offerRShareNock.done(); + sendGShareNock.done(); + transferNock.done(); + signMpcCommitmentNockAwm.done(); + signMpcRShareNockAwm.done(); + signMpcGShareNockAwm.done(); }); }); describe('SendMany TSS ECDSA:', () => { const coin = 'hteth'; it('should send many transactions using ECDSA TSS signing', async () => { - // Mock wallet get request for TSS wallet const walletGetNock = nock(bitgoApiUrl) .get(`/api/v2/${coin}/wallet/${walletId}`) .matchHeader('any', () => true) @@ -472,48 +835,15 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { type: 'advanced', keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], multisigType: 'tss', + multisigTypeVersion: 'MPCv2', }); - // Mock keychain get request for TSS keychain - const keychainGetNock = nock(bitgoApiUrl) - .get(`/api/v2/${coin}/key/user-key-id`) - .matchHeader('any', () => true) - .reply(200, { - id: 'user-key-id', - pub: 'xpub_user', - commonKeychain: 'test-common-keychain', - source: 'user', - type: 'tss', - }); - - const sendManyStub = sinon.stub(Wallet.prototype, 'sendMany').resolves({ - txRequest: { - txRequestId: 'test-tx-request-id', - state: 'signed', - apiVersion: 'full', - pendingApprovalId: 'test-pending-approval-id', - transactions: [ - { - state: 'signed', - unsignedTx: { - derivationPath: 'm/0', - signableHex: 'testMessage', - serializedTxHex: 'testSerializedTxHex', - }, - signatureShares: [], - signedTx: { - id: 'test-tx-id', - tx: 'signed-transaction', - }, - }, - ], - }, - txid: 'test-tx-id', - tx: 'signed-transaction', - }); - - // Mock multisigType to return 'tss' - const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss'); + const nocks = nockEcdsaMPCv2SigningFlow( + coin, + walletId, + bitgoApiUrl, + advancedWalletManagerUrl, + ); const response = await agent .post(`/api/v1/${coin}/advancedwallet/${walletId}/sendMany`) @@ -539,13 +869,18 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { response.body.should.have.property('tx', 'signed-transaction'); walletGetNock.done(); - keychainGetNock.done(); - sinon.assert.calledOnce(sendManyStub); - sinon.assert.calledOnce(multisigTypeStub); + nocks.createTxRequestNock.done(); + nocks.round1SignNock.done(); + nocks.round2SignNock.done(); + nocks.round3SignNock.done(); + nocks.sendTxNock.done(); + nocks.transferNock.done(); + nocks.awmRound1Nock.done(); + nocks.awmRound2Nock.done(); + nocks.awmRound3Nock.done(); }); it('should be able to sign a fill nonce transaction', async () => { - // Mock wallet get request for TSS wallet const walletGetNock = nock(bitgoApiUrl) .get(`/api/v2/${coin}/wallet/${walletId}`) .matchHeader('any', () => true) @@ -554,48 +889,15 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { type: 'advanced', keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], multisigType: 'tss', + multisigTypeVersion: 'MPCv2', }); - // Mock keychain get request for TSS keychain - const keychainGetNock = nock(bitgoApiUrl) - .get(`/api/v2/${coin}/key/user-key-id`) - .matchHeader('any', () => true) - .reply(200, { - id: 'user-key-id', - pub: 'xpub_user', - commonKeychain: 'test-common-keychain', - source: 'user', - type: 'tss', - }); - - const sendManyStub = sinon.stub(Wallet.prototype, 'sendMany').resolves({ - txRequest: { - txRequestId: 'test-tx-request-id', - state: 'signed', - apiVersion: 'full', - pendingApprovalId: 'test-pending-approval-id', - transactions: [ - { - state: 'signed', - unsignedTx: { - derivationPath: 'm/0', - signableHex: 'testMessage', - serializedTxHex: 'testSerializedTxHex', - }, - signatureShares: [], - signedTx: { - id: 'test-tx-id', - tx: 'signed-transaction', - }, - }, - ], - }, - txid: 'test-tx-id', - tx: 'signed-transaction', - }); - - // Mock multisigType to return 'tss' - const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss'); + const nocks = nockEcdsaMPCv2SigningFlow( + coin, + walletId, + bitgoApiUrl, + advancedWalletManagerUrl, + ); const response = await agent .post(`/api/v1/${coin}/advancedwallet/${walletId}/sendMany`) @@ -613,9 +915,15 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { response.body.should.have.property('tx', 'signed-transaction'); walletGetNock.done(); - keychainGetNock.done(); - sinon.assert.calledOnce(sendManyStub); - sinon.assert.calledOnce(multisigTypeStub); + nocks.createTxRequestNock.done(); + nocks.round1SignNock.done(); + nocks.round2SignNock.done(); + nocks.round3SignNock.done(); + nocks.sendTxNock.done(); + nocks.transferNock.done(); + nocks.awmRound1Nock.done(); + nocks.awmRound2Nock.done(); + nocks.awmRound3Nock.done(); }); it('should fail when backup key is used for ECDSA TSS signing', async () => { @@ -628,6 +936,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { type: 'advanced', keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'], multisigType: 'tss', + multisigTypeVersion: 'MPCv2', }); // Mock keychain get request for backup TSS keychain @@ -642,20 +951,6 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { type: 'tss', }); - const sendManyStub = sinon.stub(Wallet.prototype, 'sendMany').resolves({ - txRequest: { - txRequestId: 'test-tx-request-id', - state: 'signed', - apiVersion: 'full', - pendingApprovalId: 'test-pending-approval-id', - }, - txid: 'test-tx-id', - tx: 'signed-transaction', - }); - - // Mock multisigType to return 'tss' - const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss'); - const response = await agent .post(`/api/v1/${coin}/advancedwallet/${walletId}/sendMany`) .set('Authorization', `Bearer ${accessToken}`) @@ -675,8 +970,6 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { walletGetNock.done(); keychainGetNock.done(); - sinon.assert.notCalled(sendManyStub); - sinon.assert.calledOnce(multisigTypeStub); }); }); @@ -768,15 +1061,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { pub: 'xpub_user', }); - const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { - nP2SHInputs: 1, - nSegwitInputs: 0, - nOutputs: 2, - }, - walletId, - }); + const prebuildStub = sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); // Mock verifyTransaction to return false const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(false); @@ -823,15 +1110,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { pub: 'xpub_user', }); - const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { - nP2SHInputs: 1, - nSegwitInputs: 0, - nOutputs: 2, - }, - walletId, - }); + const prebuildStub = sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); // Mock verifyTransaction to throw an error const verifyStub = sinon @@ -891,15 +1172,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { - nP2SHInputs: 1, - nSegwitInputs: 0, - nOutputs: 2, - }, - walletId, - }); + const prebuildStub = sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -993,8 +1268,6 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { commonKeychain: 'test-common-keychain', }); - const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss'); - const response = await agent .post(`/api/v1/${coin}/advancedwallet/${walletId}/sendMany`) .set('Authorization', `Bearer ${accessToken}`) @@ -1010,7 +1283,6 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { walletGetNock.done(); keychainGetNock.done(); - sinon.assert.calledOnce(multisigTypeStub); }); it('should ignore commonKeychain param for multisig wallet', async () => { @@ -1049,11 +1321,9 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { pub: 'xpub_bitgo', }); - const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txHex: 'prebuilt-tx-hex', - txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, - walletId, - }); + const prebuildStub = sinon + .stub(Wallet.prototype, 'prebuildTransaction') + .resolves(mockMultisigPrebuildResponse(walletId)); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); From eadac71014a8ae43d6558cb105f3efffc8c5509a Mon Sep 17 00:00:00 2001 From: Daniel Peng Date: Fri, 29 May 2026 14:54:17 -0400 Subject: [PATCH 2/2] refactor: create testUtils and nock prebuildTransaction Ticket: WAL-1489 --- src/__tests__/api/master/ecdsa.test.ts | 10 +- src/__tests__/api/master/eddsa.test.ts | 8 +- .../api/master/generateWallet.test.ts | 8 +- src/__tests__/api/master/sendMany.test.ts | 117 ++++++++++-------- src/__tests__/api/master/testUtils.ts | 8 ++ 5 files changed, 79 insertions(+), 72 deletions(-) create mode 100644 src/__tests__/api/master/testUtils.ts diff --git a/src/__tests__/api/master/ecdsa.test.ts b/src/__tests__/api/master/ecdsa.test.ts index 7b48bc20..11cdadba 100644 --- a/src/__tests__/api/master/ecdsa.test.ts +++ b/src/__tests__/api/master/ecdsa.test.ts @@ -12,16 +12,10 @@ import { TxRequest, Wallet, } from '@bitgo-beta/sdk-core'; +import { BitGoAPI } from '@bitgo-beta/sdk-api'; import { AdvancedWalletManagerClient } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; import { signAndSendEcdsaMPCv2FromTxRequest } from '../../../masterBitgoExpress/handlers/ecdsa'; -import { BitGoAPI } from '@bitgo-beta/sdk-api'; - -class BitGoAPITestHarness extends BitGoAPI { - static clearConstantsCache(): void { - BitGoAPI._constants = {}; - BitGoAPI._constantsExpire = {}; - } -} +import { BitGoAPITestHarness } from './testUtils'; describe('Ecdsa Signing Handler', () => { let bitgo: BitGoAPI; diff --git a/src/__tests__/api/master/eddsa.test.ts b/src/__tests__/api/master/eddsa.test.ts index 3059927f..eac7bdcc 100644 --- a/src/__tests__/api/master/eddsa.test.ts +++ b/src/__tests__/api/master/eddsa.test.ts @@ -13,13 +13,7 @@ import { import { BitGoAPI } from '@bitgo-beta/sdk-api'; import { AdvancedWalletManagerClient as AdvancedWalletManagerClient } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; import { handleEddsaSigning } from '../../../masterBitgoExpress/handlers/eddsa'; - -class BitGoAPITestHarness extends BitGoAPI { - static clearConstantsCache(): void { - BitGoAPI._constants = {}; - BitGoAPI._constantsExpire = {}; - } -} +import { BitGoAPITestHarness } from './testUtils'; describe('Eddsa Signing Handler', () => { let bitgo: BitGoBase; diff --git a/src/__tests__/api/master/generateWallet.test.ts b/src/__tests__/api/master/generateWallet.test.ts index 7a8513e6..b169c0e8 100644 --- a/src/__tests__/api/master/generateWallet.test.ts +++ b/src/__tests__/api/master/generateWallet.test.ts @@ -10,13 +10,7 @@ import { Environments } from '@bitgo-beta/sdk-core'; import { BitGoAPI } from '@bitgo-beta/sdk-api'; import * as middleware from '../../../shared/middleware'; import { BitGoRequest } from '../../../types/request'; - -class BitGoAPITestHarness extends BitGoAPI { - static clearConstantsCache(): void { - BitGoAPI._constants = {}; - BitGoAPI._constantsExpire = {}; - } -} +import { BitGoAPITestHarness } from './testUtils'; function mockWalletResponse(id: string, coinName: string, overrides: Record = {}) { return { diff --git a/src/__tests__/api/master/sendMany.test.ts b/src/__tests__/api/master/sendMany.test.ts index 111a516b..53f43801 100644 --- a/src/__tests__/api/master/sendMany.test.ts +++ b/src/__tests__/api/master/sendMany.test.ts @@ -10,35 +10,20 @@ import { openpgpUtils, SignatureShareRecord, SignatureShareType, - Wallet, } from '@bitgo-beta/sdk-core'; -import { BitGoAPI } from '@bitgo-beta/sdk-api'; +import * as utxolib from '@bitgo-beta/utxo-lib'; import { Tbtc } from '@bitgo-beta/sdk-coin-btc'; import { Tsol } from '@bitgo-beta/sdk-coin-sol'; import assert from 'assert'; - -class BitGoAPITestHarness extends BitGoAPI { - static clearConstantsCache(): void { - BitGoAPI._constants = {}; - BitGoAPI._constantsExpire = {}; - } -} +import { BitGoAPITestHarness } from './testUtils'; const testWalletId = 'test-wallet-id'; const testBitgoApiUrl = Environments.test.uri; const tssTxRequestId = 'test-tx-request-id'; -function mockMultisigPrebuildResponse(walletIdParam: string) { - return { - txHex: 'prebuilt-tx-hex', - txInfo: { - nP2SHInputs: 1, - nSegwitInputs: 0, - nOutputs: 2, - }, - walletId: walletIdParam, - }; -} +const TBTC_PREBUILD_PSBT_HEX = utxolib.bitgo + .createPsbtForNetwork({ network: utxolib.networks.testnet }) + .toHex(); function buildPendingEdDsaTxRequest(walletIdParam: string) { return { @@ -404,9 +389,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - const prebuildStub = sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + const prebuildBuildNock = nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -459,7 +448,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { response.body.should.have.property('status', 'signed'); walletGetNock.done(); - sinon.assert.calledOnce(prebuildStub); + prebuildBuildNock.done(); sinon.assert.calledOnce(verifyStub); keychainGetNock.done(); signNock.done(); @@ -493,9 +482,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -559,9 +552,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -625,9 +622,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - const prebuildStub = sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + const prebuildBuildNock = nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -676,7 +677,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { response.body.should.have.property('status', 'signed'); walletGetNock.done(); - sinon.assert.calledOnce(prebuildStub); + prebuildBuildNock.done(); sinon.assert.calledOnce(verifyStub); keychainGetNock.done(); signNock.done(); @@ -1061,9 +1062,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { pub: 'xpub_user', }); - const prebuildStub = sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + const prebuildBuildNock = nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); // Mock verifyTransaction to return false const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(false); @@ -1086,7 +1091,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { walletGetNock.done(); keychainGetNock.done(); - sinon.assert.calledOnce(prebuildStub); + prebuildBuildNock.done(); sinon.assert.calledOnce(verifyStub); }); @@ -1110,9 +1115,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { pub: 'xpub_user', }); - const prebuildStub = sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + const prebuildBuildNock = nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); // Mock verifyTransaction to throw an error const verifyStub = sinon @@ -1137,7 +1146,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { walletGetNock.done(); keychainGetNock.done(); - sinon.assert.calledOnce(prebuildStub); + prebuildBuildNock.done(); sinon.assert.calledOnce(verifyStub); }); @@ -1172,9 +1181,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { .matchHeader('any', () => true) .reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo' }); - const prebuildStub = sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + const prebuildBuildNock = nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -1207,7 +1220,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { walletGetNock.done(); keychainGetNock.done(); - sinon.assert.calledOnce(prebuildStub); + prebuildBuildNock.done(); sinon.assert.calledOnce(verifyStub); signNock.done(); }); @@ -1321,9 +1334,13 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { pub: 'xpub_bitgo', }); - const prebuildStub = sinon - .stub(Wallet.prototype, 'prebuildTransaction') - .resolves(mockMultisigPrebuildResponse(walletId)); + const prebuildBuildNock = nock(bitgoApiUrl) + .post(`/api/v2/${coin}/wallet/${walletId}/tx/build`) + .reply(200, { + txHex: TBTC_PREBUILD_PSBT_HEX, + txInfo: { nP2SHInputs: 1, nSegwitInputs: 0, nOutputs: 2 }, + }); + nock(bitgoApiUrl).get(`/api/v2/${coin}/public/block/latest`).reply(200, { height: 800000 }); const verifyStub = sinon.stub(Tbtc.prototype, 'verifyTransaction').resolves(true); @@ -1361,7 +1378,7 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/sendMany', () => { keychainGetNock.done(); backupKeychainGetNock.done(); bitgoKeychainGetNock.done(); - sinon.assert.calledOnce(prebuildStub); + prebuildBuildNock.done(); sinon.assert.calledOnce(verifyStub); signNock.done(); submitNock.done(); diff --git a/src/__tests__/api/master/testUtils.ts b/src/__tests__/api/master/testUtils.ts new file mode 100644 index 00000000..22653dd9 --- /dev/null +++ b/src/__tests__/api/master/testUtils.ts @@ -0,0 +1,8 @@ +import { BitGoAPI } from '@bitgo-beta/sdk-api'; + +export class BitGoAPITestHarness extends BitGoAPI { + static clearConstantsCache(): void { + BitGoAPI._constants = {}; + BitGoAPI._constantsExpire = {}; + } +}