@@ -6,9 +6,10 @@ import { app as expressApp } from '../../../masterExpressApp';
66import { AppMode , MasterExpressConfig , TlsMode } from '../../../shared/types' ;
77import { Trx } from '@bitgo/sdk-coin-trx' ;
88import { Sol } from '@bitgo/sdk-coin-sol' ;
9+ import { Sui } from '@bitgo/sdk-coin-sui' ;
910import { EnclavedExpressClient } from '../../../api/master/clients/enclavedExpressClient' ;
1011
11- describe ( 'POST /api/:coin/wallet/recoveryConsolidations ' , ( ) => {
12+ describe ( 'POST /api/:coin/wallet/recoveryconsolidations ' , ( ) => {
1213 let agent : request . SuperAgentTest ;
1314 const enclavedExpressUrl = 'http://enclaved.invalid' ;
1415 const accessToken = 'test-token' ;
@@ -40,70 +41,249 @@ describe('POST /api/:coin/wallet/recoveryConsolidations', () => {
4041 sinon . restore ( ) ;
4142 } ) ;
4243
43- it ( 'should handle TRON consolidation recovery' , async ( ) => {
44- const mockTransactions = [ { txHex : 'unsigned-tx-1' , serializedTx : 'serialized-unsigned-tx-1' } ] ;
44+ describe ( 'Non-MPC Wallets (multisigType: onchain)' , ( ) => {
45+ it ( 'should handle TRON consolidation recovery for onchain wallet' , async ( ) => {
46+ const mockTransactions = [
47+ { txHex : 'unsigned-tx-1' , serializedTx : 'serialized-unsigned-tx-1' } ,
48+ { txHex : 'unsigned-tx-2' , serializedTx : 'serialized-unsigned-tx-2' }
49+ ] ;
4550
46- const recoverConsolidationsStub = sinon . stub ( Trx . prototype , 'recoverConsolidations' ) . resolves ( {
47- transactions : mockTransactions ,
51+ const recoverConsolidationsStub = sinon . stub ( Trx . prototype , 'recoverConsolidations' ) . resolves ( {
52+ transactions : mockTransactions ,
53+ } ) ;
54+
55+ const recoveryMultisigStub = sinon
56+ . stub ( EnclavedExpressClient . prototype , 'recoveryMultisig' )
57+ . resolves ( { txHex : 'signed-tx' } ) ;
58+
59+ const response = await agent
60+ . post ( `/api/trx/wallet/recoveryconsolidations` )
61+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
62+ . send ( {
63+ multisigType : 'onchain' ,
64+ userPub : 'user-xpub' ,
65+ backupPub : 'backup-xpub' ,
66+ bitgoPub : 'bitgo-xpub' ,
67+ tokenContractAddress : 'tron-token' ,
68+ startingScanIndex : 1 ,
69+ endingScanIndex : 3 ,
70+ } ) ;
71+
72+ response . status . should . equal ( 200 ) ;
73+ response . body . should . have . property ( 'signedTxs' ) ;
74+ response . body . signedTxs . should . have . length ( 2 ) ;
75+
76+ sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
77+ sinon . assert . calledTwice ( recoveryMultisigStub ) ;
78+
79+ const callArgs = recoverConsolidationsStub . firstCall . args [ 0 ] ;
80+ callArgs . tokenContractAddress ! . should . equal ( 'tron-token' ) ;
81+ callArgs . userKey ! . should . equal ( 'user-xpub' ) ;
82+ callArgs . backupKey ! . should . equal ( 'backup-xpub' ) ;
83+ callArgs . bitgoKey . should . equal ( 'bitgo-xpub' ) ;
4884 } ) ;
4985
50- const recoveryMultisigStub = sinon
51- . stub ( EnclavedExpressClient . prototype , 'recoveryMultisig' )
52- . resolves ( { txHex : 'signed-tx' } ) ;
53-
54- const response = await agent
55- . post ( `/api/trx/wallet/recoveryConsolidations` )
56- . set ( 'Authorization' , `Bearer ${ accessToken } ` )
57- . send ( {
58- userPub : 'user-xpub' ,
59- backupPub : 'backup-xpub' ,
60- bitgoKey : 'bitgo-xpub' ,
61- tokenContractAddress : 'tron-token' ,
62- startingScanIndex : 1 ,
63- endingScanIndex : 3 ,
86+ it ( 'should handle Solana consolidation recovery for onchain wallet' , async ( ) => {
87+ const mockTransactions = [ { txHex : 'unsigned-tx-1' , serializedTx : 'serialized-unsigned-tx-1' } ] ;
88+
89+ const recoverConsolidationsStub = sinon . stub ( Sol . prototype , 'recoverConsolidations' ) . resolves ( {
90+ transactions : mockTransactions ,
6491 } ) ;
6592
66- response . status . should . equal ( 200 ) ;
67- response . body . should . have . property ( 'signedTxs' ) ;
68- sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
69- sinon . assert . calledOnce ( recoveryMultisigStub ) ;
70- const callArgs = recoverConsolidationsStub . firstCall . args [ 0 ] ;
71- callArgs . tokenContractAddress ! . should . equal ( 'tron-token' ) ;
72- callArgs . userKey . should . equal ( 'user-xpub' ) ;
73- callArgs . backupKey . should . equal ( 'backup-xpub' ) ;
74- callArgs . bitgoKey . should . equal ( 'bitgo-xpub' ) ;
93+ const recoveryMultisigStub = sinon
94+ . stub ( EnclavedExpressClient . prototype , 'recoveryMultisig' )
95+ . resolves ( { txHex : 'signed-tx' } ) ;
96+
97+ const response = await agent
98+ . post ( `/api/sol/wallet/recoveryconsolidations` )
99+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
100+ . send ( {
101+ multisigType : 'onchain' ,
102+ userPub : 'user-xpub' ,
103+ backupPub : 'backup-xpub' ,
104+ bitgoPub : 'bitgo-xpub' ,
105+ durableNonces : {
106+ publicKeys : [ 'sol-pubkey-1' , 'sol-pubkey-2' ] ,
107+ secretKey : 'sol-secret' ,
108+ } ,
109+ } ) ;
110+
111+ response . status . should . equal ( 200 ) ;
112+ response . body . should . have . property ( 'signedTxs' ) ;
113+ sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
114+ sinon . assert . calledOnce ( recoveryMultisigStub ) ;
115+
116+ const callArgs = recoverConsolidationsStub . firstCall . args [ 0 ] ;
117+ callArgs . durableNonces . should . have . property ( 'publicKeys' ) . which . is . an . Array ( ) ;
118+ callArgs . durableNonces . should . have . property ( 'secretKey' , 'sol-secret' ) ;
119+ callArgs . userKey ! . should . equal ( 'user-xpub' ) ;
120+ callArgs . backupKey ! . should . equal ( 'backup-xpub' ) ;
121+ callArgs . bitgoKey . should . equal ( 'bitgo-xpub' ) ;
122+ } ) ;
75123 } ) ;
76124
77- it ( 'should handle Solana consolidation recovery' , async ( ) => {
78- const mockTransactions = [ { txHex : 'unsigned-tx-1' , serializedTx : 'serialized-unsigned-tx-1' } ] ;
125+ describe ( 'MPC Wallets (multisigType: tss)' , ( ) => {
126+ it ( 'should handle MPC consolidation recovery with commonKeychain' , async ( ) => {
127+ const mockTxRequests = [
128+ {
129+ walletCoin : 'tsui' ,
130+ transactions : [ {
131+ unsignedTx : {
132+ txHex : 'unsigned-mpc-tx-1' ,
133+ serializedTx : 'serialized-unsigned-mpc-tx-1'
134+ } ,
135+ signatureShares : [ ]
136+ } ]
137+ }
138+ ] as any ;
79139
80- const recoverConsolidationsStub = sinon . stub ( Sol . prototype , 'recoverConsolidations' ) . resolves ( {
81- transactions : mockTransactions ,
140+ const recoverConsolidationsStub = sinon . stub ( Sui . prototype , 'recoverConsolidations' ) . resolves ( {
141+ txRequests : mockTxRequests ,
142+ } ) ;
143+
144+ const recoveryMPCStub = sinon
145+ . stub ( EnclavedExpressClient . prototype , 'recoveryMPC' )
146+ . resolves ( { txHex : 'signed-mpc-tx' } ) ;
147+
148+ const response = await agent
149+ . post ( `/api/tsui/wallet/recoveryconsolidations` )
150+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
151+ . send ( {
152+ multisigType : 'tss' ,
153+ commonKeychain : 'common-keychain-key' ,
154+ apiKey : 'test-api-key' ,
155+ startingScanIndex : 0 ,
156+ endingScanIndex : 5 ,
157+ } ) ;
158+
159+ response . status . should . equal ( 200 ) ;
160+ response . body . should . have . property ( 'signedTxs' ) ;
161+ response . body . signedTxs . should . have . length ( 1 ) ;
162+
163+ sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
164+ sinon . assert . calledOnce ( recoveryMPCStub ) ;
165+
166+ const callArgs = recoverConsolidationsStub . firstCall . args [ 0 ] ;
167+ callArgs . userKey ! . should . equal ( '' ) ;
168+ callArgs . backupKey ! . should . equal ( '' ) ;
169+ callArgs . bitgoKey . should . equal ( 'common-keychain-key' ) ;
170+
171+ const mpcCallArgs = recoveryMPCStub . firstCall . args [ 0 ] ;
172+ mpcCallArgs . userPub . should . equal ( 'common-keychain-key' ) ;
173+ mpcCallArgs . backupPub . should . equal ( 'common-keychain-key' ) ;
174+ mpcCallArgs . apiKey . should . equal ( 'test-api-key' ) ;
82175 } ) ;
83176
84- const recoveryMultisigStub = sinon
85- . stub ( EnclavedExpressClient . prototype , 'recoveryMultisig' )
86- . resolves ( { txHex : 'signed-tx' } ) ;
87-
88- const response = await agent
89- . post ( `/api/sol/wallet/recoveryConsolidations` )
90- . set ( 'Authorization' , `Bearer ${ accessToken } ` )
91- . send ( {
92- userPub : 'user-xpub' ,
93- backupPub : 'backup-xpub' ,
94- bitgoKey : 'bitgo-xpub' ,
95- durableNonces : {
96- publicKeys : [ 'sol-pubkey-1' , 'sol-pubkey-2' ] ,
97- secretKey : 'sol-secret' ,
98- } ,
177+ it ( 'should handle SOL MPC consolidation recovery' , async ( ) => {
178+ const mockTransactions = [ { txHex : 'unsigned-mpc-tx-1' , serializedTx : 'serialized-mpc-tx-1' } ] ;
179+
180+ const recoverConsolidationsStub = sinon . stub ( Sol . prototype , 'recoverConsolidations' ) . resolves ( {
181+ transactions : mockTransactions ,
99182 } ) ;
100183
101- response . status . should . equal ( 200 ) ;
102- response . body . should . have . property ( 'signedTxs' ) ;
103- sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
104- sinon . assert . calledOnce ( recoveryMultisigStub ) ;
105- const callArgs = recoverConsolidationsStub . firstCall . args [ 0 ] ;
106- callArgs . durableNonces . should . have . property ( 'publicKeys' ) . which . is . an . Array ( ) ;
107- callArgs . durableNonces . should . have . property ( 'secretKey' , 'sol-secret' ) ;
184+ const recoveryMPCStub = sinon
185+ . stub ( EnclavedExpressClient . prototype , 'recoveryMPC' )
186+ . resolves ( { txHex : 'signed-mpc-tx' } ) ;
187+
188+ const response = await agent
189+ . post ( `/api/sol/wallet/recoveryconsolidations` )
190+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
191+ . send ( {
192+ multisigType : 'tss' ,
193+ commonKeychain : 'sol-common-key' ,
194+ apiKey : 'sol-api-key' ,
195+ durableNonces : {
196+ publicKeys : [ 'sol-pubkey-1' ] ,
197+ secretKey : 'sol-secret' ,
198+ } ,
199+ } ) ;
200+
201+ response . status . should . equal ( 200 ) ;
202+ response . body . should . have . property ( 'signedTxs' ) ;
203+ sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
204+ sinon . assert . calledOnce ( recoveryMPCStub ) ;
205+
206+ const mpcCallArgs = recoveryMPCStub . firstCall . args [ 0 ] ;
207+ mpcCallArgs . userPub . should . equal ( 'sol-common-key' ) ;
208+ mpcCallArgs . backupPub . should . equal ( 'sol-common-key' ) ;
209+ mpcCallArgs . apiKey . should . equal ( 'sol-api-key' ) ;
210+ } ) ;
211+ } ) ;
212+
213+ describe ( 'Error Cases' , ( ) => {
214+ it ( 'should throw error when commonKeychain is missing for MPC wallet' , async ( ) => {
215+ const response = await agent
216+ . post ( `/api/tsui/wallet/recoveryconsolidations` )
217+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
218+ . send ( {
219+ multisigType : 'tss' ,
220+ // Missing commonKeychain
221+ apiKey : 'test-api-key' ,
222+ } ) ;
223+
224+ response . status . should . equal ( 500 ) ;
225+ response . body . should . have . property ( 'error' ) ;
226+ response . body . should . have . property ( 'details' ) . which . match ( / M i s s i n g r e q u i r e d k e y : c o m m o n K e y c h a i n / ) ;
227+ } ) ;
228+
229+ it ( 'should throw error when required keys are missing for onchain wallet' , async ( ) => {
230+ const response = await agent
231+ . post ( `/api/trx/wallet/recoveryconsolidations` )
232+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
233+ . send ( {
234+ multisigType : 'onchain' ,
235+ userPub : 'user-xpub' ,
236+ // Missing backupPub and bitgoPub
237+ } ) ;
238+
239+ response . status . should . equal ( 500 ) ;
240+ response . body . should . have . property ( 'error' ) ;
241+ response . body . should . have . property ( 'details' ) . which . match ( / M i s s i n g r e q u i r e d k e y s / ) ;
242+ } ) ;
243+
244+ it ( 'should handle empty recovery consolidations result' , async ( ) => {
245+ const recoverConsolidationsStub = sinon . stub ( Trx . prototype , 'recoverConsolidations' ) . resolves ( {
246+ transactions : [ ] , // Empty result
247+ } as any ) ;
248+
249+ const response = await agent
250+ . post ( `/api/trx/wallet/recoveryconsolidations` )
251+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
252+ . send ( {
253+ multisigType : 'onchain' ,
254+ userPub : 'user-xpub' ,
255+ backupPub : 'backup-xpub' ,
256+ bitgoPub : 'bitgo-xpub' ,
257+ } ) ;
258+
259+ response . status . should . equal ( 200 ) ;
260+ response . body . should . have . property ( 'signedTxs' ) ;
261+ response . body . signedTxs . should . have . length ( 0 ) ; // Empty array
262+
263+ sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
264+ } ) ;
265+
266+ it ( 'should throw error when recoverConsolidations returns unexpected result structure' , async ( ) => {
267+ const recoverConsolidationsStub = sinon . stub ( Trx . prototype , 'recoverConsolidations' ) . resolves ( {
268+ // Missing both transactions and txRequests properties
269+ someOtherProperty : 'value'
270+ } as any ) ;
271+
272+ const response = await agent
273+ . post ( `/api/trx/wallet/recoveryconsolidations` )
274+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
275+ . send ( {
276+ multisigType : 'onchain' ,
277+ userPub : 'user-xpub' ,
278+ backupPub : 'backup-xpub' ,
279+ bitgoPub : 'bitgo-xpub' ,
280+ } ) ;
281+
282+ response . status . should . equal ( 500 ) ;
283+ response . body . should . have . property ( 'error' ) ;
284+ response . body . should . have . property ( 'details' ) . which . match ( / r e c o v e r C o n s o l i d a t i o n s d i d n o t r e t u r n e x p e c t e d t r a n s a c t i o n s / ) ;
285+
286+ sinon . assert . calledOnce ( recoverConsolidationsStub ) ;
287+ } ) ;
108288 } ) ;
109289} ) ;
0 commit comments