Skip to content

Commit 1ef6e5f

Browse files
committed
feat: Add support for EVM keyring wallets in generateWallet API
This commit introduces support for generating EVM keyring wallets in the `generateWallet` API. Ticket: WAL-479
1 parent 072678e commit 1ef6e5f

3 files changed

Lines changed: 165 additions & 48 deletions

File tree

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

Lines changed: 131 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,49 @@ import { BitGoRequest } from '../../../types/request';
2020
* in how the constants are fetched.
2121
*/
2222

23+
function mockWalletResponse(id: string, coinName: string, overrides: Record<string, unknown> = {}) {
24+
return {
25+
id,
26+
users: [{ user: 'user-id', permissions: ['admin', 'spend', 'view'] }],
27+
coin: coinName,
28+
label: 'test_wallet',
29+
m: 2,
30+
n: 3,
31+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
32+
keySignatures: {},
33+
enterprise: 'test_enterprise',
34+
organization: 'org-id',
35+
bitgoOrg: 'BitGo Inc',
36+
tags: [id, 'test_enterprise'],
37+
disableTransactionNotifications: false,
38+
freeze: {},
39+
deleted: false,
40+
approvalsRequired: 1,
41+
isCold: false,
42+
coinSpecific: {},
43+
admin: {},
44+
allowBackupKeySigning: false,
45+
clientFlags: [],
46+
recoverable: false,
47+
startDate: '2025-01-01T00:00:00.000Z',
48+
hasLargeNumberOfAddresses: false,
49+
config: {},
50+
balanceString: '0',
51+
confirmedBalanceString: '0',
52+
spendableBalanceString: '0',
53+
receiveAddress: {
54+
id: 'addr-id',
55+
address: '0xexampleaddress',
56+
chain: 0,
57+
index: 0,
58+
coin: coinName,
59+
wallet: id,
60+
coinSpecific: {},
61+
},
62+
...overrides,
63+
};
64+
}
65+
2366
describe('POST /api/v1/:coin/advancedwallet/generate', () => {
2467
let agent: request.SuperAgentTest;
2568
const advancedWalletManagerUrl = 'http://advancedwalletmanager.invalid';
@@ -136,54 +179,24 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
136179
type: 'advanced',
137180
})
138181
.matchHeader('any', () => true)
139-
.reply(200, {
140-
id: 'new-wallet-id',
141-
users: [
142-
{
143-
user: 'user-id',
144-
permissions: ['admin', 'spend', 'view'],
182+
.reply(
183+
200,
184+
mockWalletResponse('new-wallet-id', coin, {
185+
isCold: true,
186+
pendingApprovals: [],
187+
receiveAddress: {
188+
id: 'addr-id',
189+
address: 'tb1qexampleaddress000000000000000000000',
190+
chain: 20,
191+
index: 0,
192+
coin: coin,
193+
wallet: 'new-wallet-id',
194+
coinSpecific: {},
145195
},
146-
],
147-
coin: coin,
148-
label: 'test_wallet',
149-
m: 2,
150-
n: 3,
151-
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
152-
keySignatures: {},
153-
enterprise: 'test_enterprise',
154-
organization: 'org-id',
155-
bitgoOrg: 'BitGo Inc',
156-
tags: ['new-wallet-id', 'test_enterprise'],
157-
disableTransactionNotifications: false,
158-
freeze: {},
159-
deleted: false,
160-
approvalsRequired: 1,
161-
isCold: true,
162-
coinSpecific: {},
163-
admin: {},
164-
pendingApprovals: [],
165-
allowBackupKeySigning: false,
166-
clientFlags: [],
167-
recoverable: false,
168-
startDate: '2025-01-01T00:00:00.000Z',
169-
hasLargeNumberOfAddresses: false,
170-
config: {},
171-
balanceString: '0',
172-
confirmedBalanceString: '0',
173-
spendableBalanceString: '0',
174-
receiveAddress: {
175-
id: 'addr-id',
176-
address: 'tb1qexampleaddress000000000000000000000',
177-
chain: 20,
178-
index: 0,
179-
coin: coin,
180-
wallet: 'new-wallet-id',
181-
coinSpecific: {},
182-
},
183-
// optional-ish fields used in assertions
184-
multisigType: 'onchain',
185-
type: 'advanced',
186-
});
196+
multisigType: 'onchain',
197+
type: 'advanced',
198+
}),
199+
);
187200

188201
const response = await agent
189202
.post(`/api/v1/${coin}/advancedwallet/generate`)
@@ -1283,4 +1296,75 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
12831296
response.status.should.equal(400);
12841297
response.body.details.should.equal('MPC wallet generation is not supported for coin tbtc');
12851298
});
1299+
1300+
it('should skip calls to AWM and use existing keychains when evmKeyRingReferenceWalletId is provided', async () => {
1301+
/** GET mocks for Key Retrieval */
1302+
const userKeyNock = nock(bitgoApiUrl)
1303+
.get(`/api/v2/${ecdsaCoin}/key/user-key-id`)
1304+
.reply(200, { id: 'user-key-id', source: 'user', type: 'independent' });
1305+
1306+
const backupKeyNock = nock(bitgoApiUrl)
1307+
.get(`/api/v2/${ecdsaCoin}/key/backup-key-id`)
1308+
.reply(200, { id: 'backup-key-id', source: 'backup', type: 'independent' });
1309+
1310+
const bitgoKeyNock = nock(bitgoApiUrl).get(`/api/v2/${ecdsaCoin}/key/bitgo-key-id`).reply(200, {
1311+
id: 'bitgo-key-id',
1312+
source: 'bitgo',
1313+
type: 'independent',
1314+
isBitGo: true,
1315+
isTrust: false,
1316+
hsmType: 'institutional',
1317+
});
1318+
1319+
/** POST mock for the actual wallet creation */
1320+
const walletAddNock = nock(bitgoApiUrl)
1321+
.post(`/api/v2/${ecdsaCoin}/wallet/add`, {
1322+
label: 'test_wallet',
1323+
evmKeyRingReferenceWalletId: '59cd72485007a239fb00282ed480da1f',
1324+
})
1325+
.matchHeader('any', () => true)
1326+
.reply(
1327+
200,
1328+
mockWalletResponse('new-keyring-wallet-id', ecdsaCoin, {
1329+
multisigType: 'tss',
1330+
type: 'advanced',
1331+
}),
1332+
);
1333+
1334+
const response = await agent
1335+
.post(`/api/v1/${ecdsaCoin}/advancedwallet/generate`)
1336+
.set('Authorization', `Bearer ${accessToken}`)
1337+
.send({
1338+
label: 'test_wallet',
1339+
enterprise: 'test_enterprise',
1340+
multisigType: 'tss',
1341+
evmKeyRingReferenceWalletId: '59cd72485007a239fb00282ed480da1f',
1342+
});
1343+
1344+
response.status.should.equal(200);
1345+
response.body.wallet.id.should.equal('new-keyring-wallet-id');
1346+
1347+
/** AWM was never called — if it had been, nock would've thrown since we never mocked POST AWM calls */
1348+
walletAddNock.done();
1349+
userKeyNock.done();
1350+
backupKeyNock.done();
1351+
bitgoKeyNock.done();
1352+
});
1353+
1354+
it('should fail when evmKeyRingReferenceWalletId is provided for a non-EVM coin', async () => {
1355+
const response = await agent
1356+
.post(`/api/v1/${coin}/advancedwallet/generate`)
1357+
.set('Authorization', `Bearer ${accessToken}`)
1358+
.send({
1359+
label: 'test_wallet',
1360+
enterprise: 'test_enterprise',
1361+
multisigType: 'onchain',
1362+
evmKeyRingReferenceWalletId: '59cd72485007a239fb00282ed480da1f',
1363+
});
1364+
1365+
response.status.should.equal(400);
1366+
response.body.details.should.containEql(
1367+
'EVM keyring wallet generation is not supported for coin tbtc',
1368+
);
1369+
});
12861370
});

