From 916d4308e67e08c9a9b1c7440c2bef0b278a92f3 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Fri, 12 Sep 2025 15:39:08 -0400 Subject: [PATCH 1/6] feat(express): migrate getlightningstate to typed routes TICKET: WP-5447 --- modules/express/src/clientRoutes.ts | 2 +- .../src/lightning/lightningSignerRoutes.ts | 10 +++--- modules/express/src/typedRoutes/api/index.ts | 4 +++ .../src/typedRoutes/api/v2/lightningState.ts | 35 +++++++++++++++++++ .../lightning/lightningSignerRoutes.ts | 7 +++- .../express/test/unit/typedRoutes/decode.ts | 7 ++++ 6 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 modules/express/src/typedRoutes/api/v2/lightningState.ts diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 362969f4f6..76405862d6 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -1590,6 +1590,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void { ); router.post('express.v1.wallet.signTransaction', [prepareBitGo(config), typedPromiseWrapper(handleSignTransaction)]); + router.get('express.lightning.getState', [prepareBitGo(config), typedPromiseWrapper(handleGetLightningWalletState)]); app.post('/api/v1/wallet/:id/simpleshare', parseBody, prepareBitGo(config), promiseWrapper(handleShareWallet)); router.post('express.v1.wallet.acceptShare', [prepareBitGo(config), typedPromiseWrapper(handleAcceptShare)]); @@ -1808,5 +1809,4 @@ export function setupLightningSignerNodeRoutes(app: express.Application, config: 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 7c397fdf33..39944212a6 100644 --- a/modules/express/src/lightning/lightningSignerRoutes.ts +++ b/modules/express/src/lightning/lightningSignerRoutes.ts @@ -187,15 +187,13 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise< /** * Handle the request to get the state of a wallet from the signer. */ -export async function handleGetLightningWalletState(req: express.Request): Promise { - const coinName = req.params.coin; +export async function handleGetLightningWalletState( + req: import('../typedRoutes/api').ExpressApiRouteRequest<'express.lightning.getState', 'get'> +): Promise { + const { coin: coinName, walletId } = req.decoded; if (!isLightningCoinName(coinName)) { throw new ApiResponseError(`Invalid coin to get lightning wallet state: ${coinName}`, 400); } - const walletId = req.params.id; - if (typeof walletId !== 'string') { - throw new ApiResponseError(`Invalid wallet id: ${walletId}`, 400); - } const lndSignerClient = await LndSignerClient.create(walletId, req.config); return await lndSignerClient.getWalletState(); diff --git a/modules/express/src/typedRoutes/api/index.ts b/modules/express/src/typedRoutes/api/index.ts index 4a45375e0d..65aa318b7b 100644 --- a/modules/express/src/typedRoutes/api/index.ts +++ b/modules/express/src/typedRoutes/api/index.ts @@ -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 { GetLightningState } from './v2/lightningState'; export const ExpressApi = apiSpec({ 'express.ping': { @@ -44,6 +45,9 @@ export const ExpressApi = apiSpec({ 'express.v1.wallet.signTransaction': { post: PostSignTransaction, }, + 'express.lightning.getState': { + get: GetLightningState, + }, }); export type ExpressApi = typeof ExpressApi; diff --git a/modules/express/src/typedRoutes/api/v2/lightningState.ts b/modules/express/src/typedRoutes/api/v2/lightningState.ts new file mode 100644 index 0000000000..ce4a2cebcb --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/lightningState.ts @@ -0,0 +1,35 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest } from '@api-ts/io-ts-http'; +import { BitgoExpressError } from '../../schemas/error'; +import { WalletState } from '../../../lightning/codecs'; + +/** + * Path parameters for getting lightning node state + * @property {string} coin - A lightning coin name (e.g., lnbtc or tlnbtc) + * @property {string} walletId - The ID of the lightning self-custody wallet + */ +export const LightningStateParams = { + coin: t.string, + walletId: t.string, +}; + +/** + * Lightning - Get node state + * + * This is only used for self-custody lightning. Get the current state of the lightning node. + * + * @operationId express.lightning.getState + */ +export const GetLightningState = httpRoute({ + method: 'GET', + path: '/api/v2/{coin}/wallet/{walletId}/state', + request: httpRequest({ + params: LightningStateParams, + }), + response: { + 200: t.type({ + state: WalletState, + }), + 400: BitgoExpressError, + }, +}); diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 2f0c5dc2d9..e86e3660d8 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -153,11 +153,16 @@ describe('Lightning signer routes', () => { params: { coin: 'tlnbtc', id: apiData.wallet.id, + walletId: apiData.wallet.id, + }, + decoded: { + coin: 'tlnbtc', + walletId: apiData.wallet.id, }, config: { lightningSignerFileSystemPath: 'lightningSignerFileSystemPath', }, - } as unknown as express.Request; + } as any; await handleGetLightningWalletState(req); diff --git a/modules/express/test/unit/typedRoutes/decode.ts b/modules/express/test/unit/typedRoutes/decode.ts index 57641f6338..b564e1d711 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -5,6 +5,7 @@ import { EncryptRequestBody } from '../../../src/typedRoutes/api/common/encrypt' import { LoginRequest } from '../../../src/typedRoutes/api/common/login'; import { VerifyAddressBody } from '../../../src/typedRoutes/api/common/verifyAddress'; import { SimpleCreateRequestBody } from '../../../src/typedRoutes/api/v1/simpleCreate'; +import { LightningStateParams } from '../../../src/typedRoutes/api/v2/lightningState'; export function assertDecode(codec: t.Type, input: unknown): T { const result = codec.decode(input); @@ -100,4 +101,10 @@ describe('io-ts decode tests', function () { passphrase: 'pass', }); }); + it('express.lightning.getState params valid', function () { + assertDecode(t.type(LightningStateParams), { coin: 'lnbtc', walletId: 'wallet123' }); + }); + it('express.lightning.getState params invalid', function () { + assert.throws(() => assertDecode(t.type(LightningStateParams), { coin: 'lnbtc' })); + }); }); From 399ac1a98eff2763aebbe64da61a4b599706ac5d Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Tue, 16 Sep 2025 10:15:32 -0400 Subject: [PATCH 2/6] refactor: removed any type cast for something more explicit TICKET: WP-5447 --- .../test/unit/clientRoutes/lightning/lightningSignerRoutes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index e86e3660d8..88dc623202 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -13,6 +13,7 @@ import { handleInitLightningWallet, handleUnlockLightningWallet, } from '../../../../src/lightning/lightningSignerRoutes'; +import { ExpressApiRouteRequest } from '../../../../src/typedRoutes/api'; describe('Lightning signer routes', () => { let bitgo: TestBitGoAPI; @@ -162,7 +163,7 @@ describe('Lightning signer routes', () => { config: { lightningSignerFileSystemPath: 'lightningSignerFileSystemPath', }, - } as any; + } as unknown as ExpressApiRouteRequest<'express.lightning.getState', 'get'>; await handleGetLightningWalletState(req); From 2940920ecb7926aff0d891d6adbdd170521dfd37 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Wed, 17 Sep 2025 18:15:36 -0400 Subject: [PATCH 3/6] refactor: added response type and some jsdocs TICKET: WP-5447 --- .../src/typedRoutes/api/v2/lightningState.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/modules/express/src/typedRoutes/api/v2/lightningState.ts b/modules/express/src/typedRoutes/api/v2/lightningState.ts index ce4a2cebcb..3f1ea44282 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningState.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningState.ts @@ -13,6 +13,20 @@ export const LightningStateParams = { walletId: t.string, }; +/** + * Response + * - 200: Returns the current Lightning wallet/node state('NON_EXISTING' | 'LOCKED' | 'UNLOCKED' | 'RPC_ACTIVE' | 'SERVER_ACTIVE' | 'WAITING_TO_START'. + * - 400: BitGo Express error payload when the request is invalid (e.g., invalid coin or wallet not a self-custody lightning wallet). + * + * See platform spec: GET /api/v2/{coin}/wallet/{walletId}/state + */ +export const LightningStateResponse = { + 200: t.type({ + state: WalletState, + }), + 400: BitgoExpressError, +} as const; + /** * Lightning - Get node state * @@ -26,10 +40,5 @@ export const GetLightningState = httpRoute({ request: httpRequest({ params: LightningStateParams, }), - response: { - 200: t.type({ - state: WalletState, - }), - 400: BitgoExpressError, - }, + response: LightningStateResponse, }); From 42f9eed8f4c6053184b34a58429f64d828bfc73f Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Thu, 18 Sep 2025 16:25:40 -0400 Subject: [PATCH 4/6] refactor: refactored jsdocs TICKET: WP-5447 --- .../express/src/typedRoutes/api/v2/lightningState.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/express/src/typedRoutes/api/v2/lightningState.ts b/modules/express/src/typedRoutes/api/v2/lightningState.ts index 3f1ea44282..d504b6c26f 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningState.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningState.ts @@ -5,25 +5,23 @@ import { WalletState } from '../../../lightning/codecs'; /** * Path parameters for getting lightning node state - * @property {string} coin - A lightning coin name (e.g., lnbtc or tlnbtc) - * @property {string} walletId - The ID of the lightning self-custody wallet */ export const LightningStateParams = { + /** A lightning coin name (e.g., lnbtc or tlnbtc) */ coin: t.string, + /** The ID of the lightning self-custody wallet */ walletId: t.string, }; /** - * Response - * - 200: Returns the current Lightning wallet/node state('NON_EXISTING' | 'LOCKED' | 'UNLOCKED' | 'RPC_ACTIVE' | 'SERVER_ACTIVE' | 'WAITING_TO_START'. - * - 400: BitGo Express error payload when the request is invalid (e.g., invalid coin or wallet not a self-custody lightning wallet). - * - * See platform spec: GET /api/v2/{coin}/wallet/{walletId}/state + * Response for getting lightning node state */ export const LightningStateResponse = { + /** Current Lightning wallet/node state('NON_EXISTING' | 'LOCKED' | 'UNLOCKED' | 'RPC_ACTIVE' | 'SERVER_ACTIVE' | 'WAITING_TO_START') */ 200: t.type({ state: WalletState, }), + /** BitGo Express error payload when the request is invalid (e.g., invalid coin or wallet not a self-custody lightning wallet). */ 400: BitgoExpressError, } as const; From c6780428931f025f749330ecd4f7be6303ebd1c0 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Fri, 19 Sep 2025 13:35:17 -0400 Subject: [PATCH 5/6] refactor: added decoding test to UT TICKET: WP-5447 --- modules/express/src/typedRoutes/api/v2/lightningState.ts | 8 +++++--- .../unit/clientRoutes/lightning/lightningSignerRoutes.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/express/src/typedRoutes/api/v2/lightningState.ts b/modules/express/src/typedRoutes/api/v2/lightningState.ts index d504b6c26f..63bb7b9830 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningState.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningState.ts @@ -13,14 +13,16 @@ export const LightningStateParams = { walletId: t.string, }; +export const LightningStateResponse200 = t.type({ + state: WalletState, +}); + /** * Response for getting lightning node state */ export const LightningStateResponse = { /** Current Lightning wallet/node state('NON_EXISTING' | 'LOCKED' | 'UNLOCKED' | 'RPC_ACTIVE' | 'SERVER_ACTIVE' | 'WAITING_TO_START') */ - 200: t.type({ - state: WalletState, - }), + 200: LightningStateResponse200, /** BitGo Express error payload when the request is invalid (e.g., invalid coin or wallet not a self-custody lightning wallet). */ 400: BitgoExpressError, } as const; diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 88dc623202..51af14a6aa 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -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'; @@ -14,6 +14,7 @@ import { handleUnlockLightningWallet, } from '../../../../src/lightning/lightningSignerRoutes'; import { ExpressApiRouteRequest } from '../../../../src/typedRoutes/api'; +import { LightningStateResponse } from '../../../../src/typedRoutes/api/v2/lightningState'; describe('Lightning signer routes', () => { let bitgo: TestBitGoAPI; @@ -165,7 +166,10 @@ describe('Lightning signer routes', () => { }, } as unknown as ExpressApiRouteRequest<'express.lightning.getState', 'get'>; - await handleGetLightningWalletState(req); + const res = await handleGetLightningWalletState(req); + decodeOrElse('LightningStateResponse200', LightningStateResponse[200], res, () => { + throw new Error('Response did not match expected codec'); + }); walletStateNock.done(); readFileStub.calledOnceWith('lightningSignerFileSystemPath').should.be.true(); From 02537bdc25f771a5fa1ceccc89685a0a2eb58016 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Mon, 22 Sep 2025 18:16:36 -0400 Subject: [PATCH 6/6] refactor: removed inline import TICKET: WP-5447 --- modules/express/src/lightning/lightningSignerRoutes.ts | 3 ++- modules/express/src/typedRoutes/api/v2/lightningState.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/express/src/lightning/lightningSignerRoutes.ts b/modules/express/src/lightning/lightningSignerRoutes.ts index 39944212a6..54317f6697 100644 --- a/modules/express/src/lightning/lightningSignerRoutes.ts +++ b/modules/express/src/lightning/lightningSignerRoutes.ts @@ -22,6 +22,7 @@ import { } from './codecs'; import { LndSignerClient } from './lndSignerClient'; import { ApiResponseError } from '../errors'; +import { ExpressApiRouteRequest } from '../typedRoutes/api'; type Decrypt = (params: { input: string; password: string }) => string; @@ -188,7 +189,7 @@ export async function handleCreateSignerMacaroon(req: express.Request): Promise< * Handle the request to get the state of a wallet from the signer. */ export async function handleGetLightningWalletState( - req: import('../typedRoutes/api').ExpressApiRouteRequest<'express.lightning.getState', 'get'> + req: ExpressApiRouteRequest<'express.lightning.getState', 'get'> ): Promise { const { coin: coinName, walletId } = req.decoded; if (!isLightningCoinName(coinName)) { diff --git a/modules/express/src/typedRoutes/api/v2/lightningState.ts b/modules/express/src/typedRoutes/api/v2/lightningState.ts index 63bb7b9830..5c38159052 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningState.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningState.ts @@ -11,7 +11,7 @@ export const LightningStateParams = { coin: t.string, /** The ID of the lightning self-custody wallet */ walletId: t.string, -}; +} as const; export const LightningStateResponse200 = t.type({ state: WalletState,