Skip to content

Commit 10b2022

Browse files
feat(express): migrate keychainlocal to typed routes
2 parents a6bb6ad + 6faab4c commit 10b2022

File tree

5 files changed

+134
-7
lines changed

5 files changed

+134
-7
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ async function handleV2PendingApproval(req: express.Request): Promise<any> {
686686
* create a keychain
687687
* @param req
688688
*/
689-
function handleV2CreateLocalKeyChain(req: express.Request) {
689+
export function handleV2CreateLocalKeyChain(req: ExpressApiRouteRequest<'express.keychain.local', 'post'>) {
690690
const bitgo = req.bitgo;
691691
const coin = bitgo.coin(req.params.coin);
692692
return coin.keychains().create(req.body);
@@ -1609,12 +1609,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16091609
// API v2
16101610

16111611
// create keychain
1612-
app.post(
1613-
'/api/v2/:coin/keychain/local',
1614-
parseBody,
1615-
prepareBitGo(config),
1616-
promiseWrapper(handleV2CreateLocalKeyChain)
1617-
);
1612+
router.post('express.keychain.local', [prepareBitGo(config), typedPromiseWrapper(handleV2CreateLocalKeyChain)]);
16181613

16191614
// generate wallet
16201615
app.post('/api/v2/:coin/wallet/generate', parseBody, prepareBitGo(config), promiseWrapper(handleV2GenerateWallet));

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { PostAcceptShare } from './v1/acceptShare';
1313
import { PostSimpleCreate } from './v1/simpleCreate';
1414
import { PutPendingApproval } from './v1/pendingApproval';
1515
import { PostSignTransaction } from './v1/signTransaction';
16+
import { PostKeychainLocal } from './v2/keychainLocal';
1617
import { PostLightningInitWallet } from './v2/lightningInitWallet';
1718
import { PostVerifyCoinAddress } from './v2/verifyAddress';
1819

@@ -47,6 +48,9 @@ export const ExpressApi = apiSpec({
4748
'express.v1.wallet.signTransaction': {
4849
post: PostSignTransaction,
4950
},
51+
'express.keychain.local': {
52+
post: PostKeychainLocal,
53+
},
5054
'express.lightning.initWallet': {
5155
post: PostLightningInitWallet,
5256
},
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Request parameters for creating a local keychain
7+
* @property {string} coin - Coin identifier (e.g. btc, tbtc, eth)
8+
*/
9+
export const KeychainLocalRequestParams = {
10+
/** Coin identifier (e.g. btc, tbtc, eth) */
11+
coin: t.string,
12+
} as const;
13+
14+
export const KeychainLocalRequestResponse200 = t.type({
15+
prv: t.string,
16+
pub: t.string,
17+
});
18+
19+
/**
20+
* Response for creating a local keychain
21+
*/
22+
export const KeychainLocalResponse = {
23+
/** Newly generated local keychain. */
24+
200: KeychainLocalRequestResponse200,
25+
/** Invalid request or key generation fails. */
26+
400: BitgoExpressError,
27+
} as const;
28+
29+
/**
30+
* Local client-side function to create a new keychain.
31+
* Creating your keychains is a critical step for safely securing your Bitcoin. When generating new keychains, this API uses a random number generator that adheres to industry standards. If you provide your own seed, you must take extreme caution when creating it.
32+
* Returns an object containing the xprv and xpub for the new chain. The created keychain is not known to the BitGo service. To use it with the BitGo service, use the ‘Store Keychain’ API call.
33+
*
34+
* For security reasons, it is highly recommended that you encrypt and destroy the original xprv immediately to prevent theft.
35+
*
36+
* @operationId express.keychain.local
37+
*/
38+
export const PostKeychainLocal = httpRoute({
39+
path: '/api/v2/{coin}/keychain/local',
40+
method: 'POST',
41+
request: httpRequest({
42+
params: KeychainLocalRequestParams,
43+
}),
44+
response: KeychainLocalResponse,
45+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as sinon from 'sinon';
2+
3+
import 'should-http';
4+
import 'should-sinon';
5+
import '../../lib/asserts';
6+
7+
import nock from 'nock';
8+
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
9+
import { BitGo } from 'bitgo';
10+
import { BaseCoin, decodeOrElse } from '@bitgo/sdk-core';
11+
12+
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
13+
import { handleV2CreateLocalKeyChain } from '../../../src/clientRoutes';
14+
import { KeychainLocalResponse } from '../../../src/typedRoutes/api/v2/keychainLocal';
15+
16+
describe('Create Local Keychain', () => {
17+
let bitgo: TestBitGoAPI;
18+
19+
before(async function () {
20+
if (!nock.isActive()) {
21+
nock.activate();
22+
}
23+
24+
bitgo = TestBitGo.decorate(BitGo, { env: 'test' });
25+
bitgo.initializeTestVars();
26+
27+
nock.disableNetConnect();
28+
nock.enableNetConnect('127.0.0.1');
29+
});
30+
31+
after(() => {
32+
if (nock.isActive()) {
33+
nock.restore();
34+
}
35+
});
36+
37+
it('should create a local keychain and return the keypair', async () => {
38+
const createdKeypair = { prv: 'prv-example', pub: 'pub-example' };
39+
const keychainsInstance = { create: sinon.stub().returns(createdKeypair) } as const;
40+
const coinStub = {
41+
keychains: sinon.stub<[], typeof keychainsInstance>().returns(keychainsInstance),
42+
} as unknown as BaseCoin;
43+
const coinMethodStub = sinon.stub(bitgo, 'coin').returns(coinStub as any);
44+
45+
const req = {
46+
bitgo: bitgo,
47+
params: { coin: 'tbtc' },
48+
body: {},
49+
} as unknown as ExpressApiRouteRequest<'express.keychain.local', 'post'>;
50+
51+
const res = await handleV2CreateLocalKeyChain(req);
52+
53+
decodeOrElse('KeychainLocalResponse200', KeychainLocalResponse[200], res, (errors) => {
54+
throw new Error(`Response did not match expected codec: ${errors}`);
55+
});
56+
57+
sinon.assert.calledOnce(keychainsInstance.create);
58+
coinMethodStub.restore();
59+
});
60+
});

modules/express/test/unit/typedRoutes/decode.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { LoginRequest } from '../../../src/typedRoutes/api/common/login';
66
import { VerifyAddressBody } from '../../../src/typedRoutes/api/common/verifyAddress';
77
import { VerifyAddressV2Body, VerifyAddressV2Params } from '../../../src/typedRoutes/api/v2/verifyAddress';
88
import { SimpleCreateRequestBody } from '../../../src/typedRoutes/api/v1/simpleCreate';
9+
import { KeychainLocalRequestParams } from '../../../src/typedRoutes/api/v2/keychainLocal';
910
import {
1011
LightningInitWalletBody,
1112
LightningInitWalletParams,
@@ -133,6 +134,28 @@ describe('io-ts decode tests', function () {
133134
passphrase: 'pass',
134135
});
135136
});
137+
it('express.keychain.local', function () {
138+
// coin parameter is required
139+
assert.throws(() => assertDecode(t.type(KeychainLocalRequestParams), {}));
140+
// coin must be a string
141+
assert.throws(() =>
142+
assertDecode(t.type(KeychainLocalRequestParams), {
143+
coin: 123,
144+
})
145+
);
146+
// valid with coin parameter
147+
assertDecode(t.type(KeychainLocalRequestParams), {
148+
coin: 'btc',
149+
});
150+
// valid with different coin
151+
assertDecode(t.type(KeychainLocalRequestParams), {
152+
coin: 'eth',
153+
});
154+
// valid with testnet coin
155+
assertDecode(t.type(KeychainLocalRequestParams), {
156+
coin: 'tbtc',
157+
});
158+
});
136159
it('express.lightning.initWallet params', function () {
137160
// missing walletId
138161
assert.throws(() => assertDecode(t.type(LightningInitWalletParams), { coin: 'ltc' }));

0 commit comments

Comments
 (0)