Skip to content

Commit 38f6430

Browse files
feat(express): migrate initwallet to typed routes
2 parents 288ce71 + c0b322c commit 38f6430

File tree

6 files changed

+95
-32
lines changed

6 files changed

+95
-32
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
15581558
router.post('express.decrypt', [prepareBitGo(config), typedPromiseWrapper(handleDecrypt)]);
15591559
router.post('express.encrypt', [prepareBitGo(config), typedPromiseWrapper(handleEncrypt)]);
15601560
router.post('express.verifyaddress', [prepareBitGo(config), typedPromiseWrapper(handleVerifyAddress)]);
1561+
router.post('express.lightning.initWallet', [prepareBitGo(config), typedPromiseWrapper(handleInitLightningWallet)]);
15611562
app.post(
15621563
'/api/v[12]/calculateminerfeeinfo',
15631564
parseBody,
@@ -1782,12 +1783,6 @@ export function setupEnclavedExpressRoutes(app: express.Application, config: Con
17821783
}
17831784

17841785
export function setupLightningSignerNodeRoutes(app: express.Application, config: Config): void {
1785-
app.post(
1786-
'/api/v2/:coin/wallet/:id/initwallet',
1787-
parseBody,
1788-
prepareBitGo(config),
1789-
promiseWrapper(handleInitLightningWallet)
1790-
);
17911786
app.post(
17921787
'/api/v2/:coin/wallet/:id/signermacaroon',
17931788
parseBody,

modules/express/src/lightning/lightningSignerRoutes.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,10 @@ import {
1414
import * as utxolib from '@bitgo/utxo-lib';
1515
import { Buffer } from 'buffer';
1616

17-
import {
18-
CreateSignerMacaroonRequest,
19-
GetWalletStateResponse,
20-
InitLightningWalletRequest,
21-
UnlockLightningWalletRequest,
22-
} from './codecs';
17+
import { CreateSignerMacaroonRequest, GetWalletStateResponse, UnlockLightningWalletRequest } from './codecs';
2318
import { LndSignerClient } from './lndSignerClient';
2419
import { ApiResponseError } from '../errors';
20+
import { ExpressApiRouteRequest } from '../typedRoutes/api';
2521

2622
type Decrypt = (params: { input: string; password: string }) => string;
2723

@@ -61,29 +57,16 @@ function getMacaroonRootKey(passphrase: string, nodeAuthEncryptedPrv: string, de
6157
/**
6258
* Handle the request to initialise remote signer LND for a wallet.
6359
*/
64-
export async function handleInitLightningWallet(req: express.Request): Promise<unknown> {
60+
export async function handleInitLightningWallet(
61+
req: ExpressApiRouteRequest<'express.lightning.initWallet', 'post'>
62+
): Promise<unknown> {
6563
const bitgo = req.bitgo;
66-
const coinName = req.params.coin;
64+
const { coin: coinName, walletId, passphrase, expressHost } = req.decoded;
6765
if (!isLightningCoinName(coinName)) {
6866
throw new ApiResponseError(`Invalid coin ${coinName}. This is not a lightning coin.`, 400);
6967
}
7068
const coin = bitgo.coin(coinName);
7169

72-
const walletId = req.params.id;
73-
if (typeof walletId !== 'string') {
74-
throw new ApiResponseError(`Invalid wallet id: ${walletId}`, 400);
75-
}
76-
77-
const { passphrase, expressHost } = decodeOrElse(
78-
InitLightningWalletRequest.name,
79-
InitLightningWalletRequest,
80-
req.body,
81-
(_) => {
82-
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
83-
throw new ApiResponseError('Invalid request body to initialize lightning wallet', 400);
84-
}
85-
);
86-
8770
const wallet = await coin.wallets().get({ id: walletId, includeBalance: false });
8871
if (wallet.subType() !== 'lightningSelfCustody') {
8972
throw new ApiResponseError(`not a self custodial lighting wallet ${walletId}`, 400);

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

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

1718
export const ExpressApi = apiSpec({
@@ -45,6 +46,9 @@ export const ExpressApi = apiSpec({
4546
'express.v1.wallet.signTransaction': {
4647
post: PostSignTransaction,
4748
},
49+
'express.lightning.initWallet': {
50+
post: PostLightningInitWallet,
51+
},
4852
'express.verifycoinaddress': {
4953
post: PostVerifyCoinAddress,
5054
},
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
/**
6+
* Path parameters for initializing a Lightning wallet
7+
* @property {string} coin - A lightning coin name (e.g, lnbtc, tlnbtc).
8+
* @property {string} walletId - The ID of the wallet.
9+
*/
10+
export const LightningInitWalletParams = {
11+
coin: t.string,
12+
walletId: t.string,
13+
} as const;
14+
15+
/**
16+
* Request body for initializing a Lightning wallet
17+
*/
18+
export const LightningInitWalletBody = {
19+
/** Passphrase to encrypt the admin macaroon of the signer node. */
20+
passphrase: t.string,
21+
/** Optional hostname or IP address to set the `expressHost` field of the wallet. */
22+
expressHost: optional(t.string),
23+
} as const;
24+
25+
/**
26+
* Response for initializing a Lightning wallet
27+
*/
28+
export const LightningInitWalletResponse = {
29+
/** Returns the updated wallet. On success, the wallet's `coinSpecific` will include the encrypted admin macaroon for the Lightning signer node. */
30+
200: t.unknown,
31+
/** BitGo Express error payload when initialization cannot proceed (for example: invalid coin, unsupported environment, wallet not in an initializable state). */
32+
400: BitgoExpressError,
33+
} as const;
34+
35+
/**
36+
* Lightning - This is only used for self-custody lightning. Initialize a newly created Lightning Network Daemon (LND) for the first time.
37+
* Returns the updated wallet with the encrypted admin macaroon in the `coinSpecific` response field.
38+
*
39+
* @operationId express.lightning.initWallet
40+
*/
41+
export const PostLightningInitWallet = httpRoute({
42+
path: '/api/v2/:coin/wallet/:walletId/initwallet',
43+
method: 'POST',
44+
request: httpRequest({ params: LightningInitWalletParams, body: LightningInitWalletBody }),
45+
response: LightningInitWalletResponse,
46+
});
47+
48+
export type PostLightningInitWallet = typeof PostLightningInitWallet;

modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
22
import { BitGo } from 'bitgo';
3-
import { common } from '@bitgo/sdk-core';
3+
import { common, decodeOrElse } from '@bitgo/sdk-core';
44
import nock from 'nock';
55
import * as express from 'express';
66
import * as sinon from 'sinon';
@@ -13,6 +13,8 @@ import {
1313
handleInitLightningWallet,
1414
handleUnlockLightningWallet,
1515
} from '../../../../src/lightning/lightningSignerRoutes';
16+
import { ExpressApiRouteRequest } from '../../../../src/typedRoutes/api';
17+
import { PostLightningInitWallet } from '../../../../src/typedRoutes/api/v2/lightningInitWallet';
1618

1719
describe('Lightning signer routes', () => {
1820
let bitgo: TestBitGoAPI;
@@ -74,9 +76,18 @@ describe('Lightning signer routes', () => {
7476
config: {
7577
lightningSignerFileSystemPath: 'lightningSignerFileSystemPath',
7678
},
77-
} as unknown as express.Request;
79+
decoded: {
80+
coin: 'tlnbtc',
81+
walletId: apiData.wallet.id,
82+
passphrase: apiData.initWalletRequestBody.passphrase,
83+
...(includingOptionalFields ? { expressHost: apiData.initWalletRequestBody.expressHost } : {}),
84+
},
85+
} as unknown as ExpressApiRouteRequest<'express.lightning.initWallet', 'post'>;
7886

79-
await handleInitLightningWallet(req);
87+
const res = await handleInitLightningWallet(req);
88+
decodeOrElse('PostLightningInitWallet.response.200', PostLightningInitWallet.response[200], res, (_) => {
89+
throw new Error('Response did not match expected codec');
90+
});
8091

8192
wpWalletUpdateNock.done();
8293
signerInitWalletNock.done();

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ 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 {
10+
LightningInitWalletBody,
11+
LightningInitWalletParams,
12+
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
913

1014
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
1115
const result = codec.decode(input);
@@ -129,4 +133,22 @@ describe('io-ts decode tests', function () {
129133
passphrase: 'pass',
130134
});
131135
});
136+
it('express.lightning.initWallet params', function () {
137+
// missing walletId
138+
assert.throws(() => assertDecode(t.type(LightningInitWalletParams), { coin: 'ltc' }));
139+
// valid
140+
assertDecode(t.type(LightningInitWalletParams), { coin: 'ltc', walletId: 'wallet123' });
141+
});
142+
it('express.lightning.initWallet body', function () {
143+
// missing passphrase
144+
assert.throws(() => assertDecode(t.type(LightningInitWalletBody), {}));
145+
// passphrase must be string
146+
assert.throws(() => assertDecode(t.type(LightningInitWalletBody), { passphrase: 123 }));
147+
// expressHost optional and must be string if provided
148+
assert.throws(() => assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 99 }));
149+
// valid minimal
150+
assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p' });
151+
// valid with expressHost
152+
assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 'host.example' });
153+
});
132154
});

0 commit comments

Comments
 (0)