Skip to content

Commit b5130b6

Browse files
feat(mbe): integration test for eddsa consolidation
1 parent 2bb7ce9 commit b5130b6

File tree

2 files changed

+220
-3
lines changed

2 files changed

+220
-3
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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(/Consolidations failed: 1 and succeeded: 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(/All consolidations failed/);
216+
217+
walletGetNock.done();
218+
keychainGetNock.done();
219+
});
220+
});

src/api/master/handlers/handleConsolidate.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export async function handleConsolidate(
5353
// Always force apiVersion to 'full' for TSS/MPC
5454
consolidationParams.apiVersion = 'full';
5555

56-
// EDDSA (e.g., Solana)
5756
if (baseCoin.getMPCAlgorithm() === MPCType.EDDSA) {
5857
consolidationParams.customCommitmentGeneratingFunction = createCustomCommitmentGenerator(
5958
bitgo,
@@ -73,9 +72,7 @@ export async function handleConsolidate(
7372
signingKeychain.commonKeychain!,
7473
);
7574
}
76-
// ECDSA (future-proof, not needed for Solana)
7775
else if (baseCoin.getMPCAlgorithm() === MPCType.ECDSA) {
78-
// Add ECDSA custom signing hooks here if needed, following SDK pattern
7976
throw new Error('ECDSA MPC consolidations not yet implemented');
8077
}
8178
} else {

0 commit comments

Comments
 (0)