diff --git a/modules/sdk-core/src/bitgo/trading/network/network.ts b/modules/sdk-core/src/bitgo/trading/network/network.ts index 90902cac32..7b3e362e4e 100644 --- a/modules/sdk-core/src/bitgo/trading/network/network.ts +++ b/modules/sdk-core/src/bitgo/trading/network/network.ts @@ -120,20 +120,16 @@ export class TradingNetwork implements ITradingNetwork { */ async prepareAllocation({ walletPassphrase, + clientExternalId = uuidV4(), + nonce = crypto.randomBytes(32).toString('hex'), ...body }: PrepareNetworkAllocationParams): Promise { - if (!body.clientExternalId) { - body.clientExternalId = uuidV4(); - } - if (!body.nonce) { - body.nonce = crypto.randomBytes(32).toString('hex'); - } - - const payload = JSON.stringify(body); + const allocation = { ...body, clientExternalId, nonce }; + const payload = JSON.stringify(allocation); const signature = await this.wallet.toTradingAccount().signPayload({ payload, walletPassphrase }); return { - ...body, + ...allocation, payload, signature, }; diff --git a/modules/sdk-core/src/bitgo/trading/network/types.ts b/modules/sdk-core/src/bitgo/trading/network/types.ts index 0d120fa07c..8d2d4aa7fd 100644 --- a/modules/sdk-core/src/bitgo/trading/network/types.ts +++ b/modules/sdk-core/src/bitgo/trading/network/types.ts @@ -124,7 +124,10 @@ export type GetNetworkAllocationByIdResponse = { allocation: NetworkAllocation; }; -export type PrepareNetworkAllocationParams = Omit & { +export type PrepareNetworkAllocationParams = Omit< + CreateNetworkAllocationParams, + 'payload' | 'signature' | 'clientExternalId' | 'nonce' +> & { walletPassphrase?: string; clientExternalId?: string; nonce?: string; diff --git a/modules/sdk-core/test/unit/bitgo/trading/network/network.test.ts b/modules/sdk-core/test/unit/bitgo/trading/network/network.test.ts new file mode 100644 index 0000000000..94c0c297e8 --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/trading/network/network.test.ts @@ -0,0 +1,150 @@ +/** + * @prettier + */ +import assert from 'assert'; +import sinon from 'sinon'; +import { TradingNetwork } from '../../../../../src'; + +describe('TradingNetwork', function () { + const enterpriseId = 'enterprise-id'; + const walletId = 'wallet-id'; + const signature = 'signature'; + let mockBitGo: any; + let mockWallet: any; + let requestCalls: Array<{ + method: string; + url: string; + headers: Record; + body: unknown; + }>; + let tradingNetwork: TradingNetwork; + let tradingAccount: { signPayload: sinon.SinonStub }; + + beforeEach(function () { + requestCalls = []; + tradingAccount = { signPayload: sinon.stub().resolves(signature) }; + mockWallet = { + id: sinon.stub().returns(walletId), + toTradingAccount: sinon.stub().returns(tradingAccount), + }; + + mockBitGo = { + microservicesUrl: sinon.stub().callsFake((path: string) => `https://microservices.example${path}`), + }; + + for (const method of ['get', 'post', 'put']) { + mockBitGo[method] = sinon.stub().callsFake((url: string) => { + const call: { method: string; url: string; headers: Record; body: unknown } = { + method, + url, + headers: {}, + body: undefined, + }; + requestCalls.push(call); + const request = { + set: sinon.stub().callsFake((name: string, value: string) => { + call.headers[name] = value; + return request; + }), + send: sinon.stub().callsFake((body: unknown) => { + call.body = body; + return request; + }), + result: sinon.stub().resolves({ ok: true }), + }; + return request; + }); + } + + tradingNetwork = new TradingNetwork(enterpriseId, mockWallet, mockBitGo); + }); + + it('uses enterprise-scoped URLs and headers for network reads', async function () { + await tradingNetwork.getBalances({ pageNumber: 2 }); + await tradingNetwork.getSettlementById({ settlementId: 'settlement-id' }); + + assert.deepStrictEqual(requestCalls, [ + { + method: 'get', + url: 'https://microservices.example/api/network/v1/enterprises/enterprise-id/clients/balances', + headers: { 'enterprise-id': enterpriseId }, + body: { pageNumber: 2 }, + }, + { + method: 'get', + url: 'https://microservices.example/api/network/v1/enterprises/enterprise-id/clients/settlements/settlement-id', + headers: { 'enterprise-id': enterpriseId }, + body: {}, + }, + ]); + }); + + it('prepares a signed allocation with generated identifiers', async function () { + const prepared = await tradingNetwork.prepareAllocation({ + connectionId: 'connection-id', + amount: { currency: 'tbtc' as string, quantity: '100' }, + notes: 'test', + }); + + assert.strictEqual(prepared.connectionId, 'connection-id'); + assert.strictEqual(prepared.signature, signature); + assert.match(prepared.clientExternalId, /^[0-9a-f-]{36}$/); + assert.match(prepared.nonce, /^[0-9a-f]{64}$/); + assert.strictEqual( + prepared.payload, + JSON.stringify({ + connectionId: 'connection-id', + amount: { currency: 'tbtc', quantity: '100' }, + notes: 'test', + clientExternalId: prepared.clientExternalId, + nonce: prepared.nonce, + }) + ); + tradingAccount.signPayload.calledOnce.should.be.true(); + tradingAccount.signPayload.firstCall.args[0].should.deepEqual({ + payload: prepared.payload, + walletPassphrase: undefined, + }); + }); + + it('submits allocation and deallocation payloads to connection-scoped endpoints', async function () { + const params = { + connectionId: 'connection-id', + payload: '{}', + signature, + amount: { currency: 'tbtc', quantity: '100' }, + clientExternalId: 'external-id', + nonce: 'nonce', + }; + + await tradingNetwork.createAllocation(params); + await tradingNetwork.createDeallocation(params); + + assert.deepStrictEqual(requestCalls, [ + { + method: 'post', + url: 'https://microservices.example/api/network/v1/enterprises/enterprise-id/clients/connections/connection-id/allocations', + headers: { 'enterprise-id': enterpriseId }, + body: { + payload: params.payload, + signature: params.signature, + amount: params.amount, + clientExternalId: params.clientExternalId, + nonce: params.nonce, + }, + }, + { + method: 'post', + url: 'https://microservices.example/api/network/v1/enterprises/enterprise-id/clients/connections/connection-id/deallocations', + headers: { 'enterprise-id': enterpriseId }, + body: { + payload: params.payload, + signature: params.signature, + amount: params.amount, + clientExternalId: params.clientExternalId, + nonce: params.nonce, + }, + }, + ]); + }); +});