Skip to content

Commit 9a177c9

Browse files
authored
Merge pull request #6113 from BitGo/BTC-2110-invoice-payment-pagination
feat: added pagination in invoice
2 parents 942eb42 + 2389a6d commit 9a177c9

File tree

6 files changed

+190
-14
lines changed

6 files changed

+190
-14
lines changed

examples/ts/btc/lightning/list-invoices.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async function main(): Promise<void> {
5858
status,
5959
limit: limit ? BigInt(limit) : undefined,
6060
};
61-
const invoices = await lightning.listInvoices(query);
61+
const { invoices } = await lightning.listInvoices(query);
6262

6363
// Display invoice summary
6464
console.log(`\nFound ${invoices.length} invoices:`);

examples/ts/btc/lightning/list-payments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async function main(): Promise<void> {
5454
}
5555

5656
// List payments with the provided filters
57-
const payments = await lightning.listPayments(queryParams);
57+
const { payments } = await lightning.listPayments(queryParams);
5858

5959
// Display payment summary
6060
console.log(`\nFound ${payments.length} payments:`);

modules/abstract-lightning/src/codecs/api/invoice.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,29 @@ export const InvoiceInfo = t.intersection(
8282
);
8383
export type InvoiceInfo = t.TypeOf<typeof InvoiceInfo>;
8484

85+
export const ListInvoicesResponse = t.intersection(
86+
[
87+
t.type({
88+
invoices: t.array(InvoiceInfo),
89+
}),
90+
t.partial({
91+
/**
92+
* This is the paymentHash of the last Invoice in the last iteration.
93+
* Providing this value as the prevId in the next request will return the next batch of invoices.
94+
* */
95+
nextBatchPrevId: t.string,
96+
}),
97+
],
98+
'ListInvoicesResponse'
99+
);
100+
export type ListInvoicesResponse = t.TypeOf<typeof ListInvoicesResponse>;
101+
85102
export const InvoiceQuery = t.partial(
86103
{
87104
status: InvoiceStatus,
88105
limit: BigIntFromString,
106+
/** paymentHash provided by nextBatchPrevId in the previous list */
107+
prevId: t.string,
89108
startDate: DateFromISOString,
90109
endDate: DateFromISOString,
91110
},

modules/abstract-lightning/src/codecs/api/payment.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ export const PaymentInfo = t.intersection(
7474

7575
export type PaymentInfo = t.TypeOf<typeof PaymentInfo>;
7676

77+
export const ListPaymentsResponse = t.intersection(
78+
[
79+
t.type({
80+
payments: t.array(PaymentInfo),
81+
}),
82+
t.partial({
83+
/**
84+
* This is the paymentHash of the last Payment in the last iteration.
85+
* Providing this value as the prevId in the next request will return the next batch of payments.
86+
* */
87+
nextBatchPrevId: t.string,
88+
}),
89+
],
90+
'ListPaymentsResponse'
91+
);
92+
export type ListPaymentsResponse = t.TypeOf<typeof ListPaymentsResponse>;
93+
7794
/**
7895
* Payment query parameters
7996
*/
@@ -83,6 +100,8 @@ export const PaymentQuery = t.partial(
83100
limit: BigIntFromString,
84101
startDate: DateFromISOString,
85102
endDate: DateFromISOString,
103+
/** paymentHash provided by nextBatchPrevId in the previous list */
104+
prevId: t.string,
86105
},
87106
'PaymentQuery'
88107
);

modules/abstract-lightning/src/wallet/lightning.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
PaymentQuery,
2727
LightningOnchainWithdrawParams,
2828
LightningOnchainWithdrawResponse,
29+
ListInvoicesResponse,
30+
ListPaymentsResponse,
2931
} from '../codecs';
3032
import { LightningPaymentIntent, LightningPaymentRequest } from '@bitgo/public-types';
3133

