diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 69205bc699..f4d36d60ba 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -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), @@ -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)); } diff --git a/modules/express/src/lightning/lightningSignerRoutes.ts b/modules/express/src/lightning/lightningSignerRoutes.ts index be90fa7630..05ac16e757 100644 --- a/modules/express/src/lightning/lightningSignerRoutes.ts +++ b/modules/express/src/lightning/lightningSignerRoutes.ts @@ -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; @@ -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 diff --git a/modules/express/src/typedRoutes/api/index.ts b/modules/express/src/typedRoutes/api/index.ts index 436664cf15..fa5871ff9b 100644 --- a/modules/express/src/typedRoutes/api/index.ts +++ b/modules/express/src/typedRoutes/api/index.ts @@ -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'; @@ -55,6 +56,9 @@ export const ExpressApi = apiSpec({ 'express.lightning.initWallet': { post: PostLightningInitWallet, }, + 'express.lightning.unlockWallet': { + post: PostUnlockLightningWallet, + }, 'express.verifycoinaddress': { post: PostVerifyCoinAddress, }, diff --git a/modules/express/src/typedRoutes/api/v2/unlockWallet.ts b/modules/express/src/typedRoutes/api/v2/unlockWallet.ts new file mode 100644 index 0000000000..722428adf3 --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/unlockWallet.ts @@ -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, +}); diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 5a9ed3483c..82f693644c 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -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 { @@ -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(); diff --git a/modules/express/test/unit/typedRoutes/decode.ts b/modules/express/test/unit/typedRoutes/decode.ts index ccbf4ea02d..4385b786b1 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -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(codec: t.Type, input: unknown): T { const result = codec.decode(input); @@ -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' }); + }); });