diff --git a/src/__tests__/integration/fixtures/bitgo/blockLatest.json b/src/__tests__/integration/fixtures/bitgo/blockLatest.json new file mode 100644 index 00000000..f4ac00a3 --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/blockLatest.json @@ -0,0 +1,3 @@ +{ + "height": 800000 +} diff --git a/src/__tests__/integration/fixtures/bitgo/getKeychain.backup.json b/src/__tests__/integration/fixtures/bitgo/getKeychain.backup.json new file mode 100644 index 00000000..d2877b0a --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/getKeychain.backup.json @@ -0,0 +1,6 @@ +{ + "id": "backup-key-id", + "pub": "xpub661MyMwAqRbcEnWZUJZRDiFY4SwUJdfPDLposNTiqZSvETPaQSkBsKJkCPmtptoc6iBZsG84qncYi1K1NUWjELHNfNjhTLGU2KvTDP9Evui", + "source": "backup", + "type": "independent" +} diff --git a/src/__tests__/integration/fixtures/bitgo/getKeychain.bitgo.json b/src/__tests__/integration/fixtures/bitgo/getKeychain.bitgo.json new file mode 100644 index 00000000..655dd8c3 --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/getKeychain.bitgo.json @@ -0,0 +1,7 @@ +{ + "id": "bitgo-key-id", + "pub": "xpub661MyMwAqRbcF5aJh6F6ToXGzgTs7pfPxgFx1QLGzUx6FfehAodsCW1Zv4DRb5P95BVg8birSFoMQVWpKhL5wLogDNxiusG4CCtouPeDzfK", + "source": "bitgo", + "type": "independent", + "isBitGo": true +} diff --git a/src/__tests__/integration/fixtures/bitgo/getKeychain.user.json b/src/__tests__/integration/fixtures/bitgo/getKeychain.user.json new file mode 100644 index 00000000..139bcee4 --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/getKeychain.user.json @@ -0,0 +1,6 @@ +{ + "id": "user-key-id", + "pub": "xpub661MyMwAqRbcEvJQx6spkkHLRgtjxmVdyDSvbDt2m9NFpbkHdcu5WJsHHHqFxNATbNHnhMWJiwckoMqF75EpcNhU9xeVM4oDS7urM3os4BH", + "source": "user", + "type": "independent" +} diff --git a/src/__tests__/integration/fixtures/bitgo/getWallet.hteth.json b/src/__tests__/integration/fixtures/bitgo/getWallet.hteth.json new file mode 100644 index 00000000..64a862e5 --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/getWallet.hteth.json @@ -0,0 +1,19 @@ +{ + "id": "test-wallet-id", + "coin": "hteth", + "type": "advanced", + "multisigType": "onchain", + "keys": ["user-key-id", "backup-key-id", "bitgo-key-id"], + "isCold": true, + "m": 2, + "n": 3, + "enterprise": "test-enterprise", + "label": "test-wallet", + "balance": 0, + "balanceString": "0", + "confirmedBalanceString": "0", + "spendableBalanceString": "0", + "deleted": false, + "approvalsRequired": 1, + "coinSpecific": {} +} diff --git a/src/__tests__/integration/fixtures/bitgo/getWallet.tbtc.json b/src/__tests__/integration/fixtures/bitgo/getWallet.tbtc.json new file mode 100644 index 00000000..4cc9c27c --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/getWallet.tbtc.json @@ -0,0 +1,19 @@ +{ + "id": "test-wallet-id", + "coin": "tbtc", + "type": "advanced", + "multisigType": "onchain", + "keys": ["user-key-id", "backup-key-id", "bitgo-key-id"], + "isCold": true, + "m": 2, + "n": 3, + "enterprise": "test-enterprise", + "label": "test-wallet", + "balance": 0, + "balanceString": "0", + "confirmedBalanceString": "0", + "spendableBalanceString": "0", + "deleted": false, + "approvalsRequired": 1, + "coinSpecific": {} +} diff --git a/src/__tests__/integration/fixtures/bitgo/prebuildTx.hteth.json b/src/__tests__/integration/fixtures/bitgo/prebuildTx.hteth.json new file mode 100644 index 00000000..6b2f4192 --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/prebuildTx.hteth.json @@ -0,0 +1,11 @@ +{ + "coin": "hteth", + "recipients": [ + { + "address": "0xe01866e64418db20a2831e41eb11eca2a77245a3", + "amount": "100000000000000" + } + ], + "nextContractSequenceId": 1, + "expireTime": 1893456000 +} diff --git a/src/__tests__/integration/fixtures/bitgo/prebuildTx.tbtc.json b/src/__tests__/integration/fixtures/bitgo/prebuildTx.tbtc.json new file mode 100644 index 00000000..e3502a31 --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/prebuildTx.tbtc.json @@ -0,0 +1,4 @@ +{ + "txHex": "70736274ff01005e0200000001d89f04fb23bfe665e2d5a64c24452581a3cca2671a1fa6b4e404bccb6fc9f46f0100000000ffffffff0110270000000000002200206a2459d26e8c6ca0595ad9b9e952ec8bdbd08b49cba186d64d3fb3c248c40bea000000000001012ba086010000000000220020b41d3a1e7ae51e81e1f5ac6518d6a4c449f1e1744461b9a298452913c7c14a9e01056952210295bc96ecdb5ceae2716051cd835ec0e635ff3ab464a992c0b2d7dbdf969d682021038c69636106774394c8369affe354e33b63a0deb196278320be2eaa273a5dcf542102020e1b0585251821432d92d3d716774aee750cb0758b52d4e51a480a952d410153ae220602020e1b0585251821432d92d3d716774aee750cb0758b52d4e51a480a952d410114d3b12b170000000000000000140000000000000022060295bc96ecdb5ceae2716051cd835ec0e635ff3ab464a992c0b2d7dbdf969d6820146bf51aa8000000000000000014000000000000002206038c69636106774394c8369affe354e33b63a0deb196278320be2eaa273a5dcf5414460c6594000000000000000014000000000000000000", + "txInfo": { "nP2SHInputs": 0, "nSegwitInputs": 1, "nOutputs": 1 } +} diff --git a/src/__tests__/integration/fixtures/bitgo/sendTx.hteth.json b/src/__tests__/integration/fixtures/bitgo/sendTx.hteth.json new file mode 100644 index 00000000..a612dda8 --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/sendTx.hteth.json @@ -0,0 +1,14 @@ +{ + "transfer": { + "id": "test-transfer-id", + "coin": "hteth", + "txid": "0xtest-eth-tx-id", + "state": "signed", + "value": -100000000000000, + "valueString": "-100000000000000", + "entries": [] + }, + "txid": "0xtest-eth-tx-id", + "tx": "0x", + "status": "signed" +} diff --git a/src/__tests__/integration/fixtures/bitgo/sendTx.tbtc.json b/src/__tests__/integration/fixtures/bitgo/sendTx.tbtc.json new file mode 100644 index 00000000..a7fe138d --- /dev/null +++ b/src/__tests__/integration/fixtures/bitgo/sendTx.tbtc.json @@ -0,0 +1,14 @@ +{ + "transfer": { + "id": "test-transfer-id", + "coin": "tbtc", + "txid": "test-tx-id", + "state": "signed", + "value": -10202, + "valueString": "-10202", + "entries": [] + }, + "txid": "test-tx-id", + "tx": "01000000000101030a0000", + "status": "signed" +} diff --git a/src/__tests__/integration/fixtures/keyProvider/sign.eth.json b/src/__tests__/integration/fixtures/keyProvider/sign.eth.json new file mode 100644 index 00000000..b1240b61 --- /dev/null +++ b/src/__tests__/integration/fixtures/keyProvider/sign.eth.json @@ -0,0 +1,3 @@ +{ + "signature": "0xa1b2c3d4e5f60718293a4b5c6d7e8f9012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789011b" +} diff --git a/src/__tests__/integration/fixtures/keyProvider/sign.json b/src/__tests__/integration/fixtures/keyProvider/sign.json new file mode 100644 index 00000000..1fbe819d --- /dev/null +++ b/src/__tests__/integration/fixtures/keyProvider/sign.json @@ -0,0 +1,3 @@ +{ + "signature": "70736274ff01005e0200000001d89f04fb23bfe665e2d5a64c24452581a3cca2671a1fa6b4e404bccb6fc9f46f0100000000ffffffff0110270000000000002200206a2459d26e8c6ca0595ad9b9e952ec8bdbd08b49cba186d64d3fb3c248c40bea000000000001012ba086010000000000220020b41d3a1e7ae51e81e1f5ac6518d6a4c449f1e1744461b9a298452913c7c14a9e01056952210295bc96ecdb5ceae2716051cd835ec0e635ff3ab464a992c0b2d7dbdf969d682021038c69636106774394c8369affe354e33b63a0deb196278320be2eaa273a5dcf542102020e1b0585251821432d92d3d716774aee750cb0758b52d4e51a480a952d410153ae220602020e1b0585251821432d92d3d716774aee750cb0758b52d4e51a480a952d410114d3b12b170000000000000000140000000000000022060295bc96ecdb5ceae2716051cd835ec0e635ff3ab464a992c0b2d7dbdf969d6820146bf51aa8000000000000000014000000000000002206038c69636106774394c8369affe354e33b63a0deb196278320be2eaa273a5dcf5414460c6594000000000000000014000000000000000000" +} diff --git a/src/__tests__/integration/helpers/mockBitgoServer.ts b/src/__tests__/integration/helpers/mockBitgoServer.ts index f2e02185..10dee8e1 100644 --- a/src/__tests__/integration/helpers/mockBitgoServer.ts +++ b/src/__tests__/integration/helpers/mockBitgoServer.ts @@ -17,10 +17,28 @@ export interface MockBitgoServer { const FIXTURES_DIR = path.resolve(__dirname, '../fixtures/bitgo'); -function loadFixture(name: string): unknown { +function loadFixture(name: string): Record { return require(`${FIXTURES_DIR}/${name}.json`); } +type SendManyFixtureMethod = 'getWallet' | 'prebuildTx' | 'sendTx'; +type SupportedCoin = 'hteth' | 'tbtc'; +type CoinToFixtures = { + [K in SendManyFixtureMethod]: `${K}.${C}`; +}; + +/** Registry — add a new coin here to support it across all sendMany integ test routes */ +const COIN_FIXTURES: { [C in SupportedCoin]: CoinToFixtures } = { + hteth: { getWallet: 'getWallet.hteth', prebuildTx: 'prebuildTx.hteth', sendTx: 'sendTx.hteth' }, + tbtc: { getWallet: 'getWallet.tbtc', prebuildTx: 'prebuildTx.tbtc', sendTx: 'sendTx.tbtc' }, +}; + +function coinFixtures(coin: string): CoinToFixtures { + const fixtures = COIN_FIXTURES[coin as SupportedCoin]; + if (!fixtures) throw new Error(`No fixtures registered for coin: ${coin}`); + return fixtures; +} + export async function startMockBitgoServer(): Promise { const calls: MockBitgoCall[] = []; @@ -43,17 +61,52 @@ export async function startMockBitgoServer(): Promise { const source = req.body?.source; const fixtureName = source === 'user' ? 'addKey.user' : source === 'backup' ? 'addKey.backup' : 'addKey.bitgo'; - const fixture = loadFixture(fixtureName) as Record; + const fixture = loadFixture(fixtureName); return res.json({ ...fixture, coin }); }); /** Create wallet */ app.post('/api/v2/:coin/wallet/add', (req, res) => { const { coin } = req.params; - const fixture = loadFixture('createWallet') as Record; + const fixture = loadFixture('createWallet'); + res.json({ ...fixture, coin }); + }); + + /** Get wallet — coin-specific fixture */ + app.get('/api/v2/:coin/wallet/:walletId', (req, res) => { + const { coin } = req.params; + const fixture = loadFixture(coinFixtures(coin).getWallet); + res.json({ ...fixture, coin }); + }); + + /** Get keychain — matched by keyId */ + app.get('/api/v2/:coin/key/:keyId', (req, res) => { + const { keyId, coin } = req.params; + const fixtureName = + keyId === 'user-key-id' + ? 'getKeychain.user' + : keyId === 'backup-key-id' + ? 'getKeychain.backup' + : 'getKeychain.bitgo'; + const fixture = loadFixture(fixtureName); res.json({ ...fixture, coin }); }); + /** Block height for fee estimation */ + app.get('/api/v2/:coin/public/block/latest', (_req, res) => { + res.json(loadFixture('blockLatest')); + }); + + /** Transaction prebuild — coin-specific fixture */ + app.post('/api/v2/:coin/wallet/:walletId/tx/build', (req, res) => { + res.json(loadFixture(coinFixtures(req.params.coin).prebuildTx)); + }); + + /** Transaction submit — coin-specific fixture */ + app.post('/api/v2/:coin/wallet/:walletId/tx/send', (req, res) => { + res.json(loadFixture(coinFixtures(req.params.coin).sendTx)); + }); + const server = http.createServer(app); const port = await listen(server); diff --git a/src/__tests__/integration/helpers/mockKeyProviderServer.ts b/src/__tests__/integration/helpers/mockKeyProviderServer.ts index 69dd3d7d..09d6b183 100644 --- a/src/__tests__/integration/helpers/mockKeyProviderServer.ts +++ b/src/__tests__/integration/helpers/mockKeyProviderServer.ts @@ -1,10 +1,15 @@ import * as http from 'http'; +import * as path from 'path'; import express from 'express'; import { BitGoAPI } from '@bitgo-beta/sdk-api'; import { Hteth } from '@bitgo-beta/sdk-coin-eth'; import { Tbtc } from '@bitgo-beta/sdk-coin-btc'; import { listen, close } from './servers'; +function loadKeyProviderFixture(name: string): Record { + return require(`${path.resolve(__dirname, '../fixtures/keyProvider')}/${name}.json`); +} + export interface MockKeyProviderCall { method: string; path: string; @@ -67,6 +72,24 @@ export async function startMockKeyProviderServer(): Promise { + const entry = keyStore.get(req.params.pub); + if (!entry) { + res.status(404).json({ message: `Key not found: ${req.params.pub}` }); + return; + } + res.json({ pub: req.params.pub, ...entry }); + }); + + /** External signing mode — key provider signs the payload, returns signed PSBT or ETH sig. + * ETH signablePayload starts with 0x (operation hash), BTC is a PSBT hex. */ + app.post('/sign', (req, res) => { + const { signablePayload } = req.body; + const isEth = typeof signablePayload === 'string' && signablePayload.startsWith('0x'); + res.json(loadKeyProviderFixture(isEth ? 'sign.eth' : 'sign')); + }); + const server = http.createServer(app); const port = await listen(server); diff --git a/src/__tests__/integration/sendMany.integ.test.ts b/src/__tests__/integration/sendMany.integ.test.ts new file mode 100644 index 00000000..08929ad0 --- /dev/null +++ b/src/__tests__/integration/sendMany.integ.test.ts @@ -0,0 +1,260 @@ +import 'should'; +import { startServices, IntegServices } from './helpers/setup'; +import { LOCALHOST } from './helpers/servers'; +import { SigningMode } from '../../shared/types'; + +/** + * Deterministic test keypair derived from Buffer.alloc(64, 0x42) — a public, reproducible seed. + * Not a secret. Never funded. Matches getKeychain.user.json and prebuildTx.tbtc.json. + */ +const USER_XPUB = + 'xpub661MyMwAqRbcEvJQx6spkkHLRgtjxmVdyDSvbDt2m9NFpbkHdcu5WJsHHHqFxNATbNHnhMWJiwckoMqF75EpcNhU9xeVM4oDS7urM3os4BH'; +const USER_XPRV = + 'xprv9s21ZrQH143K2SDwr5LpPcLbsf4FZJmnbzXKnqURCoqGwoR965apxWYoS2DKu2ivcMTB9uTK6XhZDEPfTeNXGf7mmACuMN6cFS5ttmrpZ3i'; + +const WALLET_ID = 'test-wallet-id'; +const ETH_RECIPIENT = '0xe01866e64418db20a2831e41eb11eca2a77245a3'; + +describe('Send many: EXTERNAL signing', () => { + let services: IntegServices; + + before(async () => { + services = await startServices({ signingMode: SigningMode.EXTERNAL }); + }); + + after(async () => { + await services.teardown(); + }); + + beforeEach(() => { + services.keyProvider.calls.length = 0; + services.bitgo.calls.length = 0; + }); + + it('signs and submits a tbtc sendMany transaction via external key provider', async () => { + const res = await fetch( + `http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/${WALLET_ID}/sendMany`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' }, + body: JSON.stringify({ + recipients: [ + { + address: 'tb1qdgj9n5nw33k2qk26mxu7j5hv30dapz6fewscd4jd87euyjxyp04qgphg92', + amount: '10000', + }, + ], + source: 'user', + pubkey: + 'xpub661MyMwAqRbcEvJQx6spkkHLRgtjxmVdyDSvbDt2m9NFpbkHdcu5WJsHHHqFxNATbNHnhMWJiwckoMqF75EpcNhU9xeVM4oDS7urM3os4BH', + }), + }, + ); + + res.status.should.equal(200); + const body = (await res.json()) as { txid: string; status: string }; + body.should.have.property('txid', 'test-tx-id'); + body.should.have.property('status', 'signed'); + + /** + * In external mode, AWM delegates signing to the key provider. + * POST /sign must be called — not POST /key (no local key generation for signing). + */ + const signCalls = services.keyProvider.calls.filter((c) => c.path === '/sign'); + signCalls.should.have.length(1); + + const storeCalls = services.keyProvider.calls.filter((c) => c.path === '/key'); + storeCalls.should.have.length(0); + + /** BitGo must receive tx/build and tx/send */ + const buildCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/tx/build')); + buildCalls.should.have.length(1); + + const sendCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/tx/send')); + sendCalls.should.have.length(1); + }); +}); + +describe('Send many: EXTERNAL signing (hteth — operation hash flow)', () => { + let services: IntegServices; + + before(async () => { + services = await startServices({ signingMode: SigningMode.EXTERNAL }); + }); + + after(async () => { + await services.teardown(); + }); + + beforeEach(() => { + services.keyProvider.calls.length = 0; + services.bitgo.calls.length = 0; + }); + + it('signs and submits an hteth sendMany via operation hash', async () => { + const res = await fetch( + `http://${LOCALHOST}:${services.mbePort}/api/v1/hteth/advancedwallet/${WALLET_ID}/sendMany`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' }, + body: JSON.stringify({ + recipients: [{ address: ETH_RECIPIENT, amount: '100000000000000' }], + source: 'user', + pubkey: + 'xpub661MyMwAqRbcEvJQx6spkkHLRgtjxmVdyDSvbDt2m9NFpbkHdcu5WJsHHHqFxNATbNHnhMWJiwckoMqF75EpcNhU9xeVM4oDS7urM3os4BH', + }), + }, + ); + + res.status.should.equal(200); + const body = (await res.json()) as { txid: string; status: string }; + body.should.have.property('txid', '0xtest-eth-tx-id'); + body.should.have.property('status', 'signed'); + + /** + * For ETH external signing, AWM computes operationHash = sha3(recipients + expireTime + sequenceId) + * and sends that hash (starting with 0x) to the key provider — NOT the full PSBT. + * This is the critical difference from the BTC flow. + */ + const signCalls = services.keyProvider.calls.filter((c) => c.path === '/sign'); + signCalls.should.have.length(1); + const signPayload = (signCalls[0].body as { signablePayload: string }).signablePayload; + signPayload.should.startWith('0x'); + + /** POST /key must NOT be called */ + services.keyProvider.calls.filter((c) => c.path === '/key').should.have.length(0); + + /** BitGo must receive tx/build and tx/send */ + services.bitgo.calls.filter((c) => c.path.endsWith('/tx/build')).should.have.length(1); + services.bitgo.calls.filter((c) => c.path.endsWith('/tx/send')).should.have.length(1); + }); +}); + +describe('Send many: LOCAL signing (tbtc)', () => { + let services: IntegServices; + + before(async () => { + services = await startServices({ signingMode: SigningMode.LOCAL }); + + /** + * Seed the mock key provider with a known xprv so AWM can retrieve it + * via GET /key/:pub and sign the PSBT locally. The xpub must match + * getKeychain.user.json and the bip32Derivation in prebuildTx.tbtc.json. + */ + await fetch(`http://127.0.0.1:${services.keyProvider.port}/key`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + pub: USER_XPUB, + prv: USER_XPRV, + coin: 'tbtc', + source: 'user', + type: 'independent', + }), + }); + }); + + after(async () => { + await services.teardown(); + }); + + beforeEach(() => { + services.keyProvider.calls.length = 0; + services.bitgo.calls.length = 0; + }); + + it('signs a tbtc sendMany locally using the stored xprv', async () => { + const res = await fetch( + `http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/${WALLET_ID}/sendMany`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' }, + body: JSON.stringify({ + recipients: [ + { + address: 'tb1qdgj9n5nw33k2qk26mxu7j5hv30dapz6fewscd4jd87euyjxyp04qgphg92', + amount: '10000', + }, + ], + source: 'user', + pubkey: USER_XPUB, + }), + }, + ); + + res.status.should.equal(200); + const body = (await res.json()) as { txid: string; status: string }; + body.should.have.property('txid', 'test-tx-id'); + body.should.have.property('status', 'signed'); + + /** + * In local mode, AWM retrieves the xprv via GET /key/:pub and signs internally. + * POST /sign must NOT be called — signing happens inside AWM, not in the key provider. + */ + services.keyProvider.calls.filter((c) => c.path === '/sign').should.have.length(0); + services.keyProvider.calls.filter((c) => c.path.startsWith('/key/')).length.should.be.above(0); + + services.bitgo.calls.filter((c) => c.path.endsWith('/tx/build')).should.have.length(1); + services.bitgo.calls.filter((c) => c.path.endsWith('/tx/send')).should.have.length(1); + }); +}); + +describe('Send many: LOCAL signing (hteth)', () => { + let services: IntegServices; + + before(async () => { + services = await startServices({ signingMode: SigningMode.LOCAL }); + + /** Seed mock key provider with known xprv so AWM can sign locally */ + await fetch(`http://127.0.0.1:${services.keyProvider.port}/key`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + pub: USER_XPUB, + prv: USER_XPRV, + coin: 'hteth', + source: 'user', + type: 'independent', + }), + }); + }); + + after(async () => { + await services.teardown(); + }); + + beforeEach(() => { + services.keyProvider.calls.length = 0; + services.bitgo.calls.length = 0; + }); + + it('signs an hteth sendMany locally using the stored xprv', async () => { + const res = await fetch( + `http://${LOCALHOST}:${services.mbePort}/api/v1/hteth/advancedwallet/${WALLET_ID}/sendMany`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' }, + body: JSON.stringify({ + recipients: [{ address: ETH_RECIPIENT, amount: '100000000000000' }], + source: 'user', + pubkey: USER_XPUB, + }), + }, + ); + + res.status.should.equal(200); + const body = (await res.json()) as { txid: string; status: string }; + body.should.have.property('txid', '0xtest-eth-tx-id'); + body.should.have.property('status', 'signed'); + + /** + * In local mode, AWM retrieves xprv via GET /key/:pub and signs the + * operation hash internally — POST /sign is never called. + */ + services.keyProvider.calls.filter((c) => c.path === '/sign').should.have.length(0); + services.keyProvider.calls.filter((c) => c.path.startsWith('/key/')).length.should.be.above(0); + + services.bitgo.calls.filter((c) => c.path.endsWith('/tx/build')).should.have.length(1); + services.bitgo.calls.filter((c) => c.path.endsWith('/tx/send')).should.have.length(1); + }); +});