1+ import 'should' ;
2+ import sinon from 'sinon' ;
3+ import * as request from 'supertest' ;
4+ import nock from 'nock' ;
5+ import { app as expressApp } from '../../../masterExpressApp' ;
6+ import { AppMode , MasterExpressConfig , TlsMode } from '../../../shared/types' ;
7+ import { Environments , Wallet } from '@bitgo/sdk-core' ;
8+ import * as eddsa from '../../../api/master/handlers/eddsa' ;
9+
10+ describe ( 'POST /api/:coin/wallet/:walletId/consolidate (EDDSA MPC)' , ( ) => {
11+ let agent : request . SuperAgentTest ;
12+ const coin = 'tsol' ;
13+ const walletId = 'test-wallet-id' ;
14+ const accessToken = 'test-access-token' ;
15+ const bitgoApiUrl = Environments . test . uri ;
16+ const enclavedExpressUrl = 'https://test-enclaved-express.com' ;
17+
18+ before ( ( ) => {
19+ nock . disableNetConnect ( ) ;
20+ nock . enableNetConnect ( '127.0.0.1' ) ;
21+
22+ const config : MasterExpressConfig = {
23+ appMode : AppMode . MASTER_EXPRESS ,
24+ port : 0 ,
25+ bind : 'localhost' ,
26+ timeout : 30000 ,
27+ logFile : '' ,
28+ env : 'test' ,
29+ disableEnvCheck : true ,
30+ authVersion : 2 ,
31+ enclavedExpressUrl : enclavedExpressUrl ,
32+ enclavedExpressCert : 'test-cert' ,
33+ tlsMode : TlsMode . DISABLED ,
34+ mtlsRequestCert : false ,
35+ allowSelfSigned : true ,
36+ } ;
37+ const app = expressApp ( config ) ;
38+ agent = request . agent ( app ) ;
39+ } ) ;
40+
41+ afterEach ( ( ) => {
42+ nock . cleanAll ( ) ;
43+ sinon . restore ( ) ;
44+ } ) ;
45+
46+ it ( 'should consolidate using EDDSA MPC custom hooks' , async ( ) => {
47+ // Mock wallet get request
48+ const walletGetNock = nock ( bitgoApiUrl )
49+ . get ( `/api/v2/${ coin } /wallet/${ walletId } ` )
50+ . reply ( 200 , {
51+ id : walletId ,
52+ type : 'cold' ,
53+ subType : 'onPrem' ,
54+ keys : [ 'user-key-id' , 'backup-key-id' , 'bitgo-key-id' ] ,
55+ multisigType : 'tss' ,
56+ } ) ;
57+
58+ // Mock keychain get request
59+ const keychainGetNock = nock ( bitgoApiUrl )
60+ . get ( `/api/v2/${ coin } /key/user-key-id` )
61+ . reply ( 200 , {
62+ id : 'user-key-id' ,
63+ commonKeychain : 'pubkey' ,
64+ } ) ;
65+
66+ // Mock sendAccountConsolidations on Wallet prototype
67+ const sendConsolidationsStub = sinon
68+ . stub ( Wallet . prototype , 'sendAccountConsolidations' )
69+ . resolves ( {
70+ success : [
71+ {
72+ txid : 'mpc-txid-1' ,
73+ status : 'signed' ,
74+ } ,
75+ ] ,
76+ failure : [ ] ,
77+ } ) ;
78+
79+ // Spy on custom EDDSA hooks - these should return actual functions, not strings
80+ const mockCommitmentFn = sinon . stub ( ) . resolves ( { userToBitgoCommitment : 'commitment' } ) ;
81+ const mockRShareFn = sinon . stub ( ) . resolves ( { rShare : 'rshare' } ) ;
82+ const mockGShareFn = sinon . stub ( ) . resolves ( { gShare : 'gshare' } ) ;
83+
84+ const commitmentSpy = sinon . stub ( eddsa , 'createCustomCommitmentGenerator' ) . returns ( mockCommitmentFn ) ;
85+ const rshareSpy = sinon . stub ( eddsa , 'createCustomRShareGenerator' ) . returns ( mockRShareFn ) ;
86+ const gshareSpy = sinon . stub ( eddsa , 'createCustomGShareGenerator' ) . returns ( mockGShareFn ) ;
87+
88+ const response = await agent
89+ . post ( `/api/${ coin } /wallet/${ walletId } /consolidate` )
90+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
91+ . send ( {
92+ source : 'user' ,
93+ commonKeychain : 'pubkey' ,
94+ } ) ;
95+
96+ response . status . should . equal ( 200 ) ;
97+ response . body . should . have . property ( 'success' ) ;
98+ response . body . success . should . have . length ( 1 ) ;
99+ response . body . success [ 0 ] . should . have . property ( 'txid' , 'mpc-txid-1' ) ;
100+
101+ walletGetNock . done ( ) ;
102+ keychainGetNock . done ( ) ;
103+ sinon . assert . calledOnce ( sendConsolidationsStub ) ;
104+ sinon . assert . calledOnce ( commitmentSpy ) ;
105+ sinon . assert . calledOnce ( rshareSpy ) ;
106+ sinon . assert . calledOnce ( gshareSpy ) ;
107+ } ) ;
108+
109+ it ( 'should handle partial failures (some success, some failure)' , async ( ) => {
110+ // Mock wallet get request
111+ const walletGetNock = nock ( bitgoApiUrl )
112+ . get ( `/api/v2/${ coin } /wallet/${ walletId } ` )
113+ . reply ( 200 , {
114+ id : walletId ,
115+ type : 'cold' ,
116+ subType : 'onPrem' ,
117+ keys : [ 'user-key-id' , 'backup-key-id' , 'bitgo-key-id' ] ,
118+ multisigType : 'tss' ,
119+ } ) ;
120+
121+ // Mock keychain get request
122+ const keychainGetNock = nock ( bitgoApiUrl )
123+ . get ( `/api/v2/${ coin } /key/user-key-id` )
124+ . reply ( 200 , {
125+ id : 'user-key-id' ,
126+ commonKeychain : 'pubkey' ,
127+ } ) ;
128+
129+ // Mock partial failure response
130+ sinon
131+ . stub ( Wallet . prototype , 'sendAccountConsolidations' )
132+ . resolves ( {
133+ success : [ { txid : 'success-txid' , status : 'signed' } ] ,
134+ failure : [ { error : 'Insufficient funds' , address : '0xfailed' } ] ,
135+ } ) ;
136+
137+ // Mock EDDSA hooks
138+ const mockCommitmentFn = sinon . stub ( ) . resolves ( { userToBitgoCommitment : 'commitment' } ) ;
139+ const mockRShareFn = sinon . stub ( ) . resolves ( { rShare : 'rshare' } ) ;
140+ const mockGShareFn = sinon . stub ( ) . resolves ( { gShare : 'gshare' } ) ;
141+
142+ sinon . stub ( eddsa , 'createCustomCommitmentGenerator' ) . returns ( mockCommitmentFn ) ;
143+ sinon . stub ( eddsa , 'createCustomRShareGenerator' ) . returns ( mockRShareFn ) ;
144+ sinon . stub ( eddsa , 'createCustomGShareGenerator' ) . returns ( mockGShareFn ) ;
145+
146+ const response = await agent
147+ . post ( `/api/${ coin } /wallet/${ walletId } /consolidate` )
148+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
149+ . send ( {
150+ source : 'user' ,
151+ commonKeychain : 'pubkey' ,
152+ consolidateAddresses : [ '0x1234567890abcdef' , '0xfedcba0987654321' ] ,
153+ } ) ;
154+
155+ response . status . should . equal ( 500 ) ;
156+ response . body . should . have . property ( 'error' , 'Internal Server Error' ) ;
157+ response . body . should . have
158+ . property ( 'details' )
159+ . which . match ( / C o n s o l i d a t i o n s f a i l e d : 1 a n d s u c c e e d e d : 1 / ) ;
160+
161+ walletGetNock . done ( ) ;
162+ keychainGetNock . done ( ) ;
163+ } ) ;
164+
165+ it ( 'should handle total failures (all failed)' , async ( ) => {
166+ // Mock wallet get request
167+ const walletGetNock = nock ( bitgoApiUrl )
168+ . get ( `/api/v2/${ coin } /wallet/${ walletId } ` )
169+ . reply ( 200 , {
170+ id : walletId ,
171+ type : 'cold' ,
172+ subType : 'onPrem' ,
173+ keys : [ 'user-key-id' , 'backup-key-id' , 'bitgo-key-id' ] ,
174+ multisigType : 'tss' ,
175+ } ) ;
176+
177+ // Mock keychain get request
178+ const keychainGetNock = nock ( bitgoApiUrl )
179+ . get ( `/api/v2/${ coin } /key/user-key-id` )
180+ . reply ( 200 , {
181+ id : 'user-key-id' ,
182+ commonKeychain : 'pubkey' ,
183+ } ) ;
184+
185+ // Mock total failure response
186+ sinon
187+ . stub ( Wallet . prototype , 'sendAccountConsolidations' )
188+ . resolves ( {
189+ success : [ ] ,
190+ failure : [
191+ { error : 'Insufficient funds' , address : '0xfailed1' } ,
192+ { error : 'Invalid address' , address : '0xfailed2' } ,
193+ ] ,
194+ } ) ;
195+
196+ // Mock EDDSA hooks
197+ const mockCommitmentFn = sinon . stub ( ) . resolves ( { userToBitgoCommitment : 'commitment' } ) ;
198+ const mockRShareFn = sinon . stub ( ) . resolves ( { rShare : 'rshare' } ) ;
199+ const mockGShareFn = sinon . stub ( ) . resolves ( { gShare : 'gshare' } ) ;
200+
201+ sinon . stub ( eddsa , 'createCustomCommitmentGenerator' ) . returns ( mockCommitmentFn ) ;
202+ sinon . stub ( eddsa , 'createCustomRShareGenerator' ) . returns ( mockRShareFn ) ;
203+ sinon . stub ( eddsa , 'createCustomGShareGenerator' ) . returns ( mockGShareFn ) ;
204+
205+ const response = await agent
206+ . post ( `/api/${ coin } /wallet/${ walletId } /consolidate` )
207+ . set ( 'Authorization' , `Bearer ${ accessToken } ` )
208+ . send ( {
209+ source : 'user' ,
210+ commonKeychain : 'pubkey' ,
211+ } ) ;
212+
213+ response . status . should . equal ( 500 ) ;
214+ response . body . should . have . property ( 'error' ) ;
215+ response . body . should . have . property ( 'details' ) . which . match ( / A l l c o n s o l i d a t i o n s f a i l e d / ) ;
216+
217+ walletGetNock . done ( ) ;
218+ keychainGetNock . done ( ) ;
219+ } ) ;
220+ } ) ;
0 commit comments