Skip to content

Commit ce9b735

Browse files
committed
feat(abstract-lightning): add cursor-based pagination for lightning transactions
Ticket: BTC-2566 - Add prevId parameter to TransactionQuery for cursor-based pagination - Add nextBatchPrevId to ListTransactionsResponse for pagination continuation - Update lightning wallet interface to return paginated response format - Add comprehensive JSDoc documentation for pagination cursor pattern - Update example to use new destructured response format - Add comprehensive test coverage for pagination scenarios
1 parent b25fe3f commit ce9b735

File tree

4 files changed

+225
-8
lines changed

4 files changed

+225
-8
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ async function main(): Promise<void> {
6666
if (endDate) queryParams.endDate = endDate;
6767

6868
// List transactions with the provided filters
69-
const transactions = await lightning.listTransactions(queryParams);
69+
const { transactions } = await lightning.listTransactions(queryParams);
7070

7171
// Display transaction summary
7272
console.log(`\nFound ${transactions.length} transactions:`);

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

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,38 @@ export const Transaction = t.intersection(
7373
);
7474
export type Transaction = t.TypeOf<typeof Transaction>;
7575

76+
export const ListTransactionsResponse = t.intersection(
77+
[
78+
t.type({
79+
transactions: t.array(Transaction),
80+
}),
81+
t.partial({
82+
/**
83+
* Transaction ID of the last transaction in this batch.
84+
* Use as prevId in next request to continue pagination.
85+
*/
86+
nextBatchPrevId: t.string,
87+
}),
88+
],
89+
'ListTransactionsResponse'
90+
);
91+
export type ListTransactionsResponse = t.TypeOf<typeof ListTransactionsResponse>;
92+
7693
/**
77-
* Transaction query parameters
94+
* Transaction query parameters with cursor-based pagination
7895
*/
7996
export const TransactionQuery = t.partial(
8097
{
81-
blockHeight: BigIntFromString,
98+
/** Maximum number of transactions to return per page */
8299
limit: BigIntFromString,
100+
/** Optional filter for transactions at a specific block height */
101+
blockHeight: BigIntFromString,
102+
/** Optional start date filter */
83103
startDate: DateFromISOString,
104+
/** Optional end date filter */
84105
endDate: DateFromISOString,
106+
/** Transaction ID for cursor-based pagination (from nextBatchPrevId) */
107+
prevId: t.string,
85108
},
86109
'TransactionQuery'
87110
);

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
SubmitPaymentParams,
2727
Transaction,
2828
TransactionQuery,
29+
ListTransactionsResponse,
2930
PaymentInfo,
3031
PaymentQuery,
3132
LightningOnchainWithdrawParams,
@@ -199,14 +200,15 @@ export interface ILightningWallet {
199200
getTransaction(txId: string): Promise<Transaction>;
200201

201202
/**
202-
* List transactions for a wallet with optional filtering
203+
* List transactions for a wallet with optional filtering and cursor-based pagination
203204
* @param {TransactionQuery} params Query parameters for filtering transactions
204205
* @param {bigint} [params.limit] The maximum number of transactions to return
205206
* @param {Date} [params.startDate] The start date for the query
206207
* @param {Date} [params.endDate] The end date for the query
207-
* @returns {Promise<Transaction[]>} List of transactions
208+
* @param {string} [params.prevId] Transaction ID for cursor-based pagination (from nextBatchPrevId)
209+
* @returns {Promise<ListTransactionsResponse>} List of transactions with pagination info
208210
*/
209-
listTransactions(params: TransactionQuery): Promise<Transaction[]>;
211+
listTransactions(params: TransactionQuery): Promise<ListTransactionsResponse>;
210212
}
211213

212214
export class LightningWallet implements ILightningWallet {
@@ -472,12 +474,12 @@ export class LightningWallet implements ILightningWallet {
472474
});
473475
}
474476

