Skip to content

Commit

Permalink
feat: add express endpoints for lightning invoice
Browse files Browse the repository at this point in the history
Add
- list invoices
- update wallet coin specific

Ticket: BTC-1836
  • Loading branch information
007harshmahajan committed Feb 27, 2025
1 parent 18b9716 commit 8d99a14
Show file tree
Hide file tree
Showing 3 changed files with 242 additions and 0 deletions.
15 changes: 15 additions & 0 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ import {
handleUnlockLightningWallet,
} from './lightning/lightningSignerRoutes';
import { handleCreateLightningInvoice, handlePayLightningInvoice } from './lightning/lightningInvoiceRoutes';
import {
handleListLightningInvoices,
handleUpdateLightningWalletCoinSpecific,
} from './lightning/lightningWalletRoutes';
import { ProxyAgent } from 'proxy-agent';

const { version } = require('bitgo/package.json');
Expand Down Expand Up @@ -1717,4 +1721,15 @@ export function setupLightningRoutes(app: express.Application, config: Config):
prepareBitGo(config),
promiseWrapper(handlePayLightningInvoice)
);
app.get(
'/api/v2/:coin/wallet/:id/lightning/invoices',
prepareBitGo(config),
promiseWrapper(handleListLightningInvoices)
);
app.put(
'/api/v2/:coin/wallet/:id/coinspecific',
parseBody,
prepareBitGo(config),
promiseWrapper(handleUpdateLightningWalletCoinSpecific)
);
}
41 changes: 41 additions & 0 deletions modules/express/src/lightning/lightningWalletRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as express from 'express';
import { ApiResponseError } from '../errors';
import { getLightningWallet, InvoiceQuery, UpdateLightningWalletSignedRequest } from '@bitgo/abstract-lightning';
import { decodeOrElse } from '@bitgo/sdk-core';
import * as t from 'io-ts';

const UpdateWalletRequest = t.intersection([
UpdateLightningWalletSignedRequest,
t.type({
passphrase: t.string,
}),
]);

export async function handleListLightningInvoices(req: express.Request): Promise<unknown> {
const bitgo = req.bitgo;

const params = decodeOrElse(InvoiceQuery.name, InvoiceQuery, req.query, (error) => {
throw new ApiResponseError(`Invalid query parameters for listing lightning invoices: ${error}`, 400);
});

const coin = bitgo.coin(req.params.coin);
const wallet = await coin.wallets().get({ id: req.params.id });
const lightningWallet = getLightningWallet(wallet);

return await lightningWallet.listInvoices(params);
}

export async function handleUpdateLightningWalletCoinSpecific(req: express.Request): Promise<unknown> {
const bitgo = req.bitgo;

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

const coin = bitgo.coin(req.params.coin);
const wallet = await coin.wallets().get({ id: req.params.id });
const lightningWallet = getLightningWallet(wallet);

return await lightningWallet.updateWalletCoinSpecific(params, passphrase);
}
186 changes: 186 additions & 0 deletions modules/express/test/unit/lightning/lightningWalletRoutes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import * as sinon from 'sinon';
import * as should from 'should';
import * as express from 'express';
import {
handleListLightningInvoices,
handleUpdateLightningWalletCoinSpecific,
} from '../../../src/lightning/lightningWalletRoutes';
import { BitGo } from 'bitgo';
import { InvoiceInfo } from '@bitgo/abstract-lightning';