@@ -143,9 +145,10 @@ export interface ILightningWallet {
143145
* @param {bigint} [params.limit] The maximum number of invoices to return
144146
* @param {Date} [params.startDate] The start date for the query
145147
* @param {Date} [params.endDate] The end date for the query
146-
* @returns {Promise<InvoiceInfo[]>} List of invoices
148+
* @param {string} [params.prevId] Continue iterating (provided by nextBatchPrevId in the previous list)
149+
* @returns {Promise<ListInvoicesResponse>} List of invoices and nextBatchPrevId
147150
*/
148-
listInvoices(params: InvoiceQuery): Promise<InvoiceInfo[]>;
151+
listInvoices(params: InvoiceQuery): Promise<ListInvoicesResponse>;
149152

150153
/**
151154
* Pay a lightning invoice
@@ -181,9 +184,10 @@ export interface ILightningWallet {
181184
* @param {bigint} [params.limit] The maximum number of payments to return
182185
* @param {Date} [params.startDate] The start date for the query
183186
* @param {Date} [params.endDate] The end date for the query
184-
* @returns {Promise<PaymentInfo[]>} List of payments
187+
* @param {string} [params.prevId] Continue iterating (provided by nextBatchPrevId in the previous list)
188+
* @returns {Promise<ListPaymentsResponse>} List of payments and nextBatchPrevId
185189
*/
186-
listPayments(params: PaymentQuery): Promise<PaymentInfo[]>;
190+
listPayments(params: PaymentQuery): Promise<ListPaymentsResponse>;
187191
/**
188192
* Get transaction details by ID
189193
* @param {string} txId - Transaction ID to lookup
@@ -234,8 +238,8 @@ export class LightningWallet implements ILightningWallet {
234238
});
235239
}
236240

237-
async listInvoices(params: InvoiceQuery): Promise<InvoiceInfo[]> {
238-
const returnCodec = t.array(InvoiceInfo);
241+
async listInvoices(params: InvoiceQuery): Promise<ListInvoicesResponse> {
242+
const returnCodec = ListInvoicesResponse;
239243
const createInvoiceResponse = await this.wallet.bitgo
240244
.get(this.wallet.bitgo.url(`/wallet/${this.wallet.id()}/lightning/invoice`, 2))
241245
.query(InvoiceQuery.encode(params))
@@ -364,12 +368,12 @@ export class LightningWallet implements ILightningWallet {
364368
});
365369
}
366370

367-
async listPayments(params: PaymentQuery): Promise<PaymentInfo[]> {
371+
async listPayments(params: PaymentQuery): Promise<ListPaymentsResponse> {
368372
const response = await this.wallet.bitgo
369373
.get(this.wallet.bitgo.url(`/wallet/${this.wallet.id()}/lightning/payment`, 2))
370374
.query(PaymentQuery.encode(params))
371375
.result();
372-
return decodeOrElse(t.array(PaymentInfo).name, t.array(PaymentInfo), response, (error) => {
376+
return decodeOrElse(ListPaymentsResponse.name, ListPaymentsResponse, response, (error) => {
373377
throw new Error(`Invalid payment list response: ${error}`);
374378
});
375379
}

modules/bitgo/test/v2/unit/lightning/lightningWallets.ts

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
getLightningAuthKeychains,
1717
updateWalletCoinSpecific,
1818
LightningOnchainWithdrawParams,
19+
PaymentInfo,
20+
PaymentQuery,
1921
} from '@bitgo/abstract-lightning';
2022

2123
import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src';
@@ -264,17 +266,56 @@ describe('Lightning wallets', function () {
264266
const listInvoicesNock = nock(bgUrl)
265267
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`)
266268
.query(InvoiceQuery.encode(query))
267-
.reply(200, [InvoiceInfo.encode(invoice)]);
269+
.reply(200, { invoices: [InvoiceInfo.encode(invoice)] });
268270
const invoiceResponse = await wallet.listInvoices(query);
269-
assert.strictEqual(invoiceResponse.length, 1);
270-
assert.deepStrictEqual(invoiceResponse[0], invoice);
271+
assert.strictEqual(invoiceResponse.invoices.length, 1);
272+
assert.deepStrictEqual(invoiceResponse.invoices[0], invoice);
273+
listInvoicesNock.done();
274+
});
275+
276+
it('should work properly with pagination while listing invoices', async function () {
277+
const invoice1: InvoiceInfo = {
278+
valueMsat: 1000n,
279+
paymentHash: 'foo1',
280+
invoice: 'tlnfoobar1',
281+
walletId: wallet.wallet.id(),
282+
status: 'open',
283+
expiresAt: new Date(),
284+
createdAt: new Date(),
285+
updatedAt: new Date(),
286+
};
287+
const invoice2: InvoiceInfo = {
288+
...invoice1,
289+
paymentHash: 'foo2',
290+
invoice: 'tlnfoobar2',
291+
};
292+
const invoice3: InvoiceInfo = {
293+
...invoice1,
294+
paymentHash: 'foo3',
295+
invoice: 'tlnfoobar3',
296+
};
297+
const allInvoices = [InvoiceInfo.encode(invoice1), InvoiceInfo.encode(invoice2), InvoiceInfo.encode(invoice3)];
298+
const query = {
299+
status: 'open',
300+
startDate: new Date(),
301+
limit: 2n,
302+
} as InvoiceQuery;
303+
const listInvoicesNock = nock(bgUrl)
304+
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`)
305+
.query(InvoiceQuery.encode(query))
306+
.reply(200, { invoices: allInvoices.slice(0, 2), nextBatchPrevId: invoice2.paymentHash });
307+
const invoiceResponse = await wallet.listInvoices(query);
308+
assert.strictEqual(invoiceResponse.invoices.length, 2);
309+
assert.deepStrictEqual(invoiceResponse.invoices[0], invoice1);
310+
assert.deepStrictEqual(invoiceResponse.invoices[1], invoice2);
311+
assert.strictEqual(invoiceResponse.nextBatchPrevId, invoice2.paymentHash);
271312
listInvoicesNock.done();
272313
});
273314

274315
it('listInvoices should throw error if wp response is invalid', async function () {
275316
const listInvoicesNock = nock(bgUrl)
276317
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/invoice`)
277-
.reply(200, [{ valueMsat: '1000' }]);
318+
.reply(200, { invoices: [{ valueMsat: '1000' }] });
278319
await assert.rejects(async () => await wallet.listInvoices({}), /Invalid list invoices response/);
279320
listInvoicesNock.done();
280321
});
@@ -464,6 +505,99 @@ describe('Lightning wallets', function () {
464505
});
465506
});
466507

508+
describe('payments', function () {
509+
let wallet: LightningWallet;
510+
beforeEach(function () {
511+
wallet = getLightningWallet(
512+
new Wallet(bitgo, basecoin, {
513+
id: 'walletId',
514+
coin: 'tlnbtc',
515+
subType: 'lightningCustody',
516+
coinSpecific: { keys: ['def', 'ghi'] },
517+
})
518+
) as LightningWallet;
519+
});
520+
521+
it('should get payments', async function () {
522+
const payment: PaymentInfo = {
523+
paymentHash: 'foo',
524+
walletId: wallet.wallet.id(),
525+
txRequestId: 'txReqId',
526+
status: 'settled',
527+
invoice: 'tlnfoobar',
528+
destination: 'destination',
529+
feeLimitMsat: 100n,
530+
amountMsat: 1000n,
531+
createdAt: new Date(),
532+
updatedAt: new Date(),
533+
};
534+
const query = {
535+
status: 'settled',
536+
startDate: new Date(),
537+
limit: 100n,
538+
} as PaymentQuery;
539+
const listPaymentsNock = nock(bgUrl)
540+
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/payment`)
541+
.query(PaymentQuery.encode(query))
542+
.reply(200, { payments: [PaymentInfo.encode(payment)] });
543+
const listPaymentsResponse = await wallet.listPayments(query);
544+
assert.strictEqual(listPaymentsResponse.payments.length, 1);
545+
assert.deepStrictEqual(listPaymentsResponse.payments[0], payment);
546+
listPaymentsNock.done();
547+
});
548+
549+
it('should work properly with pagination while listing payments', async function () {
550+
const payment1: PaymentInfo = {
551+
paymentHash: 'foo1',
552+
walletId: wallet.wallet.id(),
553+
txRequestId: 'txReqId1',
554+
status: 'settled',
555+
invoice: 'tlnfoobar1',
556+
destination: 'destination',
557+
feeLimitMsat: 100n,
558+
amountMsat: 1000n,
559+
createdAt: new Date(),
560+
updatedAt: new Date(),
561+
};
562+
const payment2: PaymentInfo = {
563+
...payment1,
564+
paymentHash: 'foo2',
565+
txRequestId: 'txReqId2',
566+
invoice: 'tlnfoobar2',
567+
};
568+
const payment3: PaymentInfo = {
569+
...payment1,
570+
paymentHash: 'foo3',
571+
txRequestId: 'txReqId3',
572+
invoice: 'tlnfoobar3',
573+
};
574+
const allPayments = [PaymentInfo.encode(payment1), PaymentInfo.encode(payment2), PaymentInfo.encode(payment3)];
575+
const query = {
576+
status: 'settled',
577+
startDate: new Date(),
578+
limit: 2n,
579+
} as PaymentQuery;
580+
const listPaymentsNock = nock(bgUrl)
581+
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/payment`)
582+
.query(PaymentQuery.encode(query))
583+
.reply(200, { payments: allPayments.slice(0, 2), nextBatchPrevId: payment2.paymentHash });
584+
const listPaymentsResponse = await wallet.listPayments(query);
585+
assert.strictEqual(listPaymentsResponse.payments.length, 2);
586+
assert.deepStrictEqual(listPaymentsResponse.payments[0], payment1);
587+
assert.deepStrictEqual(listPaymentsResponse.payments[1], payment2);
588+
assert.strictEqual(listPaymentsResponse.nextBatchPrevId, payment2.paymentHash);
589+
listPaymentsNock.done();
590+
});
591+
592+
it('listPayments should throw error if wp response is invalid', async function () {
593+
const listPaymentsNock = nock(bgUrl)
594+
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/payment`)
595+
.reply(200, { payments: [{ amountMsat: '1000' }] });
596+
await assert.rejects(async () => await wallet.listPayments({}), /Invalid payment list response/);
597+
listPaymentsNock.done();
598+
});
599+
});
600+
467601
describe('Get lightning key(s)', function () {
468602
const walletData = {
469603
id: 'fakeid',

0 commit comments

Comments
 (0)