Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,10 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
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)]);
router.post('express.lightning.unlockWallet', [
prepareBitGo(config),
typedPromiseWrapper(handleUnlockLightningWallet),
]);
router.post('express.calculateminerfeeinfo', [
prepareBitGo(config),
typedPromiseWrapper(handleCalculateMinerFeeInfo),
Expand Down Expand Up @@ -1783,11 +1787,5 @@ export function setupLightningSignerNodeRoutes(app: express.Application, config:
prepareBitGo(config),
promiseWrapper(handleCreateSignerMacaroon)
);
app.post(
'/api/v2/:coin/wallet/:id/unlockwallet',
parseBody,
prepareBitGo(config),
promiseWrapper(handleUnlockLightningWallet)
);
app.get('/api/v2/:coin/wallet/:id/state', prepareBitGo(config), promiseWrapper(handleGetLightningWalletState));
}
24 changes: 6 additions & 18 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import {
} from '@bitgo/abstract-lightning';
import * as utxolib from '@bitgo/utxo-lib';
import { Buffer } from 'buffer';
import { ExpressApiRouteRequest } from '../typedRoutes/api';

import { CreateSignerMacaroonRequest, GetWalletStateResponse, UnlockLightningWalletRequest } from './codecs';
import { CreateSignerMacaroonRequest, GetWalletStateResponse } 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 @@ -187,25 +187,13 @@ export async function handleGetLightningWalletState(req: express.Request): Promi
/**
* Handle the request to unlock a wallet in the signer.
*/
export async function handleUnlockLightningWallet(req: express.Request): Promise<{ message: string }> {
const coinName = req.params.coin;
export async function handleUnlockLightningWallet(
req: ExpressApiRouteRequest<'express.lightning.unlockWallet', 'post'>
): Promise<{ message: string }> {
const { coin: coinName, id: walletId, passphrase } = req.decoded;
if (!isLightningCoinName(coinName)) {
throw new ApiResponseError(`Invalid coin to unlock lightning wallet: ${coinName}`, 400);
}
const walletId = req.params.id;
if (typeof walletId !== 'string') {
throw new ApiResponseError(`Invalid wallet id: ${walletId}`, 400);
}

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

const lndSignerClient = await LndSignerClient.create(walletId, req.config);
// The passphrase at LND can only accommodate a base64 character set
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 @@ -15,6 +15,7 @@ import { PutPendingApproval } from './v1/pendingApproval';
import { PostSignTransaction } from './v1/signTransaction';
import { PostKeychainLocal } from './v2/keychainLocal';
import { PostLightningInitWallet } from './v2/lightningInitWallet';
import { PostUnlockLightningWallet } from './v2/unlockWallet';
import { PostVerifyCoinAddress } from './v2/verifyAddress';
import { PostDeriveLocalKeyChain } from './v1/deriveLocalKeyChain';

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

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

/**
* Request body for unlocking a lightning wallet
* @property {string} passphrase - Passphrase to unlock the lightning wallet.
*/
export const UnlockLightningWalletBody = {
/** Passphrase to unlock the lightning wallet. */
passphrase: t.string,
} as const;

export const UnlockLightningWalletResponse200 = t.type({
message: t.string,
});

/**
* Response for unlocking a lightning wallet.
*/
export const UnlockLightningWalletResponse = {
/** Confirmation message. */
200: UnlockLightningWalletResponse200,
/** BitGo Express error payload. */
400: BitgoExpressError,
} as const;

/**
* Lightning - Unlock node
*
* This is only used for self-custody lightning. Unlock the Lightning Network Daemon (LND) node with the given wallet password.
*
* @operationId express.lightning.unlockWallet
*/
export const PostUnlockLightningWallet = httpRoute({
method: 'POST',
path: '/api/v2/{coin}/wallet/{id}/unlockwallet',
request: httpRequest({
params: UnlockLightningWalletParams,
body: UnlockLightningWalletBody,
}),
response: UnlockLightningWalletResponse,
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import nock from 'nock';
import * as express from 'express';
import * as sinon from 'sinon';
import * as fs from 'fs';
import { UnlockLightningWalletResponse } from '../../../../src/typedRoutes/api/v2/unlockWallet';

import { lightningSignerConfigs, apiData, signerApiData } from './lightningSignerFixture';
import {
Expand Down Expand Up @@ -184,17 +185,14 @@ describe('Lightning signer routes', () => {

const req = {
bitgo: bitgo,
body: apiData.unlockWalletRequestBody,
params: {
coin: 'tlnbtc',
id: 'fakeid',
},
config: {
lightningSignerFileSystemPath: 'lightningSignerFileSystemPath',
},
} as unknown as express.Request;
config: { lightningSignerFileSystemPath: 'lightningSignerFileSystemPath' },
decoded: { coin: 'tlnbtc', id: 'fakeid', passphrase: apiData.unlockWalletRequestBody.passphrase },
} as unknown as ExpressApiRouteRequest<'express.lightning.unlockWallet', 'post'>;

await handleUnlockLightningWallet(req);
const res = await handleUnlockLightningWallet(req);
decodeOrElse('UnlockLightningWalletResponse200', UnlockLightningWalletResponse[200], res, (_) => {
throw new Error('Response did not match expected codec');
});

unlockwalletNock.done();
readFileStub.calledOnceWith('lightningSignerFileSystemPath').should.be.true();
Expand Down
9 changes: 9 additions & 0 deletions modules/express/test/unit/typedRoutes/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
LightningInitWalletBody,
LightningInitWalletParams,
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet';

export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
const result = codec.decode(input);
Expand Down Expand Up @@ -174,4 +175,12 @@ describe('io-ts decode tests', function () {
// valid with expressHost
assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 'host.example' });
});
it('express.lightning.unlockWallet', function () {
// params require coin and id
assertDecode(t.type(UnlockLightningWalletParams), { coin: 'tlnbtc', id: 'wallet123' });
// missing passphrase
assert.throws(() => assertDecode(t.type(UnlockLightningWalletBody), {}));
// valid body
assertDecode(t.type(UnlockLightningWalletBody), { passphrase: 'secret' });
});
});