describe('Lightning Wallet Routes', () => {
let bitgo;
const coin = 'tlnbtc';

const mockRequestObject = (params: { body?: any; params?: any; query?: any; bitgo?: any }) => {
const req: Partial<express.Request> = {};
req.body = params.body || {};
req.params = params.params || {};
req.query = params.query || {};
req.bitgo = params.bitgo;
return req as express.Request;
};

afterEach(() => {
sinon.restore();
});

describe('List Lightning Invoices', () => {
it('should successfully list lightning invoices', async () => {
const queryParams = {
status: 'open',
limit: '10',
startDate: '2024-01-01',
endDate: '2024-01-31',
};

const mockResponse: InvoiceInfo[] = [
{
invoice: 'lntb100u1p3h2jk3pp5...',
paymentHash: 'abc123',
valueMsat: 10000n,
walletId: 'testWalletId',
status: 'open',
expiresAt: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
},
];

const listInvoicesSpy = sinon.stub().resolves(mockResponse);
const mockLightningWallet = {
listInvoices: listInvoicesSpy,
};

const proxyquire = require('proxyquire');
const lightningRoutes = proxyquire('../../../src/lightning/lightningWalletRoutes', {
'@bitgo/abstract-lightning': {
getLightningWallet: () => mockLightningWallet,
},
});

const walletStub = {};
const coinStub = {
wallets: () => ({ get: sinon.stub().resolves(walletStub) }),
};
const stubBitgo = sinon.createStubInstance(BitGo as any, { coin: coinStub });

const req = mockRequestObject({
params: { id: 'testWalletId', coin },
query: queryParams,
bitgo: stubBitgo,
});

const result = await lightningRoutes.handleListLightningInvoices(req);

should(result).deepEqual(mockResponse);
should(listInvoicesSpy).be.calledOnce();
const [firstArg] = listInvoicesSpy.getCall(0).args;

should(firstArg).have.property('status', 'open');
should(firstArg).have.property('limit', 10n);
should(firstArg.startDate).be.instanceOf(Date);
should(firstArg.endDate).be.instanceOf(Date);
});

it('should handle invalid query parameters', async () => {
const invalidQuery = {
status: 'invalidStatus',
limit: 'notANumber',
};

const req = mockRequestObject({
params: { id: 'testWalletId', coin },
query: invalidQuery,
});
req.bitgo = bitgo;

await should(handleListLightningInvoices(req)).be.rejectedWith(
/Invalid query parameters for listing lightning invoices: Invalid value '"invalidStatus"' supplied to InvoiceQuery\.status\.0, expected "open"/
);
});
});

describe('Update Wallet Coin Specific', () => {
it('should successfully update wallet coin specific data', async () => {
const inputParams = {
encryptedSignerMacaroon: 'encrypted-macaroon-data',
signerHost: 'signer.example.com',
passphrase: 'wallet-password-123',
};

const expectedResponse = {
coinSpecific: {
updated: true,
},
};

const updateStub = sinon.stub().resolves(expectedResponse);
const mockLightningWallet = {
updateWalletCoinSpecific: updateStub,
};

const proxyquire = require('proxyquire');
const lightningRoutes = proxyquire('../../../src/lightning/lightningWalletRoutes', {
'@bitgo/abstract-lightning': {
getLightningWallet: () => mockLightningWallet,
},
});

const walletStub = {};
const coinStub = {
wallets: () => ({ get: sinon.stub().resolves(walletStub) }),
};
const stubBitgo = sinon.createStubInstance(BitGo as any, { coin: coinStub });

const req = mockRequestObject({
params: { id: 'testWalletId', coin },
body: inputParams,
bitgo: stubBitgo,
});

const result = await lightningRoutes.handleUpdateLightningWalletCoinSpecific(req);

should(result).deepEqual(expectedResponse);
should(updateStub).be.calledOnce();
const [firstArg, secondArg] = updateStub.getCall(0).args;
should(firstArg).have.property('encryptedSignerMacaroon', 'encrypted-macaroon-data');
should(firstArg).have.property('signerHost', 'signer.example.com');
should(secondArg).equal('wallet-password-123');
});

it('should throw error when passphrase is missing', async () => {
const invalidParams = {
encryptedSignerMacaroon: 'encrypted-data',
signerHost: 'signer.example.com',
};

const req = mockRequestObject({
params: { id: 'testWalletId', coin },
body: invalidParams,
});
req.bitgo = bitgo;

await should(handleUpdateLightningWalletCoinSpecific(req)).be.rejectedWith(
'Invalid request body to update lightning wallet coin specific'
);
});

it('should handle invalid request body', async () => {
const invalidParams = {
signerHost: 12345, // invalid type
passphrase: 'valid-pass',
};

const req = mockRequestObject({
params: { id: 'testWalletId', coin },
body: invalidParams,
});
req.bitgo = bitgo;

await should(handleUpdateLightningWalletCoinSpecific(req)).be.rejectedWith(
'Invalid request body to update lightning wallet coin specific'
);
});
});
});

0 comments on commit 8d99a14

Please sign in to comment.