Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/ts/btc/lightning/list-transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ async function main(): Promise<void> {
if (endDate) queryParams.endDate = endDate;

// List transactions with the provided filters
const transactions = await lightning.listTransactions(queryParams);
const { transactions } = await lightning.listTransactions(queryParams);

// Display transaction summary
console.log(`\nFound ${transactions.length} transactions:`);
Expand Down
27 changes: 25 additions & 2 deletions modules/abstract-lightning/src/codecs/api/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,38 @@ export const Transaction = t.intersection(
);
export type Transaction = t.TypeOf<typeof Transaction>;

export const ListTransactionsResponse = t.intersection(
[
t.type({
transactions: t.array(Transaction),
}),
t.partial({
/**
* Transaction ID of the last transaction in this batch.
* Use as prevId in next request to continue pagination.
*/
nextBatchPrevId: t.string,
}),
],
'ListTransactionsResponse'
);
export type ListTransactionsResponse = t.TypeOf<typeof ListTransactionsResponse>;

/**
* Transaction query parameters
* Transaction query parameters with cursor-based pagination
*/
export const TransactionQuery = t.partial(
{
blockHeight: BigIntFromString,
/** Maximum number of transactions to return per page */
limit: BigIntFromString,
/** Optional filter for transactions at a specific block height */
blockHeight: BigIntFromString,
/** Optional start date filter */
startDate: DateFromISOString,
/** Optional end date filter */
endDate: DateFromISOString,
/** Transaction ID for cursor-based pagination (from nextBatchPrevId) */
prevId: t.string,
},
'TransactionQuery'
);
Expand Down
12 changes: 7 additions & 5 deletions modules/abstract-lightning/src/wallet/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
SubmitPaymentParams,
Transaction,
TransactionQuery,
ListTransactionsResponse,
PaymentInfo,
PaymentQuery,
LightningOnchainWithdrawParams,
Expand Down Expand Up @@ -199,14 +200,15 @@ export interface ILightningWallet {
getTransaction(txId: string): Promise<Transaction>;

/**
* List transactions for a wallet with optional filtering
* List transactions for a wallet with optional filtering and cursor-based pagination
* @param {TransactionQuery} params Query parameters for filtering transactions
* @param {bigint} [params.limit] The maximum number of transactions to return
* @param {Date} [params.startDate] The start date for the query
* @param {Date} [params.endDate] The end date for the query
* @returns {Promise<Transaction[]>} List of transactions
* @param {string} [params.prevId] Transaction ID for cursor-based pagination (from nextBatchPrevId)
* @returns {Promise<ListTransactionsResponse>} List of transactions with pagination info
*/
listTransactions(params: TransactionQuery): Promise<Transaction[]>;
listTransactions(params: TransactionQuery): Promise<ListTransactionsResponse>;
}

export class LightningWallet implements ILightningWallet {
Expand Down Expand Up @@ -472,12 +474,12 @@ export class LightningWallet implements ILightningWallet {
});
}

async listTransactions(params: TransactionQuery): Promise<Transaction[]> {
async listTransactions(params: TransactionQuery): Promise<ListTransactionsResponse> {
const response = await this.wallet.bitgo
.get(this.wallet.bitgo.url(`/wallet/${this.wallet.id()}/lightning/transaction`, 2))
.query(TransactionQuery.encode(params))
.result();
return decodeOrElse(t.array(Transaction).name, t.array(Transaction), response, (error) => {
return decodeOrElse(ListTransactionsResponse.name, ListTransactionsResponse, response, (error) => {
throw new Error(`Invalid transaction list response: ${error}`);
});
}
Expand Down
192 changes: 192 additions & 0 deletions modules/bitgo/test/v2/unit/lightning/lightningWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
LightningOnchainWithdrawParams,
PaymentInfo,
PaymentQuery,
TransactionQuery,
} from '@bitgo/abstract-lightning';

import { BitGo, common, GenerateLightningWalletOptions, Wallet, Wallets } from '../../../../src';
Expand Down Expand Up @@ -1067,4 +1068,195 @@ describe('Lightning wallets', function () {
getPendingApprovalNock.done();
});
});