475-
async listTransactions(params: TransactionQuery): Promise<Transaction[]> {
477+
async listTransactions(params: TransactionQuery): Promise<ListTransactionsResponse> {
476478
const response = await this.wallet.bitgo
477479
.get(this.wallet.bitgo.url(`/wallet/${this.wallet.id()}/lightning/transaction`, 2))
478480
.query(TransactionQuery.encode(params))
479481
.result();
480-
return decodeOrElse(t.array(Transaction).name, t.array(Transaction), response, (error) => {
482+
return decodeOrElse(ListTransactionsResponse.name, ListTransactionsResponse, response, (error) => {
481483
throw new Error(`Invalid transaction list response: ${error}`);
482484
});
483485
}

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

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
LightningOnchainWithdrawParams,
1818
PaymentInfo,
1919
PaymentQuery,
20+
TransactionQuery,
2021
} from '@bitgo/abstract-lightning';
2122

2223
import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src';
@@ -1067,4 +1068,195 @@ describe('Lightning wallets', function () {
10671068
getPendingApprovalNock.done();
10681069
});
10691070
});
1071+
1072+
describe('transactions', function () {
1073+
let wallet: LightningWallet;
1074+
1075+
beforeEach(function () {
1076+
wallet = getLightningWallet(
1077+
new Wallet(bitgo, basecoin, {
1078+
id: 'walletId',
1079+
coin: 'tlnbtc',
1080+
subType: 'lightningCustody',
1081+
coinSpecific: { keys: ['def', 'ghi'] },
1082+
})
1083+
) as LightningWallet;
1084+
});
1085+
1086+
it('should list transactions', async function () {
1087+
const transaction = {
1088+
id: 'tx123',
1089+
normalizedTxHash: 'normalizedHash123',
1090+
blockHeight: 100000,
1091+
inputIds: ['input1', 'input2'],
1092+
entries: [
1093+
{
1094+
inputs: 1,
1095+
outputs: 2,
1096+
value: 50000,
1097+
valueString: '50000',
1098+
address: 'testAddress',
1099+
wallet: wallet.wallet.id(),
1100+
},
1101+
],
1102+
inputs: [
1103+
{
1104+
id: 'input1',
1105+
value: 50000,
1106+
valueString: '50000',
1107+
address: 'inputAddress',
1108+
wallet: wallet.wallet.id(),
1109+
},
1110+
],
1111+
outputs: [
1112+
{
1113+
id: 'output1',
1114+
value: 49500,
1115+
valueString: '49500',
1116+
address: 'outputAddress',
1117+
wallet: wallet.wallet.id(),
1118+
},
1119+
],
1120+
size: 250,
1121+
date: new Date('2023-01-01T00:00:00Z'),
1122+
fee: 500,
1123+
feeString: '500',
1124+
hex: 'deadbeef',
1125+
confirmations: 6,
1126+
};
1127+
const query = {
1128+
limit: 100n,
1129+
startDate: new Date(),
1130+
};
1131+
const listTransactionsNock = nock(bgUrl)
1132+
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`)
1133+
.query(TransactionQuery.encode(query))
1134+
.reply(200, { transactions: [transaction] });
1135+
const listTransactionsResponse = await wallet.listTransactions(query);
1136+
assert.strictEqual(listTransactionsResponse.transactions.length, 1);
1137+
assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction);
1138+
assert.strictEqual(listTransactionsResponse.nextBatchPrevId, undefined);
1139+
listTransactionsNock.done();
1140+
});
1141+
1142+
it('should work properly with pagination while listing transactions', async function () {
1143+
const transaction1 = {
1144+
id: 'tx123',
1145+
normalizedTxHash: 'normalizedHash123',
1146+
blockHeight: 100000,
1147+
inputIds: ['input1', 'input2'],
1148+
entries: [
1149+
{
1150+
inputs: 1,
1151+
outputs: 2,
1152+
value: 50000,
1153+
valueString: '50000',
1154+
address: 'testAddress',
1155+
wallet: wallet.wallet.id(),
1156+
},
1157+
],
1158+
inputs: [
1159+
{
1160+
id: 'input1',
1161+
value: 50000,
1162+
valueString: '50000',
1163+
address: 'inputAddress',
1164+
wallet: wallet.wallet.id(),
1165+
},
1166+
],
1167+
outputs: [
1168+
{
1169+
id: 'output1',
1170+
value: 49500,
1171+
valueString: '49500',
1172+
address: 'outputAddress',
1173+
wallet: wallet.wallet.id(),
1174+
},
1175+
],
1176+
size: 250,
1177+
date: new Date('2023-01-01T00:00:00Z'),
1178+
fee: 500,
1179+
feeString: '500',
1180+
hex: 'deadbeef',
1181+
confirmations: 6,
1182+
};
1183+
const transaction2 = {
1184+
...transaction1,
1185+
id: 'tx456',
1186+
normalizedTxHash: 'normalizedHash456',
1187+
blockHeight: 100001,
1188+
date: new Date('2023-01-02T00:00:00Z'),
1189+
};
1190+
const query = {
1191+
limit: 2n,
1192+
startDate: new Date('2023-01-01'),
1193+
};
1194+
const listTransactionsNock = nock(bgUrl)
1195+
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`)
1196+
.query(TransactionQuery.encode(query))
1197+
.reply(200, { transactions: [transaction1, transaction2], nextBatchPrevId: transaction2.id });
1198+
const listTransactionsResponse = await wallet.listTransactions(query);
1199+
assert.strictEqual(listTransactionsResponse.transactions.length, 2);
1200+
assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction1);
1201+
assert.deepStrictEqual(listTransactionsResponse.transactions[1], transaction2);
1202+
assert.strictEqual(listTransactionsResponse.nextBatchPrevId, transaction2.id);
1203+
listTransactionsNock.done();
1204+
});
1205+
1206+
it('should handle prevId parameter for pagination cursor', async function () {
1207+
const transaction3 = {
1208+
id: 'tx789',
1209+
normalizedTxHash: 'normalizedHash789',
1210+
blockHeight: 100002,
1211+
inputIds: ['input1'],
1212+
entries: [
1213+
{
1214+
inputs: 1,
1215+
outputs: 1,
1216+
value: 40000,
1217+
valueString: '40000',
1218+
address: 'testAddress',
1219+
wallet: wallet.wallet.id(),
1220+
},
1221+
],
1222+
inputs: [
1223+
{
1224+
id: 'input1',
1225+
value: 40000,
1226+
valueString: '40000',
1227+
address: 'inputAddress',
1228+
wallet: wallet.wallet.id(),
1229+
},
1230+
],
1231+
outputs: [
1232+
{
1233+
id: 'output1',
1234+
value: 39500,
1235+
valueString: '39500',
1236+
address: 'outputAddress',
1237+
wallet: wallet.wallet.id(),
1238+
},
1239+
],
1240+
size: 200,
1241+
date: new Date('2023-01-03T00:00:00Z'),
1242+
fee: 500,
1243+
feeString: '500',
1244+
hex: 'cafebabe',
1245+
confirmations: 4,
1246+
};
1247+
const query = {
1248+
limit: 1n,
1249+
prevId: 'tx456', // Continue from this transaction ID
1250+
};
1251+
const listTransactionsNock = nock(bgUrl)
1252+
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`)
1253+
.query(TransactionQuery.encode(query))
1254+
.reply(200, { transactions: [transaction3] });
1255+
const listTransactionsResponse = await wallet.listTransactions(query);
1256+
assert.strictEqual(listTransactionsResponse.transactions.length, 1);
1257+
assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction3);
1258+
assert.strictEqual(listTransactionsResponse.nextBatchPrevId, undefined);
1259+
listTransactionsNock.done();
1260+
});
1261+
});
10701262
});

0 commit comments

Comments
 (0)