diff --git a/cmd/commands/commands.go b/cmd/commands/commands.go index ff20701e6d..cde907119f 100644 --- a/cmd/commands/commands.go +++ b/cmd/commands/commands.go @@ -2074,6 +2074,20 @@ var listChainTxnsCommand = cli.Command{ "transactions until the chain tip, including " + "unconfirmed, set this value to -1", }, + cli.UintFlag{ + Name: "index_offset", + Usage: "the index of a transaction that will be " + + "used in a query to determine which " + + "transaction should be returned in the " + + "response", + }, + cli.IntFlag{ + Name: "max_transactions", + Usage: "(optional) the max number of transactions to " + + "return; leave at default of 0 to return " + + "all transactions", + Value: 0, + }, }, Description: ` List all transactions an address of the wallet was involved in. @@ -2096,7 +2110,10 @@ func listChainTxns(ctx *cli.Context) error { client, cleanUp := getClient(ctx) defer cleanUp() - req := &lnrpc.GetTransactionsRequest{} + req := &lnrpc.GetTransactionsRequest{ + IndexOffset: uint32(ctx.Uint64("index_offset")), + MaxTransactions: uint32(ctx.Uint64("max_transactions")), + } if ctx.IsSet("start_height") { req.StartHeight = int32(ctx.Int64("start_height")) diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md index 12049b40c2..6fa3c0f063 100644 --- a/docs/release-notes/release-notes-0.19.0.md +++ b/docs/release-notes/release-notes-0.19.0.md @@ -62,6 +62,8 @@ # New Features ## Functional Enhancements +* [Add ability](https://github.com/lightningnetwork/lnd/pull/8998) to paginate + wallet transactions. ## RPC Additions * [Add a new rpc endpoint](https://github.com/lightningnetwork/lnd/pull/8843) @@ -196,6 +198,7 @@ The underlying functionality between those two options remain the same. # Contributors (Alphabetical Order) +* Abdullahi Yunus * Animesh Bilthare * Boris Nagaev * CharlieZKSmith diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go index eabe952b21..beb56a7ef9 100644 --- a/lnrpc/lightning.pb.go +++ b/lnrpc/lightning.pb.go @@ -2068,6 +2068,12 @@ type GetTransactionsRequest struct { EndHeight int32 `protobuf:"varint,2,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"` // An optional filter to only include transactions relevant to an account. Account string `protobuf:"bytes,3,opt,name=account,proto3" json:"account,omitempty"` + // The index of a transaction that will be used in a query to determine which + // transaction should be returned in the response. + IndexOffset uint32 `protobuf:"varint,4,opt,name=index_offset,json=indexOffset,proto3" json:"index_offset,omitempty"` + // The maximal number of transactions returned in the response to this query. + // This value should be set to 0 to return all transactions. + MaxTransactions uint32 `protobuf:"varint,5,opt,name=max_transactions,json=maxTransactions,proto3" json:"max_transactions,omitempty"` } func (x *GetTransactionsRequest) Reset() { @@ -2123,6 +2129,20 @@ func (x *GetTransactionsRequest) GetAccount() string { return "" } +func (x *GetTransactionsRequest) GetIndexOffset() uint32 { + if x != nil { + return x.IndexOffset + } + return 0 +} + +func (x *GetTransactionsRequest) GetMaxTransactions() uint32 { + if x != nil { + return x.MaxTransactions + } + return 0 +} + type TransactionDetails struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2130,6 +2150,12 @@ type TransactionDetails struct { // The list of transactions relevant to the wallet. Transactions []*Transaction `protobuf:"bytes,1,rep,name=transactions,proto3" json:"transactions,omitempty"` + // The index of the last item in the set of returned transactions. This can be + // used to seek further, pagination style. + LastIndex uint64 `protobuf:"varint,2,opt,name=last_index,json=lastIndex,proto3" json:"last_index,omitempty"` + // The index of the last item in the set of returned transactions. This can be + // used to seek backwards, pagination style. + FirstIndex uint64 `protobuf:"varint,3,opt,name=first_index,json=firstIndex,proto3" json:"first_index,omitempty"` } func (x *TransactionDetails) Reset() { @@ -2171,6 +2197,20 @@ func (x *TransactionDetails) GetTransactions() []*Transaction { return nil } +func (x *TransactionDetails) GetLastIndex() uint64 { + if x != nil { + return x.LastIndex + } + return 0 +} + +func (x *TransactionDetails) GetFirstIndex() uint64 { + if x != nil { + return x.FirstIndex + } + return 0 +} + type FeeLimit struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -18558,20 +18598,29 @@ var file_lightning_proto_rawDesc = []byte{ 0x75, 0x73, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x4f, 0x75, 0x74, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x11, 0x70, 0x72, 0x65, - 0x76, 0x69, 0x6f, 0x75, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0x74, - 0x0a, 0x16, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, - 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, - 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4c, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, + 0x76, 0x69, 0x6f, 0x75, 0x73, 0x4f, 0x75, 0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x73, 0x22, 0xc2, + 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, + 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x5f, 0x6f, + 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x69, 0x6e, 0x64, + 0x65, 0x78, 0x4f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, + 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x0f, 0x6d, 0x61, 0x78, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x22, 0x8c, 0x01, 0x0a, 0x12, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x36, 0x0a, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x22, 0x68, 0x0a, 0x08, 0x46, 0x65, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, + 0x6e, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x64, 0x65, + 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x66, 0x69, 0x72, 0x73, 0x74, 0x49, 0x6e, 0x64, + 0x65, 0x78, 0x22, 0x68, 0x0a, 0x08, 0x46, 0x65, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x05, 0x66, 0x69, 0x78, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x05, 0x66, 0x69, 0x78, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0a, 0x66, 0x69, 0x78, 0x65, 0x64, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x09, 0x66, 0x69, diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto index 4ea163f20d..c504eac8f4 100644 --- a/lnrpc/lightning.proto +++ b/lnrpc/lightning.proto @@ -757,11 +757,35 @@ message GetTransactionsRequest { // An optional filter to only include transactions relevant to an account. string account = 3; + + /* + The index of a transaction that will be used in a query to determine which + transaction should be returned in the response. + */ + uint32 index_offset = 4; + + /* + The maximal number of transactions returned in the response to this query. + This value should be set to 0 to return all transactions. + */ + uint32 max_transactions = 5; } message TransactionDetails { // The list of transactions relevant to the wallet. repeated Transaction transactions = 1; + + /* + The index of the last item in the set of returned transactions. This can be + used to seek further, pagination style. + */ + uint64 last_index = 2; + + /* + The index of the last item in the set of returned transactions. This can be + used to seek backwards, pagination style. + */ + uint64 first_index = 3; } message FeeLimit { diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json index c4774334ed..2b42f80890 100644 --- a/lnrpc/lightning.swagger.json +++ b/lnrpc/lightning.swagger.json @@ -2591,6 +2591,22 @@ "in": "query", "required": false, "type": "string" + }, + { + "name": "index_offset", + "description": "The index of a transaction that will be used in a query to determine which\ntransaction should be returned in the response.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "max_transactions", + "description": "The maximal number of transactions returned in the response to this query.\nThis value should be set to 0 to return all transactions.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" } ], "tags": [ @@ -2774,6 +2790,22 @@ "in": "query", "required": false, "type": "string" + }, + { + "name": "index_offset", + "description": "The index of a transaction that will be used in a query to determine which\ntransaction should be returned in the response.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" + }, + { + "name": "max_transactions", + "description": "The maximal number of transactions returned in the response to this query.\nThis value should be set to 0 to return all transactions.", + "in": "query", + "required": false, + "type": "integer", + "format": "int64" } ], "tags": [ @@ -7492,6 +7524,16 @@ "$ref": "#/definitions/lnrpcTransaction" }, "description": "The list of transactions relevant to the wallet." + }, + "last_index": { + "type": "string", + "format": "uint64", + "description": "The index of the last item in the set of returned transactions. This can be\nused to seek further, pagination style." + }, + "first_index": { + "type": "string", + "format": "uint64", + "description": "The index of the last item in the set of returned transactions. This can be\nused to seek backwards, pagination style." } } }, diff --git a/lnrpc/rpc_utils.go b/lnrpc/rpc_utils.go index eff0a5a53e..15e3f2f762 100644 --- a/lnrpc/rpc_utils.go +++ b/lnrpc/rpc_utils.go @@ -117,9 +117,13 @@ func RPCTransaction(tx *lnwallet.TransactionDetail) *Transaction { } // RPCTransactionDetails returns a set of rpc transaction details. -func RPCTransactionDetails(txns []*lnwallet.TransactionDetail) *TransactionDetails { +func RPCTransactionDetails(txns []*lnwallet.TransactionDetail, firstIdx, + lastIdx uint64) *TransactionDetails { + txDetails := &TransactionDetails{ Transactions: make([]*Transaction, len(txns)), + FirstIndex: firstIdx, + LastIndex: lastIdx, } for i, tx := range txns { diff --git a/lnrpc/walletrpc/walletkit.swagger.json b/lnrpc/walletrpc/walletkit.swagger.json index 5acafa67df..30d806af92 100644 --- a/lnrpc/walletrpc/walletkit.swagger.json +++ b/lnrpc/walletrpc/walletkit.swagger.json @@ -1166,6 +1166,16 @@ "$ref": "#/definitions/lnrpcTransaction" }, "description": "The list of transactions relevant to the wallet." + }, + "last_index": { + "type": "string", + "format": "uint64", + "description": "The index of the last item in the set of returned transactions. This can be\nused to seek further, pagination style." + }, + "first_index": { + "type": "string", + "format": "uint64", + "description": "The index of the last item in the set of returned transactions. This can be\nused to seek backwards, pagination style." } } }, diff --git a/lnrpc/walletrpc/walletkit_server.go b/lnrpc/walletrpc/walletkit_server.go index b15012f0c0..3b16f4da71 100644 --- a/lnrpc/walletrpc/walletkit_server.go +++ b/lnrpc/walletrpc/walletkit_server.go @@ -1376,9 +1376,9 @@ func (w *WalletKit) ListSweeps(ctx context.Context, // can match our list of sweeps against the list of transactions that // the wallet is still tracking. Sweeps are currently always swept to // the default wallet account. - transactions, err := w.cfg.Wallet.ListTransactionDetails( + txns, firstIdx, lastIdx, err := w.cfg.Wallet.ListTransactionDetails( in.StartHeight, btcwallet.UnconfirmedHeight, - lnwallet.DefaultAccountName, + lnwallet.DefaultAccountName, 0, 0, ) if err != nil { return nil, err @@ -1389,7 +1389,7 @@ func (w *WalletKit) ListSweeps(ctx context.Context, txDetails []*lnwallet.TransactionDetail ) - for _, tx := range transactions { + for _, tx := range txns { _, ok := sweepTxns[tx.Hash.String()] if !ok { continue @@ -1408,7 +1408,7 @@ func (w *WalletKit) ListSweeps(ctx context.Context, return &ListSweepsResponse{ Sweeps: &ListSweepsResponse_TransactionDetails{ TransactionDetails: lnrpc.RPCTransactionDetails( - txDetails, + txDetails, firstIdx, lastIdx, ), }, }, nil diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go index fd4a433486..8b7ef55380 100644 --- a/lntest/mock/walletcontroller.go +++ b/lntest/mock/walletcontroller.go @@ -187,9 +187,10 @@ func (w *WalletController) ListUnspentWitness(int32, int32, // ListTransactionDetails currently returns dummy values. func (w *WalletController) ListTransactionDetails(int32, int32, - string) ([]*lnwallet.TransactionDetail, error) { + string, uint32, uint32) ([]*lnwallet.TransactionDetail, + uint64, uint64, error) { - return nil, nil + return nil, 0, 0, nil } // LeaseOutput returns the current time and a nil error. diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go index d0444edd58..ee62bdb277 100644 --- a/lnwallet/btcwallet/btcwallet.go +++ b/lnwallet/btcwallet/btcwallet.go @@ -1554,7 +1554,9 @@ func unminedTransactionsToDetail( // // This is a part of the WalletController interface. func (b *BtcWallet) ListTransactionDetails(startHeight, endHeight int32, - accountFilter string) ([]*lnwallet.TransactionDetail, error) { + accountFilter string, indexOffset uint32, + maxTransactions uint32) ([]*lnwallet.TransactionDetail, uint64, uint64, + error) { // Grab the best block the wallet knows of, we'll use this to calculate // # of confirmations shortly below. @@ -1566,7 +1568,7 @@ func (b *BtcWallet) ListTransactionDetails(startHeight, endHeight int32, stop := base.NewBlockIdentifierFromHeight(endHeight) txns, err := b.wallet.GetTransactions(start, stop, accountFilter, nil) if err != nil { - return nil, err + return nil, 0, 0, err } txDetails := make([]*lnwallet.TransactionDetail, 0, @@ -1580,7 +1582,7 @@ func (b *BtcWallet) ListTransactionDetails(startHeight, endHeight int32, currentHeight, blockPackage, b.netParams, ) if err != nil { - return nil, err + return nil, 0, 0, err } txDetails = append(txDetails, details...) @@ -1588,13 +1590,38 @@ func (b *BtcWallet) ListTransactionDetails(startHeight, endHeight int32, for _, tx := range txns.UnminedTransactions { detail, err := unminedTransactionsToDetail(tx, b.netParams) if err != nil { - return nil, err + return nil, 0, 0, err } txDetails = append(txDetails, detail) } - return txDetails, nil + // Return empty transaction list, if offset is more than all + // transactions. + if int(indexOffset) >= len(txDetails) { + txDetails = []*lnwallet.TransactionDetail{} + + return txDetails, 0, 0, nil + } + + end := indexOffset + maxTransactions + + // If maxTransactions is set to 0, then we'll return all transactions + // starting from the offset. + if maxTransactions == 0 { + end = uint32(len(txDetails)) + txDetails = txDetails[indexOffset:end] + + return txDetails, uint64(indexOffset), uint64(end - 1), nil + } + + if end > uint32(len(txDetails)) { + end = uint32(len(txDetails)) + } + + txDetails = txDetails[indexOffset:end] + + return txDetails, uint64(indexOffset), uint64(end - 1), nil } // txSubscriptionClient encapsulates the transaction notification client from diff --git a/lnwallet/interface.go b/lnwallet/interface.go index e2e491c735..c9dee9202a 100644 --- a/lnwallet/interface.go +++ b/lnwallet/interface.go @@ -402,7 +402,9 @@ type WalletController interface { // retrieve the transactions relevant to a specific account. When // empty, transactions of all wallet accounts are returned. ListTransactionDetails(startHeight, endHeight int32, - accountFilter string) ([]*TransactionDetail, error) + accountFilter string, indexOffset uint32, + maxTransactions uint32) ([]*TransactionDetail, uint64, uint64, + error) // LeaseOutput locks an output to the given ID, preventing it from being // available for any future coin selection attempts. The absolute time diff --git a/lnwallet/mock.go b/lnwallet/mock.go index 6623d8014f..a8610dc779 100644 --- a/lnwallet/mock.go +++ b/lnwallet/mock.go @@ -198,9 +198,9 @@ func (w *mockWalletController) ListUnspentWitness(int32, int32, // ListTransactionDetails currently returns dummy values. func (w *mockWalletController) ListTransactionDetails(int32, int32, - string) ([]*TransactionDetail, error) { + string, uint32, uint32) ([]*TransactionDetail, uint64, uint64, error) { - return nil, nil + return nil, 0, 0, nil } // LeaseOutput returns the current time and a nil error. diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go index 9910caefe5..c85aa20668 100644 --- a/lnwallet/test/test_interface.go +++ b/lnwallet/test/test_interface.go @@ -199,7 +199,9 @@ func assertTxInWallet(t *testing.T, w *lnwallet.LightningWallet, // We'll fetch all of our transaction and go through each one until // finding the expected transaction with its expected confirmation // status. - txs, err := w.ListTransactionDetails(0, btcwallet.UnconfirmedHeight, "") + txs, _, _, err := w.ListTransactionDetails( + 0, btcwallet.UnconfirmedHeight, "", 0, 1000, + ) require.NoError(t, err, "unable to retrieve transactions") for _, tx := range txs { if tx.Hash != txHash { @@ -1101,8 +1103,8 @@ func testListTransactionDetails(miner *rpctest.Harness, // should be confirmed. err = waitForWalletSync(miner, alice) require.NoError(t, err, "Couldn't sync Alice's wallet") - txDetails, err := alice.ListTransactionDetails( - startHeight, chainTip, "", + txDetails, _, _, err := alice.ListTransactionDetails( + startHeight, chainTip, "", 0, 1000, ) require.NoError(t, err, "unable to fetch tx details") @@ -1213,8 +1215,8 @@ func testListTransactionDetails(miner *rpctest.Harness, // unconfirmed transactions. The transaction above should be included // with a confirmation height of 0, indicating that it has not been // mined yet. - txDetails, err = alice.ListTransactionDetails( - chainTip, btcwallet.UnconfirmedHeight, "", + txDetails, _, _, err = alice.ListTransactionDetails( + chainTip, btcwallet.UnconfirmedHeight, "", 0, 1000, ) require.NoError(t, err, "unable to fetch tx details") var mempoolTxFound bool @@ -1266,7 +1268,9 @@ func testListTransactionDetails(miner *rpctest.Harness, // transactions from the last block. err = waitForWalletSync(miner, alice) require.NoError(t, err, "Couldn't sync Alice's wallet") - txDetails, err = alice.ListTransactionDetails(chainTip, chainTip, "") + txDetails, _, _, err = alice.ListTransactionDetails( + chainTip, chainTip, "", 0, 1000, + ) require.NoError(t, err, "unable to fetch tx details") var burnTxFound bool for _, txDetail := range txDetails { @@ -1307,13 +1311,116 @@ func testListTransactionDetails(miner *rpctest.Harness, // Query for transactions only in the latest block. We do not expect // any transactions to be returned. - txDetails, err = alice.ListTransactionDetails(chainTip, chainTip, "") + txDetails, _, _, err = alice.ListTransactionDetails( + chainTip, chainTip, "", 0, 1000, + ) require.NoError(t, err, "unexpected error") if len(txDetails) != 0 { t.Fatalf("expected 0 transactions, got: %v", len(txDetails)) } } +func testListTransactionDetailsOffset(miner *rpctest.Harness, + alice, _ *lnwallet.LightningWallet, t *testing.T) { + + // Create 5 new outputs spendable by the wallet. + const numTxns = 5 + const outputAmt = btcutil.SatoshiPerBitcoin + isOurAddress := make(map[string]bool) + txids := make(map[chainhash.Hash]struct{}) + for i := 0; i < numTxns; i++ { + addr, err := alice.NewAddress( + lnwallet.WitnessPubKey, false, + lnwallet.DefaultAccountName, + ) + require.NoError(t, err) + + isOurAddress[addr.EncodeAddress()] = true + script, err := txscript.PayToAddrScript(addr) + require.NoError(t, err) + + output := &wire.TxOut{ + Value: outputAmt, + PkScript: script, + } + txid, err := miner.SendOutputs([]*wire.TxOut{output}, 2500) + require.NoError(t, err) + txids[*txid] = struct{}{} + } + + // Get the miner's current best block height before we mine blocks. + _, startHeight, err := miner.Client.GetBestBlock() + require.NoError(t, err, "cannot get best block") + + // Generate 10 blocks to mine all the transactions created above. + const numBlocksMined = 10 + _, err = miner.Client.Generate(numBlocksMined) + require.NoError(t, err, "unable to mine blocks") + + // Our new best block height should be our start height + the number of + // blocks we just mined. + chainTip := startHeight + numBlocksMined + + err = waitForWalletSync(miner, alice) + require.NoError(t, err, "Couldn't sync Alice's wallet") + + // Query for transactions, setting max_transactions to 5. We expect 5 + // transactions to be returned. + txDetails, firstIdx, lastIdx, err := alice.ListTransactionDetails( + startHeight, chainTip, "", 0, 5, + ) + require.NoError(t, err) + require.Len(t, txDetails, 5) + require.EqualValues(t, 0, firstIdx) + require.EqualValues(t, 4, lastIdx) + + // Query for transactions, setting max_transactions to less than the + // number of transactions we have (5). + txDetails, _, _, err = alice.ListTransactionDetails( + startHeight, chainTip, "", 0, 1, + ) + require.NoError(t, err) + require.Len(t, txDetails, 1) + + // Query for transactions, setting indexOffset to 5 (equal to number + // of transactions we have) and max_transactions to 0. + txDetails, _, _, err = alice.ListTransactionDetails( + startHeight, chainTip, "", 5, 0, + ) + require.NoError(t, err) + require.Len(t, txDetails, 0) + + // Query for transactions, setting indexOffset to 4 (edge offset) and + // max_transactions to 0. + txDetails, _, _, err = alice.ListTransactionDetails( + startHeight, chainTip, "", 4, 0, + ) + require.NoError(t, err) + require.Len(t, txDetails, 1) + + // Query for transactions, setting max_transactions to 0. + txDetails, _, _, err = alice.ListTransactionDetails( + startHeight, chainTip, "", 0, 0, + ) + require.NoError(t, err) + require.Len(t, txDetails, 5) + + // Query for transactions, more than we have in the wallet (5). + txDetails, _, _, err = alice.ListTransactionDetails( + startHeight, chainTip, "", 0, 10, + ) + require.NoError(t, err) + require.Len(t, txDetails, 5) + + // Query for transactions where the offset is greater than the number + // of transactions available. + txDetails, _, _, err = alice.ListTransactionDetails( + startHeight, chainTip, "", 10, 100, + ) + require.NoError(t, err) + require.Len(t, txDetails, 0) +} + func testTransactionSubscriptions(miner *rpctest.Harness, alice, _ *lnwallet.LightningWallet, t *testing.T) { @@ -2812,6 +2919,10 @@ var walletTests = []walletTestCase{ name: "transaction details", test: testListTransactionDetails, }, + { + name: "transaction details offset", + test: testListTransactionDetailsOffset, + }, { name: "get transaction details", test: testGetTransactionDetails, diff --git a/rpcserver.go b/rpcserver.go index 60dbd66481..7e016e056c 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -6476,14 +6476,16 @@ func (r *rpcServer) GetTransactions(ctx context.Context, endHeight = req.EndHeight } - transactions, err := r.server.cc.Wallet.ListTransactionDetails( - req.StartHeight, endHeight, req.Account, - ) + txns, firstIdx, lastIdx, err := + r.server.cc.Wallet.ListTransactionDetails( + req.StartHeight, endHeight, req.Account, + req.IndexOffset, req.MaxTransactions, + ) if err != nil { return nil, err } - return lnrpc.RPCTransactionDetails(transactions), nil + return lnrpc.RPCTransactionDetails(txns, firstIdx, lastIdx), nil } // DescribeGraph returns a description of the latest graph state from the PoV