Skip to content
2 changes: 1 addition & 1 deletion modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1586,6 +1586,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)]);
Expand Down Expand Up @@ -1787,5 +1788,4 @@ export function setupLightningSignerNodeRoutes(app: express.Application, config:
prepareBitGo(config),
promiseWrapper(handleCreateSignerMacaroon)
);
app.get('/api/v2/:coin/wallet/:id/state', prepareBitGo(config), promiseWrapper(handleGetLightningWalletState));
}
10 changes: 4 additions & 6 deletions modules/express/src/lightning/lightningSignerRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,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<GetWalletStateResponse> {
const coinName = req.params.coin;
export async function handleGetLightningWalletState(
req: ExpressApiRouteRequest<'express.lightning.getState', 'get'>
): Promise<GetWalletStateResponse> {
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();
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 @@ -14,6 +14,7 @@ import { PostSimpleCreate } from './v1/simpleCreate';
import { PutPendingApproval } from './v1/pendingApproval';
import { PostSignTransaction } from './v1/signTransaction';
import { PostKeychainLocal } from './v2/keychainLocal';
import { GetLightningState } from './v2/lightningState';
import { PostLightningInitWallet } from './v2/lightningInitWallet';
import { PostUnlockLightningWallet } from './v2/unlockWallet';
import { PostVerifyCoinAddress } from './v2/verifyAddress';
Expand Down Expand Up @@ -53,6 +54,9 @@ export const ExpressApi = apiSpec({
'express.keychain.local': {
post: PostKeychainLocal,
},
'express.lightning.getState': {
get: GetLightningState,
},
'express.lightning.initWallet': {
post: PostLightningInitWallet,
},
Expand Down
44 changes: 44 additions & 0 deletions modules/express/src/typedRoutes/api/v2/lightningState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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
*/
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,
} as const;

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: 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;

/**
* 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: LightningStateResponse,
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '../../../../src/lightning/lightningSignerRoutes';
import { ExpressApiRouteRequest } from '../../../../src/typedRoutes/api';
import { PostLightningInitWallet } from '../../../../src/typedRoutes/api/v2/lightningInitWallet';
import { LightningStateResponse } from '../../../../src/typedRoutes/api/v2/lightningState';

describe('Lightning signer routes', () => {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -165,13 +166,21 @@ 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 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();
Expand Down
7 changes: 7 additions & 0 deletions modules/express/test/unit/typedRoutes/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { VerifyAddressBody } from '../../../src/typedRoutes/api/common/verifyAdd
import { VerifyAddressV2Body, VerifyAddressV2Params } from '../../../src/typedRoutes/api/v2/verifyAddress';
import { SimpleCreateRequestBody } from '../../../src/typedRoutes/api/v1/simpleCreate';
import { KeychainLocalRequestParams } from '../../../src/typedRoutes/api/v2/keychainLocal';
import { LightningStateParams } from '../../../src/typedRoutes/api/v2/lightningState';
import {
LightningInitWalletBody,
LightningInitWalletParams,
Expand Down Expand Up @@ -157,6 +158,12 @@ describe('io-ts decode tests', function () {
coin: 'tbtc',
});
});
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' }));
});
it('express.lightning.initWallet params', function () {
// missing walletId
assert.throws(() => assertDecode(t.type(LightningInitWalletParams), { coin: 'ltc' }));
Expand Down