Skip to content

Commit 4cf70a3

Browse files
committed
test(mbe): add integration tests for MPC sendMany
Ticket: WP-00000
1 parent 47cef83 commit 4cf70a3

File tree

2 files changed

+266
-0
lines changed

2 files changed

+266
-0
lines changed

src/__tests__/api/master/sendMany.test.ts

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
88
import { Environments, Wallet } from '@bitgo/sdk-core';
99
import { Coin } from 'bitgo';
1010
import assert from 'assert';
11+
import * as eddsa from '../../../api/master/handlers/eddsa';
12+
import * as ecdsa from '../../../api/master/handlers/ecdsa';
1113

1214
describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
1315
let agent: request.SuperAgentTest;
@@ -223,6 +225,267 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
223225
});
224226
});
225227

228+
describe('SendMany TSS EDDSA:', () => {
229+
it('should send many transactions using EDDSA TSS signing', async () => {
230+
// Mock wallet get request for TSS wallet
231+
const walletGetNock = nock(bitgoApiUrl)
232+
.get(`/api/v2/${coin}/wallet/${walletId}`)
233+
.matchHeader('any', () => true)
234+
.reply(200, {
235+
id: walletId,
236+
type: 'cold',
237+
subType: 'onPrem',
238+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
239+
multisigType: 'tss',
240+
});
241+
242+
// Mock keychain get request for TSS keychain
243+
const keychainGetNock = nock(bitgoApiUrl)
244+
.get(`/api/v2/${coin}/key/user-key-id`)
245+
.matchHeader('any', () => true)
246+
.reply(200, {
247+
id: 'user-key-id',
248+
pub: 'xpub_user',
249+
commonKeychain: 'test-common-keychain',
250+
source: 'user',
251+
type: 'tss',
252+
});
253+
254+
const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({
255+
txRequestId: 'test-tx-request-id',
256+
txHex: 'prebuilt-tx-hex',
257+
txInfo: {
258+
nP2SHInputs: 1,
259+
nSegwitInputs: 0,
260+
nOutputs: 2,
261+
},
262+
walletId,
263+
});
264+
265+
const verifyStub = sinon.stub(Coin.Btc.prototype, 'verifyTransaction').resolves(true);
266+
267+
// Mock multisigType to return 'tss'
268+
const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss');
269+
270+
// Mock getMPCAlgorithm to return 'eddsa'
271+
const getMPCAlgorithmStub = sinon
272+
.stub(Coin.Btc.prototype, 'getMPCAlgorithm')
273+
.returns('eddsa');
274+
275+
// Mock handleEddsaSigning
276+
const handleEddsaSigningStub = sinon.stub().resolves({
277+
txRequestId: 'test-tx-request-id',
278+
state: 'signed',
279+
apiVersion: 'full',
280+
transactions: [
281+
{
282+
signedTx: {
283+
id: 'test-tx-id',
284+
tx: 'signed-transaction',
285+
},
286+
},
287+
],
288+
});
289+
290+
// Import and stub the handleEddsaSigning function
291+
sinon.stub(eddsa, 'handleEddsaSigning').callsFake(handleEddsaSigningStub);
292+
293+
const response = await agent
294+
.post(`/api/${coin}/wallet/${walletId}/sendMany`)
295+
.set('Authorization', `Bearer ${accessToken}`)
296+
.send({
297+
recipients: [
298+
{
299+
address: 'tb1qtest1',
300+
amount: '100000',
301+
},
302+
{
303+
address: 'tb1qtest2',
304+
amount: '200000',
305+
},
306+
],
307+
source: 'user',
308+
pubkey: 'xpub_user',
309+
});
310+
311+
response.status.should.equal(200);
312+
response.body.should.have.property('txRequest');
313+
response.body.should.have.property('txid', 'test-tx-id');
314+
response.body.should.have.property('tx', 'signed-transaction');
315+
316+
walletGetNock.done();
317+
keychainGetNock.done();
318+
sinon.assert.calledOnce(prebuildStub);
319+
sinon.assert.calledOnce(verifyStub);
320+
sinon.assert.calledTwice(multisigTypeStub);
321+
sinon.assert.calledOnce(getMPCAlgorithmStub);
322+
sinon.assert.calledOnce(handleEddsaSigningStub);
323+
});
324+
});
325+
326+
describe('SendMany TSS ECDSA:', () => {
327+
it('should send many transactions using ECDSA TSS signing', async () => {
328+
// Mock wallet get request for TSS wallet
329+
const walletGetNock = nock(bitgoApiUrl)
330+
.get(`/api/v2/${coin}/wallet/${walletId}`)
331+
.matchHeader('any', () => true)
332+
.reply(200, {
333+
id: walletId,
334+
type: 'cold',
335+
subType: 'onPrem',
336+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
337+
multisigType: 'tss',
338+
});
339+
340+
// Mock keychain get request for TSS keychain
341+
const keychainGetNock = nock(bitgoApiUrl)
342+
.get(`/api/v2/${coin}/key/user-key-id`)
343+
.matchHeader('any', () => true)
344+
.reply(200, {
345+
id: 'user-key-id',
346+
pub: 'xpub_user',
347+
commonKeychain: 'test-common-keychain',
348+
source: 'user',
349+
type: 'tss',
350+
});
351+
352+
const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({
353+
txRequestId: 'test-tx-request-id',
354+
txHex: 'prebuilt-tx-hex',
355+
txInfo: {
356+
nP2SHInputs: 1,
357+
nSegwitInputs: 0,
358+
nOutputs: 2,
359+
},
360+
walletId,
361+
});
362+
363+
const verifyStub = sinon.stub(Coin.Btc.prototype, 'verifyTransaction').resolves(true);
364+
365+
// Mock multisigType to return 'tss'
366+
const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss');
367+
368+
// Mock getMPCAlgorithm to return 'ecdsa'
369+
const getMPCAlgorithmStub = sinon
370+
.stub(Coin.Btc.prototype, 'getMPCAlgorithm')
371+
.returns('ecdsa');
372+
373+
// Mock handleEcdsaSigning
374+
const handleEcdsaSigningStub = sinon.stub().resolves({
375+
txRequestId: 'test-tx-request-id',
376+
state: 'signed',
377+
apiVersion: 'full',
378+
transactions: [
379+
{
380+
signedTx: {
381+
id: 'test-tx-id',
382+
tx: 'signed-transaction',
383+
},
384+
},
385+
],
386+
});
387+
388+
// Import and stub the handleEcdsaSigning function
389+
sinon.stub(ecdsa, 'handleEcdsaSigning').callsFake(handleEcdsaSigningStub);
390+
391+
const response = await agent
392+
.post(`/api/${coin}/wallet/${walletId}/sendMany`)
393+
.set('Authorization', `Bearer ${accessToken}`)
394+
.send({
395+
recipients: [
396+
{
397+
address: 'tb1qtest1',
398+
amount: '100000',
399+
},
400+
{
401+
address: 'tb1qtest2',
402+
amount: '200000',
403+
},
404+
],
405+
source: 'user',
406+
pubkey: 'xpub_user',
407+
});
408+
409+
response.status.should.equal(200);
410+
response.body.should.have.property('txRequest');
411+
response.body.should.have.property('txid', 'test-tx-id');
412+
response.body.should.have.property('tx', 'signed-transaction');
413+
414+
walletGetNock.done();
415+
keychainGetNock.done();
416+
sinon.assert.calledOnce(prebuildStub);
417+
sinon.assert.calledOnce(verifyStub);
418+
sinon.assert.calledTwice(multisigTypeStub);
419+
sinon.assert.calledOnce(getMPCAlgorithmStub);
420+
sinon.assert.calledOnce(handleEcdsaSigningStub);
421+
});
422+
423+
it('should fail when backup key is used for ECDSA TSS signing', async () => {
424+
// Mock wallet get request for TSS wallet
425+
const walletGetNock = nock(bitgoApiUrl)
426+
.get(`/api/v2/${coin}/wallet/${walletId}`)
427+
.matchHeader('any', () => true)
428+
.reply(200, {
429+
id: walletId,
430+
type: 'cold',
431+
subType: 'onPrem',
432+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
433+
multisigType: 'tss',
434+
});
435+
436+
// Mock keychain get request for backup TSS keychain
437+
const keychainGetNock = nock(bitgoApiUrl)
438+
.get(`/api/v2/${coin}/key/backup-key-id`)
439+
.matchHeader('any', () => true)
440+
.reply(200, {
441+
id: 'backup-key-id',
442+
pub: 'xpub_backup',
443+
commonKeychain: 'test-common-keychain',
444+
source: 'backup',
445+
type: 'tss',
446+
});
447+
448+
const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({
449+
txRequestId: 'test-tx-request-id',
450+
txHex: 'prebuilt-tx-hex',
451+
txInfo: {
452+
nP2SHInputs: 1,
453+
nSegwitInputs: 0,
454+
nOutputs: 2,
455+
},
456+
walletId,
457+
});
458+
459+
const verifyStub = sinon.stub(Coin.Btc.prototype, 'verifyTransaction').resolves(true);
460+
461+
// Mock multisigType to return 'tss'
462+
const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss');
463+
464+
const response = await agent
465+
.post(`/api/${coin}/wallet/${walletId}/sendMany`)
466+
.set('Authorization', `Bearer ${accessToken}`)
467+
.send({
468+
recipients: [
469+
{
470+
address: 'tb1qtest1',
471+
amount: '100000',
472+
},
473+
],
474+
source: 'backup',
475+
pubkey: 'xpub_backup',
476+
});
477+
478+
response.status.should.equal(500);
479+
response.body.details.should.equal('Backup MPC signing not supported for sendMany');
480+
481+
walletGetNock.done();
482+
keychainGetNock.done();
483+
sinon.assert.calledOnce(prebuildStub);
484+
sinon.assert.calledOnce(verifyStub);
485+
sinon.assert.calledTwice(multisigTypeStub);
486+
});
487+
});
488+
226489
it('should throw error when provided pubkey does not match wallet keychain', async () => {
227490
// Mock wallet get request
228491
const walletGetNock = nock(bitgoApiUrl)

src/api/master/handlers/handleSendMany.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ async function signAndSendTxRequests(
196196
if (!signingKeychain.commonKeychain) {
197197
throw new Error(`Common keychain not found for keychain ${signingKeychain.pub || 'unknown'}`);
198198
}
199+
if (signingKeychain.source === 'backup') {
200+
throw new Error('Backup MPC signing not supported for sendMany');
201+
}
199202

200203
let signedTxRequest: TxRequest;
201204
const mpcAlgorithm = wallet.baseCoin.getMPCAlgorithm();

0 commit comments

Comments
 (0)