Skip to content

Commit 82e93f3

Browse files
committed
feat(awm, mbe): added test cases for eddsa recovery
Ticket: WP-5337
1 parent 9beed7b commit 82e93f3

File tree

2 files changed

+182
-112
lines changed

2 files changed

+182
-112
lines changed
Lines changed: 108 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,22 @@
1-
import { AppMode, AdvancedWalletManagerConfig, TlsMode } from '../../../initConfig';
2-
import { app as expressApp } from '../../../advancedWalletManagerApp';
3-
4-
import express from 'express';
5-
import nock from 'nock';
61
import 'should';
72
import * as request from 'supertest';
8-
import { DklsTypes, DklsUtils } from '@bitgo-beta/sdk-lib-mpc';
3+
import nock from 'nock';
4+
import { app as expressApp } from '../../../advancedWalletManagerApp';
5+
import { AdvancedWalletManagerConfig, AppMode, TlsMode } from '../../../shared/types';
96

10-
describe('recoveryMpc', async () => {
11-
let cfg: AdvancedWalletManagerConfig;
12-
let app: express.Application;
7+
describe('recoveryMpc', () => {
138
let agent: request.SuperAgentTest;
149

1510
// test config
1611
const kmsUrl = 'http://kms.invalid';
17-
const eddsaCoin = 'tsol';
18-
const nonSol = 'tnear';
12+
const sol = 'tsol';
1913
const accessToken = 'test-token';
2014

21-
// sinon stubs
22-
// let configStub: sinon.SinonStub;
23-
24-
// kms nocks setup
25-
const [userShare, backupShare] = await DklsUtils.generateDKGKeyShares();
26-
const userKeyShare = userShare.getKeyShare().toString('base64');
27-
const backupKeyShare = backupShare.getKeyShare().toString('base64');
28-
const commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());
29-
30-
const mockKmsUserResponse = {
31-
prv: JSON.stringify(userKeyShare),
32-
pub: commonKeychain,
33-
source: 'user',
34-
type: 'tss',
35-
};
36-
37-
const mockKmsBackupResponse = {
38-
prv: JSON.stringify(backupKeyShare),
39-
pub: commonKeychain,
40-
source: 'backup',
41-
type: 'tss',
42-
};
43-
const input = {
44-
txHex:
45-
'',
46-
pub: commonKeychain,
47-
};
48-
4915
before(async () => {
50-
// nock config
5116
nock.disableNetConnect();
5217
nock.enableNetConnect('127.0.0.1');
5318

54-
// app config
55-
cfg = {
19+
const config: AdvancedWalletManagerConfig = {
5620
appMode: AppMode.ADVANCED_WALLET_MANAGER,
5721
port: 0, // Let OS assign a free port
5822
bind: 'localhost',
@@ -64,36 +28,114 @@ describe('recoveryMpc', async () => {
6428
recoveryMode: true,
6529
};
6630

67-
// app setup
68-
app = expressApp(cfg);
31+
const app = expressApp(config);
6932
agent = request.agent(app);
7033
});
7134

7235
afterEach(() => {
7336
nock.cleanAll();
7437
});
7538

76-
// happy path test
77-
it('should be sign a MPC Recovery', async () => {
78-
// nocks for KMS responses
79-
const userKmsNock = nock(kmsUrl)
80-
.get(`/key/${input.pub}`)
81-
.query({ source: 'user', useLocalEncipherment: false })
82-
.reply(200, mockKmsUserResponse)
83-
.persist();
84-
const backupKmsNock = nock(kmsUrl)
85-
.get(`/key/${input.pub}`)
86-
.query({ source: 'backup', useLocalEncipherment: false })
87-
.reply(200, mockKmsBackupResponse)
88-
.persist();
89-
90-
const eddsaSignatureResponse = await agent
91-
.post(`/api/${eddsaCoin}/mpc/recovery`)
92-
.set('Authorization', `Bearer ${accessToken}`)
93-
.send(input);
94-
95-
eddsaSignatureResponse.status.should.equal(200);
96-
eddsaSignatureResponse.body.should.have.property('txHex');
97-
eddsaSignatureResponse.body.txHex.should.equal(input.txHex);
39+
after(() => {
40+
nock.enableNetConnect();
41+
});
42+
43+
describe('ECDSA MPC recovery', () => {
44+
it('should successfully generate MPC solana transactions', async () => {
45+
const mockKmsUserResponse = {
46+
prv: '{"uShare":{"i":1,"t":2,"n":3,"y":"85aa6462d927329418f70f6d0863cf6cf33e7da2934f935e5927f1b13062d779","seed":"2f55c80fd6b5583dcde8037b2ee461d2e7d445a4d3e7a9b2a0d3d00b5f534169","chaincode":"66e80f2bf41a5706608352d51ceb07a5aa1729cab6c6993c124d5731546ed9a1"},"bitgoYShare":{"i":1,"j":3,"y":"483e53b72de3aa893df698d0b20b20777fb3d2716cc8483a9e9797174fd52b16","v":"e70696459e46434a2a12cc988e3ae714a61fe96da8a6764d058b849cab50d6dc","u":"49abf8144d265a77cf6d098eff784d6ce56ec77a182f6b39f47d5d8e28f2a802","chaincode":"797348468202f1d7fede0a7851f80162b02e7da306e65075dd864b6789b9bc5b"},"backupYShare":{"i":1,"j":2,"y":"249a9798d0064a989a16cd8f479edf09ffaee73f4175d2ac555ba90ff41b89da","v":"98e31d2b643e40060ba344c6a41fc096ea7e39a1ae879f65e4af645870e90ee0","u":"ac047b1bceab2e1a42d97ab540b39176e545d9c0af4a192aee8e1dae91a4240b","chaincode":"585bdc05c8f84802cbe7b9a1a07d4aa9c5fede93597a622854e9bad83a2d5b78"}}',
47+
pub: 'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174',
48+
source: 'user',
49+
type: 'tss',
50+
};
51+
52+
const mockKmsBackupResponse = {
53+
prv: '{"uShare":{"i":2,"t":2,"n":3,"y":"249a9798d0064a989a16cd8f479edf09ffaee73f4175d2ac555ba90ff41b89da","seed":"abab5be2b32d07cf39b2a162af0f78bad8325b2fbdc89d14fd8b4e5767b74097","chaincode":"585bdc05c8f84802cbe7b9a1a07d4aa9c5fede93597a622854e9bad83a2d5b78"},"bitgoYShare":{"i":2,"j":3,"y":"483e53b72de3aa893df698d0b20b20777fb3d2716cc8483a9e9797174fd52b16","v":"e70696459e46434a2a12cc988e3ae714a61fe96da8a6764d058b849cab50d6dc","u":"eb54da28da3da22eb3d61797a02a96264be8940b7115aefbb90b9dd044db7f06","chaincode":"797348468202f1d7fede0a7851f80162b02e7da306e65075dd864b6789b9bc5b"},"userYShare":{"i":2,"j":1,"y":"85aa6462d927329418f70f6d0863cf6cf33e7da2934f935e5927f1b13062d779","v":"76cfdcbf0f769f21c64e0faf0072ebccbcc3aaa844522336af27f8e50ed7ca5f","u":"6ce814af82683423c8d8befd13f6eeeb0cd3f7274d1ebfdd5807fd2e4eaadb08","chaincode":"66e80f2bf41a5706608352d51ceb07a5aa1729cab6c6993c124d5731546ed9a1"}}',
54+
pub: 'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174',
55+
source: 'backup',
56+
type: 'tss',
57+
};
58+
59+
nock(kmsUrl)
60+
.get(`/key/${mockKmsUserResponse.pub}`)
61+
.query({ source: 'user', useLocalEncipherment: false })
62+
.reply(200, mockKmsUserResponse)
63+
.persist();
64+
65+
nock(kmsUrl)
66+
.get(`/key/${mockKmsBackupResponse.pub}`)
67+
.query({ source: 'backup', useLocalEncipherment: false })
68+
.reply(200, mockKmsBackupResponse)
69+
.persist();
70+
71+
const input = {
72+
commonKeychain:
73+
'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174',
74+
unsignedSweepPrebuildTx: {
75+
txRequests: [
76+
{
77+
unsignedTx: '',
78+
signableHex:
79+
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECvoOqYkvCPusjYyhX4GdUtzSeVIcx6GkwdpSk8SkU0/cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQtFGO2YBsrubq15CKqJLwXG3VEF1aEs36Rao6EaJDLAQECAAAMAgAAALhJxgAAAAAA',
80+
derivationPath: 'm/0',
81+
},
82+
],
83+
},
84+
};
85+
86+
const eddsaSignatureResponse = await agent
87+
.post(`/api/${sol}/mpc/recovery`)
88+
.set('Authorization', `Bearer ${accessToken}`)
89+
.send(input);
90+
91+
eddsaSignatureResponse.status.should.equal(200);
92+
eddsaSignatureResponse.body.should.have.property('txHex');
93+
94+
nock.cleanAll();
95+
});
96+
97+
it('should throw 500 Internal Server Error if KMS cannot find user or backup keys', async () => {
98+
const commonKeychain = 'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174';
99+
const mockKmsUserResponse = {};
100+
const mockKmsBackupResponse = {};
101+
102+
nock(kmsUrl)
103+
.get(`/key/${commonKeychain}`)
104+
.query({ source: 'user', useLocalEncipherment: false })
105+
.reply(200, mockKmsUserResponse)
106+
.persist();
107+
108+
nock(kmsUrl)
109+
.get(`/key/${commonKeychain}`)
110+
.query({ source: 'backup', useLocalEncipherment: false })
111+
.reply(200, mockKmsBackupResponse)
112+
.persist();
113+
114+
const input = {
115+
commonKeychain:
116+
'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174',
117+
unsignedSweepPrebuildTx: {
118+
txRequests: [
119+
{
120+
unsignedTx: '',
121+
signableHex:
122+
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECvoOqYkvCPusjYyhX4GdUtzSeVIcx6GkwdpSk8SkU0/cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQtFGO2YBsrubq15CKqJLwXG3VEF1aEs36Rao6EaJDLAQECAAAMAgAAALhJxgAAAAAA',
123+
derivationPath: 'm/0',
124+
},
125+
],
126+
},
127+
};
128+
129+
const eddsaSignatureResponse = await agent
130+
.post(`/api/${sol}/mpc/recovery`)
131+
.set('Authorization', `Bearer ${accessToken}`)
132+
.send(input);
133+
134+
eddsaSignatureResponse.status.should.equal(500);
135+
eddsaSignatureResponse.body.should.have.property('error');
136+
eddsaSignatureResponse.body.error.should.equal('Failed to retrieve key from KMS');
137+
138+
nock.cleanAll();
139+
});
98140
});
99141
});

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

Lines changed: 74 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ import { BitGoRequest } from '../../../types/request';
1010
import { BitGoAPI } from '@bitgo-beta/sdk-api';
1111
import { AdvancedWalletManagerClient } from '../../../api/master/clients/advancedWalletManagerClient';
1212
import { CoinFamily } from '@bitgo-beta/statics';
13+
import coinFactory from '../../../shared/coinFactory';
1314

1415
describe('Recovery Tests', () => {
1516
let agent: request.SuperAgentTest;
16-
let mockBitgo: BitGoAPI;
17-
let coinStub: sinon.SinonStub;
1817
const advancedWalletManagerUrl = 'http://advancedwalletmanager.invalid';
1918
const accessToken = 'test-token';
2019
const config: MasterExpressConfig = {
@@ -37,43 +36,11 @@ describe('Recovery Tests', () => {
3736
nock.disableNetConnect();
3837
nock.enableNetConnect('127.0.0.1');
3938

40-
// Create mock BitGo instance with base functionality
41-
mockBitgo = {
42-
coin: sinon.stub(),
43-
_coinFactory: {},
44-
_useAms: false,
45-
initCoinFactory: sinon.stub(),
46-
registerToken: sinon.stub(),
47-
getValidate: sinon.stub(),
48-
validateAddress: sinon.stub(),
49-
verifyAddress: sinon.stub(),
50-
verifyPassword: sinon.stub(),
51-
encrypt: sinon.stub(),
52-
decrypt: sinon.stub(),
53-
lock: sinon.stub(),
54-
unlock: sinon.stub(),
55-
getSharingKey: sinon.stub(),
56-
ping: sinon.stub(),
57-
authenticate: sinon.stub(),
58-
authenticateWithAccessToken: sinon.stub(),
59-
logout: sinon.stub(),
60-
me: sinon.stub(),
61-
session: sinon.stub(),
62-
getUser: sinon.stub(),
63-
users: sinon.stub(),
64-
getWallet: sinon.stub(),
65-
getWallets: sinon.stub(),
66-
addWallet: sinon.stub(),
67-
removeWallet: sinon.stub(),
68-
getAsUser: sinon.stub(),
69-
register: sinon.stub(),
70-
} as unknown as BitGoAPI;
71-
72-
coinStub = mockBitgo.coin as sinon.SinonStub;
39+
const bitgo = new BitGoAPI({ env: 'test' });
7340

7441
// Setup middleware stubs before creating app
7542
sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => {
76-
(req as BitGoRequest<MasterExpressConfig>).bitgo = mockBitgo;
43+
(req as BitGoRequest<MasterExpressConfig>).bitgo = bitgo;
7744
(req as BitGoRequest<MasterExpressConfig>).config = config;
7845
next();
7946
});
@@ -123,7 +90,11 @@ describe('Recovery Tests', () => {
12390
isValidPub: mockIsValidPub,
12491
getFamily: sinon.stub().returns(CoinFamily.BTC),
12592
};
126-
coinStub.withArgs(coin).returns(mockCoin);
93+
// coinStub.withArgs(coin).returns(mockCoin);
94+
sinon
95+
.stub(coinFactory, 'getCoin')
96+
.withArgs(coin)
97+
.returns(mockCoin as any);
12798

12899
// Setup coin middleware
129100
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
@@ -184,7 +155,7 @@ describe('Recovery Tests', () => {
184155
);
185156

186157
// Verify SDK coin method calls
187-
coinStub.calledWith(coin).should.be.true();
158+
// coinStub.calledWith(coin).should.be.true();
188159
mockIsValidPub.calledWith(userPub).should.be.true();
189160
mockIsValidPub.calledWith(backupPub).should.be.true();
190161
mockRecover
@@ -315,13 +286,6 @@ describe('Recovery Tests', () => {
315286
const ethCoinId = 'hteth';
316287

317288
beforeEach(() => {
318-
ethCoin = {
319-
recover: sinon.stub().resolves({ txHex: 'eth-tx-hex' }),
320-
isValidPub: sinon.stub().returns(true),
321-
getFamily: sinon.stub().returns(CoinFamily.ETH),
322-
};
323-
coinStub.withArgs(ethCoinId).returns(ethCoin);
324-
325289
// Setup coin middleware for ETH coin
326290
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
327291
(req as BitGoRequest<MasterExpressConfig>).params = { coin: ethCoinId };
@@ -413,14 +377,14 @@ describe('Recovery Tests', () => {
413377
// Setup mocks for Solana
414378
let solCoin: any;
415379
const solCoinId = 'tsol';
380+
const solExplorerUrl = 'https://api.devnet.solana.com';
416381

417382
beforeEach(() => {
418383
solCoin = {
419384
isValidPub: sinon.stub().returns(true),
420385
getFamily: sinon.stub().returns(CoinFamily.SOL),
421386
getMPCAlgorithm: sinon.stub().returns('eddsa'),
422387
};
423-
coinStub.withArgs(solCoinId).returns(solCoin);
424388

425389
// Setup coin middleware for Solana coin
426390
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
@@ -434,6 +398,70 @@ describe('Recovery Tests', () => {
434398
});
435399
});
436400

401+
afterEach(() => {
402+
nock.cleanAll();
403+
});
404+
405+
it('should sign a solana recovery successfully', async () => {
406+
const solAccountBalanceNock = nock(solExplorerUrl)
407+
.post('/')
408+
.matchHeader('any', () => true)
409+
.reply(200, {
410+
result: {
411+
value: 1000000000,
412+
},
413+
});
414+
415+
const solBlockHashNock = nock(solExplorerUrl)
416+
.post('/')
417+
.matchHeader('any', () => true)
418+
.reply(200, {
419+
result: {
420+
value: {
421+
blockhash: 'FvGuZFQqWtjDCgpPgA2CJ9WgDKc7i1HioJcn9j5PX8xu',
422+
},
423+
},
424+
});
425+
426+
const solFeeNock = nock(solExplorerUrl)
427+
.post('/')
428+
.matchHeader('any', () => true)
429+
.reply(200, {
430+
result: {
431+
value: 5000,
432+
},
433+
});
434+
435+
const awmNock = nock(advancedWalletManagerUrl)
436+
.post(`/api/${solCoinId}/mpc/recovery`)
437+
.reply(200, {
438+
txHex:
439+
'AWkNYn5JOxl5bLmFN8BB/Yyz8pLvrNpyZ6fUiTDpSnkK9dtts5VEBQOdLEaG3D18sN8dPxhnS+TzmmuUPMl0WAUBAAECvoOqYkvCPusjYyhX4GdUtzSeVIcx6GkwdpSk8SkU0/cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQtFGO2YBsrubq15CKqJLwXG3VEF1aEs36Rao6EaJDLAQECAAAMAgAAALhJxgAAAAAA',
440+
});
441+
442+
const response = await agent
443+
.post(`/api/${solCoinId}/wallet/recovery`)
444+
.set('Authorization', `Bearer ${accessToken}`)
445+
.send({
446+
isTssRecovery: true,
447+
tssRecoveryParams: {
448+
commonKeychain:
449+
'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174',
450+
},
451+
recoveryDestinationAddress: 'DpgugQVWnNbTQr6jqLvkHQVWa43WTGWb7jH5zeNGJjtA',
452+
coinSpecificParams: {
453+
solanaRecoveryOptions: {}, // none are required for token recoveries
454+
},
455+
});
456+
457+
response.status.should.equal(200);
458+
response.body.should.have.property('txHex');
459+
460+
solAccountBalanceNock.isDone().should.be.true();
461+
solBlockHashNock.isDone().should.be.true();
462+
awmNock.isDone().should.be.true();
463+
});
464+
437465
it('should reject incorrect UTXO parameters for a Solana coin', async () => {
438466
const userPub = 'solana_pubkey';
439467
const recoveryDestination = 'solanaRecoveryAddress123456789012345678901234';

0 commit comments

Comments
 (0)