From 685ae3c60677a84ef6598b46bfb5d3957e5cf28e Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Thu, 11 Sep 2025 11:15:12 -0400 Subject: [PATCH 1/8] feat(express): migrate init wallet to typed routes TICKET: WP-5444 --- modules/express/src/clientRoutes.ts | 7 +---- .../src/lightning/lightningSignerRoutes.ts | 29 ++++--------------- modules/express/src/typedRoutes/api/index.ts | 4 +++ .../lightning/lightningSignerRoutes.ts | 8 ++++- .../express/test/unit/typedRoutes/decode.ts | 22 ++++++++++++++ 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 362969f4f6..cc32414f4b 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -1566,6 +1566,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, @@ -1790,12 +1791,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, diff --git a/modules/express/src/lightning/lightningSignerRoutes.ts b/modules/express/src/lightning/lightningSignerRoutes.ts index 7c397fdf33..15251fc7b0 100644 --- a/modules/express/src/lightning/lightningSignerRoutes.ts +++ b/modules/express/src/lightning/lightningSignerRoutes.ts @@ -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; @@ -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 { +export async function handleInitLightningWallet( + req: ExpressApiRouteRequest<'express.lightning.initWallet', 'post'> +): Promise { const bitgo = req.bitgo; - const coinName = req.params.coin; + const { coin: coinName, walletId, passphrase, expressHost } = req.decoded as any; 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); diff --git a/modules/express/src/typedRoutes/api/index.ts b/modules/express/src/typedRoutes/api/index.ts index 4a45375e0d..9ede8f02b5 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 { PostLightningInitWallet } from './v2/lightningInitWallet'; export const ExpressApi = apiSpec({ 'express.ping': { @@ -44,6 +45,9 @@ export const ExpressApi = apiSpec({ 'express.v1.wallet.signTransaction': { post: PostSignTransaction, }, + 'express.lightning.initWallet': { + post: PostLightningInitWallet, + }, }); export type ExpressApi = typeof ExpressApi; diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 2f0c5dc2d9..604ab0e7b9 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -74,7 +74,13 @@ 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 express.Request & { decoded: any }; await handleInitLightningWallet(req); diff --git a/modules/express/test/unit/typedRoutes/decode.ts b/modules/express/test/unit/typedRoutes/decode.ts index 57641f6338..5fb261ff55 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -5,6 +5,10 @@ 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 { + LightningInitWalletBody, + LightningInitWalletParams, +} from '../../../src/typedRoutes/api/v2/lightningInitWallet'; export function assertDecode(codec: t.Type, input: unknown): T { const result = codec.decode(input); @@ -100,4 +104,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' }); + }); }); From 9352c727f31e636312e7b527290a4dfa97f6eaf2 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Thu, 11 Sep 2025 11:42:03 -0400 Subject: [PATCH 2/8] refactor: adding missed changes TICKET: WP-5444 --- .../typedRoutes/api/v2/lightningInitWallet.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts diff --git a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts new file mode 100644 index 0000000000..40505ba7cb --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts @@ -0,0 +1,31 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; +import { BitgoExpressError } from '../../schemas/error'; + +// Params: coin (lightning coin), walletId path parameter +export const LightningInitWalletParams = { + coin: t.string, + walletId: t.string, +}; + +// Body shape (required + optional). We intentionally do NOT reuse the composite +// InitLightningWalletRequest intersection codec here because httpRequest expects +// a plain object mapping of field -> codec, not an io-ts Type instance. +export const LightningInitWalletBody = { + passphrase: t.string, + expressHost: optional(t.string), +}; + +/** + * Lightning - Initialize node + * @operationId express.lightning.initWallet + * POST /api/v2/{coin}/wallet/{walletId}/initwallet + */ +export const PostLightningInitWallet = httpRoute({ + path: '/api/v2/:coin/wallet/:walletId/initwallet', + method: 'POST', + request: httpRequest({ params: LightningInitWalletParams, body: LightningInitWalletBody }), + response: { 200: t.unknown, 400: BitgoExpressError }, +}); + +export type PostLightningInitWallet = typeof PostLightningInitWallet; From 1b717f3566fdf99f44a4224e9469dc1b1dbfbba9 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Thu, 11 Sep 2025 11:53:57 -0400 Subject: [PATCH 3/8] docs: updated documentation TICKET: WP-5444 --- .../src/typedRoutes/api/v2/lightningInitWallet.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts index 40505ba7cb..75d50e3bf6 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts @@ -2,15 +2,18 @@ import * as t from 'io-ts'; import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; import { BitgoExpressError } from '../../schemas/error'; -// Params: coin (lightning coin), walletId path parameter +/** + * + */ export const LightningInitWalletParams = { coin: t.string, walletId: t.string, }; -// Body shape (required + optional). We intentionally do NOT reuse the composite -// InitLightningWalletRequest intersection codec here because httpRequest expects -// a plain object mapping of field -> codec, not an io-ts Type instance. +/** + * Request body for initializing a Lightning wallet + * @property passphrase - Passphrase to encrypt the admin macaroon of the signer node. + */ export const LightningInitWalletBody = { passphrase: t.string, expressHost: optional(t.string), From f14ae3f32c553af11f25ab32976937c780ad7b28 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Thu, 11 Sep 2025 17:04:33 -0400 Subject: [PATCH 4/8] refactor: updated jsdocs and removed unnecessary casting TICKET: WP-5444 --- modules/express/src/lightning/lightningSignerRoutes.ts | 2 +- .../src/typedRoutes/api/v2/lightningInitWallet.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/express/src/lightning/lightningSignerRoutes.ts b/modules/express/src/lightning/lightningSignerRoutes.ts index 15251fc7b0..be90fa7630 100644 --- a/modules/express/src/lightning/lightningSignerRoutes.ts +++ b/modules/express/src/lightning/lightningSignerRoutes.ts @@ -61,7 +61,7 @@ export async function handleInitLightningWallet( req: ExpressApiRouteRequest<'express.lightning.initWallet', 'post'> ): Promise { const bitgo = req.bitgo; - const { coin: coinName, walletId, passphrase, expressHost } = req.decoded as any; + const { coin: coinName, walletId, passphrase, expressHost } = req.decoded; if (!isLightningCoinName(coinName)) { throw new ApiResponseError(`Invalid coin ${coinName}. This is not a lightning coin.`, 400); } diff --git a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts index 75d50e3bf6..4e5c0f42bf 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts @@ -3,7 +3,9 @@ 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). + * @property {string} walletId - The ID of the wallet. */ export const LightningInitWalletParams = { coin: t.string, @@ -20,8 +22,11 @@ export const LightningInitWalletBody = { }; /** - * Lightning - Initialize node + * 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 + * * POST /api/v2/{coin}/wallet/{walletId}/initwallet */ export const PostLightningInitWallet = httpRoute({ From 8d928e9c87313ca6ac8aed6758614d05c7adc1b1 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Wed, 17 Sep 2025 15:27:12 -0400 Subject: [PATCH 5/8] refactor: added unit tests that test encoding and decoding from end to end also fixed other minor errors TICKET: WP-5444 --- .../typedRoutes/api/v2/lightningInitWallet.ts | 18 ++- .../lightning/lightningSignerRoutes.ts | 2 +- .../express/test/unit/typedRoutes/decode.ts | 134 ++++++++++++++++++ 3 files changed, 149 insertions(+), 5 deletions(-) diff --git a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts index 4e5c0f42bf..3f2a890aae 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts @@ -4,13 +4,13 @@ import { BitgoExpressError } from '../../schemas/error'; /** * Path parameters for initializing a Lightning wallet - * @property {string} coin - A lightning coin name (e.g, lnbtc). + * @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 @@ -19,7 +19,17 @@ export const LightningInitWalletParams = { export const LightningInitWalletBody = { passphrase: t.string, expressHost: optional(t.string), -}; +} as const; + +/** + * Response + * - 200: Returns the updated wallet. On success, the wallet's `coinSpecific` will include the encrypted admin macaroon for the Lightning signer node. + * - 400: BitGo Express error payload when initialization cannot proceed (for example: invalid coin, unsupported environment, wallet not in an initializable state). + */ +export const LightningInitWalletResponse = { + 200: t.unknown, + 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. @@ -33,7 +43,7 @@ export const PostLightningInitWallet = httpRoute({ path: '/api/v2/:coin/wallet/:walletId/initwallet', method: 'POST', request: httpRequest({ params: LightningInitWalletParams, body: LightningInitWalletBody }), - response: { 200: t.unknown, 400: BitgoExpressError }, + response: LightningInitWalletResponse, }); export type PostLightningInitWallet = typeof PostLightningInitWallet; diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 604ab0e7b9..39050c47a7 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -80,7 +80,7 @@ describe('Lightning signer routes', () => { passphrase: apiData.initWalletRequestBody.passphrase, ...(includingOptionalFields ? { expressHost: apiData.initWalletRequestBody.expressHost } : {}), }, - } as unknown as express.Request & { decoded: any }; + } as unknown as ExpressApiRouteRequest<'express.lightning.initWallet'>; await handleInitLightningWallet(req); diff --git a/modules/express/test/unit/typedRoutes/decode.ts b/modules/express/test/unit/typedRoutes/decode.ts index 5fb261ff55..a1acdf0bf9 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -9,6 +9,12 @@ import { LightningInitWalletBody, LightningInitWalletParams, } from '../../../src/typedRoutes/api/v2/lightningInitWallet'; +import { agent as supertest } from 'supertest'; +import nock from 'nock'; +import { DefaultConfig } from '../../../src/config'; +import { app as expressApp } from '../../../src/expressApp'; +import * as sinon from 'sinon'; +import { BitGo } from 'bitgo'; export function assertDecode(codec: t.Type, input: unknown): T { const result = codec.decode(input); @@ -123,3 +129,131 @@ describe('io-ts decode tests', function () { assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 'host.example' }); }); }); + +describe('e2e tests for io-ts routes', function () { + let agent; + let authenticateStub: sinon.SinonStub; + let walletsStub: sinon.SinonStub; + let decryptStub: sinon.SinonStub; + let encryptStub: sinon.SinonStub; + let verifyAddressStub: sinon.SinonStub; + before(function () { + nock.restore(); + + const args = { + ...DefaultConfig, + debug: false, + env: 'test' as const, + logfile: '/dev/null', + }; + + const app = expressApp(args); + agent = supertest(app); + + authenticateStub = sinon.stub(BitGo.prototype, 'authenticate').callsFake(async (body: any) => { + return { + email: `${body.username}@example.com`, + password: body.password, + forceSMS: false, + } as any; + }); + encryptStub = sinon.stub(BitGo.prototype, 'encrypt').callsFake((params: any) => `enc:${params.input}`); + decryptStub = sinon.stub(BitGo.prototype, 'decrypt').callsFake((params: any) => { + const inputStr = String(params.input); + return inputStr.startsWith('enc:') ? inputStr.substring(4) : inputStr; + }); + verifyAddressStub = sinon.stub(BitGo.prototype, 'verifyAddress').callsFake((_params: any) => true); + walletsStub = sinon.stub(BitGo.prototype, 'wallets').callsFake(() => { + return { + createWalletWithKeychains: async () => ({ + wallet: 'wallet-id-123', + userKeychain: 'user-keychain', + backupKeychain: 'backup-keychain', + }), + }; + }); + }); + beforeEach(function () { + authenticateStub.resetHistory(); + walletsStub.resetHistory(); + decryptStub.resetHistory(); + encryptStub.resetHistory(); + verifyAddressStub.resetHistory(); + }); + after(function () { + authenticateStub.restore(); + walletsStub.restore(); + decryptStub.restore(); + encryptStub.restore(); + verifyAddressStub.restore(); + }); + + it('POST /api/v2/user/login success', async function () { + const res = await agent.post('/api/v2/user/login').send({ username: 'alice', password: 'pw' }); + res.status.should.equal(200); + res.body.email.should.equal('alice@example.com'); + res.body.password.should.equal('pw'); + sinon.assert.calledOnce(authenticateStub); + sinon.assert.calledWithMatch(authenticateStub, { username: 'alice', password: 'pw' }); + }); + + it('POST /api/v2/user/login decode failure', async function () { + const res = await agent.post('/api/v2/user/login').send({ username: 'alice' }); + res.status.should.equal(400); + sinon.assert.notCalled(authenticateStub); + }); + + it('POST /api/v2/encrypt success', async function () { + const res = await agent.post('/api/v2/encrypt').send({ input: 'hello' }); + res.status.should.equal(200); + res.body.encrypted.should.equal('enc:hello'); + sinon.assert.calledOnce(encryptStub); + sinon.assert.calledWithMatch(encryptStub, { input: 'hello' }); + }); + + it('POST /api/v2/encrypt decode failure', async function () { + const res = await agent.post('/api/v2/encrypt').send({ input: 123 }); + res.status.should.equal(400); + sinon.assert.notCalled(encryptStub); + }); + + it('POST /api/v2/decrypt success', async function () { + const res = await agent.post('/api/v2/decrypt').send({ input: 'enc:secret', password: 'pw' }); + res.status.should.equal(200); + res.body.decrypted.should.equal('secret'); + sinon.assert.calledOnce(decryptStub); + sinon.assert.calledWithMatch(decryptStub, { input: 'enc:secret', password: 'pw' }); + }); + + it('POST /api/v2/decrypt decode failure', async function () { + const res = await agent.post('/api/v2/decrypt').send({ password: 'pw' }); + res.status.should.equal(400); + sinon.assert.notCalled(decryptStub); + }); + + it('POST /api/v2/verifyaddress success', async function () { + const res = await agent.post('/api/v2/verifyaddress').send({ address: 'addr1' }); + res.status.should.equal(200); + res.body.should.have.property('verified', true); + sinon.assert.calledOnce(verifyAddressStub); + }); + + it('POST /api/v2/verifyaddress decode failure - invalid address', async function () { + const res = await agent.post('/api/v2/verifyaddress').send({ address: 42 }); + res.status.should.equal(400); + sinon.assert.notCalled(verifyAddressStub); + }); + + it('POST /api/v1/wallets/simplecreate success', async function () { + const res = await agent.post('/api/v1/wallets/simplecreate').send({ passphrase: 'pass' }); + res.status.should.equal(200); + res.body.wallet.should.equal('wallet-id-123'); + sinon.assert.calledOnce(walletsStub); + }); + + it('POST /api/v1/wallets/simplecreate decode - missing passphrase', async function () { + const res = await agent.post('/api/v1/wallets/simplecreate').send({}); + res.status.should.equal(400); + sinon.assert.notCalled(walletsStub); + }); +}); From 86d385f1cf257473c3cdca385e65a29c4ea31f93 Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Wed, 17 Sep 2025 15:42:10 -0400 Subject: [PATCH 6/8] refactor: fixed import TICKET: WP-5444 --- .../test/unit/clientRoutes/lightning/lightningSignerRoutes.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 39050c47a7..655d28f6f3 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; From dd93ddf0055e4b4daae2701b3c158bba9555cacd Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Wed, 17 Sep 2025 16:03:18 -0400 Subject: [PATCH 7/8] refactor: fixed code TICKET: WP-5444 --- .../test/unit/clientRoutes/lightning/lightningSignerRoutes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 655d28f6f3..75c0d90805 100644 --- a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts +++ b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts @@ -81,7 +81,7 @@ describe('Lightning signer routes', () => { passphrase: apiData.initWalletRequestBody.passphrase, ...(includingOptionalFields ? { expressHost: apiData.initWalletRequestBody.expressHost } : {}), }, - } as unknown as ExpressApiRouteRequest<'express.lightning.initWallet'>; + } as unknown as ExpressApiRouteRequest<'express.lightning.initWallet', 'post'>; await handleInitLightningWallet(req); From c0b322c32e212711af4fb720f74baf3cde34087e Mon Sep 17 00:00:00 2001 From: Daniel Zhao Date: Thu, 18 Sep 2025 16:21:28 -0400 Subject: [PATCH 8/8] refactor: removed unneccessary UTs and modified existing ones TICKET: WP-5444 --- .../typedRoutes/api/v2/lightningInitWallet.ts | 11 +- .../lightning/lightningSignerRoutes.ts | 8 +- .../express/test/unit/typedRoutes/decode.ts | 134 ------------------ 3 files changed, 11 insertions(+), 142 deletions(-) diff --git a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts index 3f2a890aae..bd40c98c67 100644 --- a/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts +++ b/modules/express/src/typedRoutes/api/v2/lightningInitWallet.ts @@ -14,20 +14,21 @@ export const LightningInitWalletParams = { /** * Request body for initializing a Lightning wallet - * @property passphrase - Passphrase to encrypt the admin macaroon of the signer node. */ 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 - * - 200: Returns the updated wallet. On success, the wallet's `coinSpecific` will include the encrypted admin macaroon for the Lightning signer node. - * - 400: BitGo Express error payload when initialization cannot proceed (for example: invalid coin, unsupported environment, wallet not in an initializable state). + * 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; @@ -36,8 +37,6 @@ export const LightningInitWalletResponse = { * Returns the updated wallet with the encrypted admin macaroon in the `coinSpecific` response field. * * @operationId express.lightning.initWallet - * - * POST /api/v2/{coin}/wallet/{walletId}/initwallet */ export const PostLightningInitWallet = httpRoute({ path: '/api/v2/:coin/wallet/:walletId/initwallet', diff --git a/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts b/modules/express/test/unit/clientRoutes/lightning/lightningSignerRoutes.ts index 75c0d90805..5a9ed3483c 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 { PostLightningInitWallet } from '../../../../src/typedRoutes/api/v2/lightningInitWallet'; describe('Lightning signer routes', () => { let bitgo: TestBitGoAPI; @@ -83,7 +84,10 @@ describe('Lightning signer routes', () => { }, } 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(); diff --git a/modules/express/test/unit/typedRoutes/decode.ts b/modules/express/test/unit/typedRoutes/decode.ts index 21f1e9ed0e..7d6a81bf5d 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -10,12 +10,6 @@ import { LightningInitWalletBody, LightningInitWalletParams, } from '../../../src/typedRoutes/api/v2/lightningInitWallet'; -import { agent as supertest } from 'supertest'; -import nock from 'nock'; -import { DefaultConfig } from '../../../src/config'; -import { app as expressApp } from '../../../src/expressApp'; -import * as sinon from 'sinon'; -import { BitGo } from 'bitgo'; export function assertDecode(codec: t.Type, input: unknown): T { const result = codec.decode(input); @@ -158,131 +152,3 @@ describe('io-ts decode tests', function () { assertDecode(t.type(LightningInitWalletBody), { passphrase: 'p', expressHost: 'host.example' }); }); }); - -describe('e2e tests for io-ts routes', function () { - let agent; - let authenticateStub: sinon.SinonStub; - let walletsStub: sinon.SinonStub; - let decryptStub: sinon.SinonStub; - let encryptStub: sinon.SinonStub; - let verifyAddressStub: sinon.SinonStub; - before(function () { - nock.restore(); - - const args = { - ...DefaultConfig, - debug: false, - env: 'test' as const, - logfile: '/dev/null', - }; - - const app = expressApp(args); - agent = supertest(app); - - authenticateStub = sinon.stub(BitGo.prototype, 'authenticate').callsFake(async (body: any) => { - return { - email: `${body.username}@example.com`, - password: body.password, - forceSMS: false, - } as any; - }); - encryptStub = sinon.stub(BitGo.prototype, 'encrypt').callsFake((params: any) => `enc:${params.input}`); - decryptStub = sinon.stub(BitGo.prototype, 'decrypt').callsFake((params: any) => { - const inputStr = String(params.input); - return inputStr.startsWith('enc:') ? inputStr.substring(4) : inputStr; - }); - verifyAddressStub = sinon.stub(BitGo.prototype, 'verifyAddress').callsFake((_params: any) => true); - walletsStub = sinon.stub(BitGo.prototype, 'wallets').callsFake(() => { - return { - createWalletWithKeychains: async () => ({ - wallet: 'wallet-id-123', - userKeychain: 'user-keychain', - backupKeychain: 'backup-keychain', - }), - }; - }); - }); - beforeEach(function () { - authenticateStub.resetHistory(); - walletsStub.resetHistory(); - decryptStub.resetHistory(); - encryptStub.resetHistory(); - verifyAddressStub.resetHistory(); - }); - after(function () { - authenticateStub.restore(); - walletsStub.restore(); - decryptStub.restore(); - encryptStub.restore(); - verifyAddressStub.restore(); - }); - - it('POST /api/v2/user/login success', async function () { - const res = await agent.post('/api/v2/user/login').send({ username: 'alice', password: 'pw' }); - res.status.should.equal(200); - res.body.email.should.equal('alice@example.com'); - res.body.password.should.equal('pw'); - sinon.assert.calledOnce(authenticateStub); - sinon.assert.calledWithMatch(authenticateStub, { username: 'alice', password: 'pw' }); - }); - - it('POST /api/v2/user/login decode failure', async function () { - const res = await agent.post('/api/v2/user/login').send({ username: 'alice' }); - res.status.should.equal(400); - sinon.assert.notCalled(authenticateStub); - }); - - it('POST /api/v2/encrypt success', async function () { - const res = await agent.post('/api/v2/encrypt').send({ input: 'hello' }); - res.status.should.equal(200); - res.body.encrypted.should.equal('enc:hello'); - sinon.assert.calledOnce(encryptStub); - sinon.assert.calledWithMatch(encryptStub, { input: 'hello' }); - }); - - it('POST /api/v2/encrypt decode failure', async function () { - const res = await agent.post('/api/v2/encrypt').send({ input: 123 }); - res.status.should.equal(400); - sinon.assert.notCalled(encryptStub); - }); - - it('POST /api/v2/decrypt success', async function () { - const res = await agent.post('/api/v2/decrypt').send({ input: 'enc:secret', password: 'pw' }); - res.status.should.equal(200); - res.body.decrypted.should.equal('secret'); - sinon.assert.calledOnce(decryptStub); - sinon.assert.calledWithMatch(decryptStub, { input: 'enc:secret', password: 'pw' }); - }); - - it('POST /api/v2/decrypt decode failure', async function () { - const res = await agent.post('/api/v2/decrypt').send({ password: 'pw' }); - res.status.should.equal(400); - sinon.assert.notCalled(decryptStub); - }); - - it('POST /api/v2/verifyaddress success', async function () { - const res = await agent.post('/api/v2/verifyaddress').send({ address: 'addr1' }); - res.status.should.equal(200); - res.body.should.have.property('verified', true); - sinon.assert.calledOnce(verifyAddressStub); - }); - - it('POST /api/v2/verifyaddress decode failure - invalid address', async function () { - const res = await agent.post('/api/v2/verifyaddress').send({ address: 42 }); - res.status.should.equal(400); - sinon.assert.notCalled(verifyAddressStub); - }); - - it('POST /api/v1/wallets/simplecreate success', async function () { - const res = await agent.post('/api/v1/wallets/simplecreate').send({ passphrase: 'pass' }); - res.status.should.equal(200); - res.body.wallet.should.equal('wallet-id-123'); - sinon.assert.calledOnce(walletsStub); - }); - - it('POST /api/v1/wallets/simplecreate decode - missing passphrase', async function () { - const res = await agent.post('/api/v1/wallets/simplecreate').send({}); - res.status.should.equal(400); - sinon.assert.notCalled(walletsStub); - }); -});