describe('transactions', function () {
let wallet: LightningWallet;

beforeEach(function () {
wallet = getLightningWallet(
new Wallet(bitgo, basecoin, {
id: 'walletId',
coin: 'tlnbtc',
subType: 'lightningCustody',
coinSpecific: { keys: ['def', 'ghi'] },
})
) as LightningWallet;
});

it('should list transactions', async function () {
const transaction = {
id: 'tx123',
normalizedTxHash: 'normalizedHash123',
blockHeight: 100000,
inputIds: ['input1', 'input2'],
entries: [
{
inputs: 1,
outputs: 2,
value: 50000,
valueString: '50000',
address: 'testAddress',
wallet: wallet.wallet.id(),
},
],
inputs: [
{
id: 'input1',
value: 50000,
valueString: '50000',
address: 'inputAddress',
wallet: wallet.wallet.id(),
},
],
outputs: [
{
id: 'output1',
value: 49500,
valueString: '49500',
address: 'outputAddress',
wallet: wallet.wallet.id(),
},
],
size: 250,
date: new Date('2023-01-01T00:00:00Z'),
fee: 500,
feeString: '500',
hex: 'deadbeef',
confirmations: 6,
};
const query = {
limit: 100n,
startDate: new Date(),
};
const listTransactionsNock = nock(bgUrl)
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`)
.query(TransactionQuery.encode(query))
.reply(200, { transactions: [transaction] });
const listTransactionsResponse = await wallet.listTransactions(query);
assert.strictEqual(listTransactionsResponse.transactions.length, 1);
assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction);
assert.strictEqual(listTransactionsResponse.nextBatchPrevId, undefined);
listTransactionsNock.done();
});

it('should work properly with pagination while listing transactions', async function () {
const transaction1 = {
id: 'tx123',
normalizedTxHash: 'normalizedHash123',
blockHeight: 100000,
inputIds: ['input1', 'input2'],
entries: [
{
inputs: 1,
outputs: 2,
value: 50000,
valueString: '50000',
address: 'testAddress',
wallet: wallet.wallet.id(),
},
],
inputs: [
{
id: 'input1',
value: 50000,
valueString: '50000',
address: 'inputAddress',
wallet: wallet.wallet.id(),
},
],
outputs: [
{
id: 'output1',
value: 49500,
valueString: '49500',
address: 'outputAddress',
wallet: wallet.wallet.id(),
},
],
size: 250,
date: new Date('2023-01-01T00:00:00Z'),
fee: 500,
feeString: '500',
hex: 'deadbeef',
confirmations: 6,
};
const transaction2 = {
...transaction1,
id: 'tx456',
normalizedTxHash: 'normalizedHash456',
blockHeight: 100001,
date: new Date('2023-01-02T00:00:00Z'),
};
const query = {
limit: 2n,
startDate: new Date('2023-01-01'),
};
const listTransactionsNock = nock(bgUrl)
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`)
.query(TransactionQuery.encode(query))
.reply(200, { transactions: [transaction1, transaction2], nextBatchPrevId: transaction2.id });
const listTransactionsResponse = await wallet.listTransactions(query);
assert.strictEqual(listTransactionsResponse.transactions.length, 2);
assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction1);
assert.deepStrictEqual(listTransactionsResponse.transactions[1], transaction2);
assert.strictEqual(listTransactionsResponse.nextBatchPrevId, transaction2.id);
listTransactionsNock.done();
});

it('should handle prevId parameter for pagination cursor', async function () {
const transaction3 = {
id: 'tx789',
normalizedTxHash: 'normalizedHash789',
blockHeight: 100002,
inputIds: ['input1'],
entries: [
{
inputs: 1,
outputs: 1,
value: 40000,
valueString: '40000',
address: 'testAddress',
wallet: wallet.wallet.id(),
},
],
inputs: [
{
id: 'input1',
value: 40000,
valueString: '40000',
address: 'inputAddress',
wallet: wallet.wallet.id(),
},
],
outputs: [
{
id: 'output1',
value: 39500,
valueString: '39500',
address: 'outputAddress',
wallet: wallet.wallet.id(),
},
],
size: 200,
date: new Date('2023-01-03T00:00:00Z'),
fee: 500,
feeString: '500',
hex: 'cafebabe',
confirmations: 4,
};
const query = {
limit: 1n,
prevId: 'tx456', // Continue from this transaction ID
};
const listTransactionsNock = nock(bgUrl)
.get(`/api/v2/wallet/${wallet.wallet.id()}/lightning/transaction`)
.query(TransactionQuery.encode(query))
.reply(200, { transactions: [transaction3] });
const listTransactionsResponse = await wallet.listTransactions(query);
assert.strictEqual(listTransactionsResponse.transactions.length, 1);
assert.deepStrictEqual(listTransactionsResponse.transactions[0], transaction3);
assert.strictEqual(listTransactionsResponse.nextBatchPrevId, undefined);
listTransactionsNock.done();
});
});
});