Skip to content

Commit 2a677c2

Browse files
authored
Merge pull request #7108 from BitGo/feature/BTC-2566-lightning-pagination
feat: implemented pagination for lighting transactions Add ListTransactionsResponse codec with nextBatchPrevId for pagination Update TransactionQuery to include prevId parameter for cursor navigation Enhance listTransactions method to return paginated response Add comprehensive JSDoc documentation explaining pagination patterns Update example to handle new response format Add thorough test coverage for pagination scenarios This implementation follows on-chain transaction pagination patterns with: Composite cursor using (date, id) for stable ordering prevId parameter for continuation tokens nextBatchPrevId response field for next page navigation Full backward compatibility for existing usage
2 parents 34f70ed + ce9b735 commit 2a677c2

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)