@@ -8,6 +8,8 @@ import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
88import { Environments , Wallet } from '@bitgo/sdk-core' ;
99import { Coin } from 'bitgo' ;
1010import assert from 'assert' ;
11+ import * as eddsa from '../../../api/master/handlers/eddsa' ;
12+ import * as ecdsa from '../../../api/master/handlers/ecdsa' ;
1113
1214describe ( '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 )
0 commit comments