Skip to content
7 changes: 1 addition & 6 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1558,6 +1558,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
router.post('express.decrypt', [prepareBitGo(config), typedPromiseWrapper(handleDecrypt)]);
router.post('express.encrypt', [prepareBitGo(config), typedPromiseWrapper(handleEncrypt)]);
router.post('express.verifyaddress', [prepareBitGo(config), typedPromiseWrapper(handleVerifyAddress)]);
router.post('express.lightning.initWallet', [prepareBitGo(config), typedPromiseWrapper(handleInitLightningWallet)]);
app.post(
'/api/v[12]/calculateminerfeeinfo',
parseBody,
Expand Down Expand Up @@ -1782,12 +1783,6 @@ export function setupEnclavedExpressRoutes(app: express.Application, config: Con
}

export function setupLightningSignerNodeRoutes(app: express.Application, config: Config): void {
app.post(
'/api/v2/:coin/wallet/:id/initwallet',
parseBody,
prepareBitGo(config),
promiseWrapper(handleInitLightningWallet)
);
app.post(
'/api/v2/:coin/wallet/:id/signermacaroon',
parseBody,
Expand Down
29 changes: 6 additions & 23 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@ import {
import * as utxolib from '@bitgo/utxo-lib';
import { Buffer } from 'buffer';

import {
CreateSignerMacaroonRequest,
GetWalletStateResponse,
InitLightningWalletRequest,
UnlockLightningWalletRequest,
} from './codecs';
import { CreateSignerMacaroonRequest, GetWalletStateResponse, UnlockLightningWalletRequest } from './codecs';
import { LndSignerClient } from './lndSignerClient';
import { ApiResponseError } from '../errors';
import { ExpressApiRouteRequest } from '../typedRoutes/api';

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

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

const walletId = req.params.id;
if (typeof walletId !== 'string') {
throw new ApiResponseError(`Invalid wallet id: ${walletId}`, 400);
}

const { passphrase, expressHost } = decodeOrElse(
InitLightningWalletRequest.name,
InitLightningWalletRequest,
req.body,
(_) => {
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
throw new ApiResponseError('Invalid request body to initialize lightning wallet', 400);
}
);

const wallet = await coin.wallets().get({ id: walletId, includeBalance: false });
if (wallet.subType() !== 'lightningSelfCustody') {
throw new ApiResponseError(`not a self custodial lighting wallet ${walletId}`, 400);
Expand Down
4 changes: 4 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { PostAcceptShare } from './v1/acceptShare';
import { PostSimpleCreate } from './v1/simpleCreate';
import { PutPendingApproval } from './v1/pendingApproval';
import { PostSignTransaction } from './v1/signTransaction';
import { PostLightningInitWallet } from './v2/lightningInitWallet';
import { PostVerifyCoinAddress } from './v2/verifyAddress';

export const ExpressApi = apiSpec({
Expand Down Expand Up @@ -45,6 +46,9 @@ export const ExpressApi = apiSpec({
'express.v1.wallet.signTransaction': {
post: PostSignTransaction,
},
'express.lightning.initWallet': {
post: PostLightningInitWallet,
},
'express.verifycoinaddress': {
post: PostVerifyCoinAddress,
},
Expand Down
48 changes: 48 additions & 0 deletions modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

/**
* Path parameters for initializing a Lightning wallet
* @property {string} coin - A lightning coin name (e.g, lnbtc, tlnbtc).
* @property {string} walletId - The ID of the wallet.
*/
export const LightningInitWalletParams = {
coin: t.string,
walletId: t.string,
} as const;

/**
* Request body for initializing a Lightning wallet
*/
export const LightningInitWalletBody = {
/** Passphrase to encrypt the admin macaroon of the signer node. */
passphrase: t.string,
/** Optional hostname or IP address to set the `expressHost` field of the wallet. */
expressHost: optional(t.string),
} as const;

/**
* Response for initializing a Lightning wallet
*/
export const LightningInitWalletResponse = {
/** Returns the updated wallet. On success, the wallet's `coinSpecific` will include the encrypted admin macaroon for the Lightning signer node. */
200: t.unknown,
/** BitGo Express error payload when initialization cannot proceed (for example: invalid coin, unsupported environment, wallet not in an initializable state). */
400: BitgoExpressError,
} as const;

/**
* Lightning - This is only used for self-custody lightning. Initialize a newly created Lightning Network Daemon (LND) for the first time.
* Returns the updated wallet with the encrypted admin macaroon in the `coinSpecific` response field.
*
* @operationId express.lightning.initWallet
*/
export const PostLightningInitWallet = httpRoute({
path: '/api/v2/:coin/wallet/:walletId/initwallet',
method: 'POST',
request: httpRequest({ params: LightningInitWalletParams, body: LightningInitWalletBody }),
response: LightningInitWalletResponse,
});

export type PostLightningInitWallet = typeof PostLightningInitWallet;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { BitGo } from 'bitgo';
import { common } from '@bitgo/sdk-core';
import { common, decodeOrElse } from '@bitgo/sdk-core';
import nock from 'nock';
import * as express from 'express';
import * as sinon from 'sinon';
Expand All @@ -13,6 +13,8 @@ import {
handleInitLightningWallet,
handleUnlockLightningWallet,
} from '../../../../src/lightning/lightningSignerRoutes';
import { ExpressApiRouteRequest } from '../../../../src/typedRoutes/api';
import { PostLightningInitWallet } from '../../../../src/typedRoutes/api/v2/lightningInitWallet';

describe('Lightning signer routes', () => {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -74,9 +76,18 @@ describe('Lightning signer routes', () => {
config: {
lightningSignerFileSystemPath: 'lightningSignerFileSystemPath',
},
} as unknown as express.Request;
decoded: {
coin: 'tlnbtc',
walletId: apiData.wallet.id,
passphrase: apiData.initWalletRequestBody.passphrase,
...(includingOptionalFields ? { expressHost: apiData.initWalletRequestBody.expressHost } : {}),
},
} as unknown as ExpressApiRouteRequest<'express.lightning.initWallet', 'post'>;

await handleInitLightningWallet(req);
const res = await handleInitLightningWallet(req);
decodeOrElse('PostLightningInitWallet.response.200', PostLightningInitWallet.response[200], res, (_) => {
throw new Error('Response did not match expected codec');
});

wpWalletUpdateNock.done();
signerInitWalletNock.done();
Expand Down
22 changes: 22 additions & 0 deletions modules/express/test/unit/typedRoutes/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { LoginRequest } from '../../../src/typedRoutes/api/common/login';
import { VerifyAddressBody } from '../../../src/typedRoutes/api/common/verifyAddress';
import { VerifyAddressV2Body, VerifyAddressV2Params } from '../../../src/typedRoutes/api/v2/verifyAddress';
import { SimpleCreateRequestBody } from '../../../src/typedRoutes/api/v1/simpleCreate';
import {
LightningInitWalletBody,
LightningInitWalletParams,
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';

export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
const result = codec.decode(input);
Expand Down Expand Up @@ -129,4 +133,22 @@ describe('io-ts decode tests', function () {
passphrase: 'pass',
});
});
it('express.lightning.initWallet params', function () {
// missing walletId
assert.throws(() => assertDecode(t.type(LightningInitWalletParams), { coin: 'ltc' }));
// valid
assertDecode(t.type(LightningInitWalletParams), { coin: 'ltc', walletId: 'wallet123' });
});
it('express.lightning.initWallet body', function () {
// missing passphrase
assert.throws(() => assertDecode(t.type(LightningInitWalletBody), {}));
// passphrase must be string
assert.throws(() => assertDecode(t.type(LightningInitWalletBody), { passphrase: 123 }));
// expressHost optional and must be string if provided
assert.throws(() => assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 99 }));
// valid minimal
assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p' });
// valid with expressHost
assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 'host.example' });
});
});