src/masterBitgoExpress/handlers/handleGenerateWallet.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ import { BadRequestError } from '../../shared/errors';
2121
export async function handleGenerateWallet(
2222
req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>,
2323
) {
24-
const { multisigType } = req.decoded;
24+
const { multisigType, evmKeyRingReferenceWalletId } = req.decoded;
25+
26+
if (evmKeyRingReferenceWalletId) {
27+
return handleGenerateEvmKeyRingWallet(req);
28+
}
2529

2630
if (multisigType === 'tss') {
2731
return handleGenerateMpcWallet(req);
@@ -212,3 +216,25 @@ async function handleGenerateMpcWallet(
212216

213217
return { ...result, wallet: result.wallet.toJSON() };
214218
}
219+
220+
/**
221+
* This function generates an EVM keyring wallet by reusing keys from a reference wallet.
222+
*/
223+
async function handleGenerateEvmKeyRingWallet(
224+
req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>,
225+
) {
226+
const bitgo = req.bitgo;
227+
const baseCoin = await coinFactory.getCoin(req.decoded.coin, bitgo);
228+
if (!baseCoin.isEVM()) {
229+
throw new BadRequestError(
230+
`EVM keyring wallet generation is not supported for coin ${req.decoded.coin}`,
231+
);
232+
}
233+
234+
const result = await baseCoin.wallets().generateWallet(req.decoded);
235+
236+
return {
237+
...result,
238+
wallet: result.wallet.toJSON(),
239+
};
240+
}

src/masterBitgoExpress/routers/generateWalletRoute.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,13 @@ const GenerateWalletRequest = {
329329
* @maximum 3
330330
*/
331331
walletVersion: optional(t.number),
332+
333+
/**
334+
* Reference wallet ID for EVM keyring wallets
335+
* @example "59cd72485007a239fb00282ed480da1f"
336+
* @pattern ^[0-9a-f]{32}$
337+
*/
338+
evmKeyRingReferenceWalletId: optional(t.string),
332339
};
333340

334341
/**

0 commit comments

Comments
 (0)