Repository: lightningnetwork/lnd (language: go)

Issue:
Provide ability to paginate transactions similar to invoices and payments
### Background

Payments and Invoices have standard controls for pagination that enable you to specify how many items to receive and what index to start from. Transactions have a different set of pagination controls that enable you to specify a start height and end height.

This makes it challenging to build a UI that has decent pagination of all core activity types (transactions, invoices and payments) since the mechanism to fetch the data varies depending on what data type is being fetched.

For Payments and Invoices I can easily fetch a certain 100 items. But for Transactions there is no way for me to fetch a certain block of 100 items - I don't know wether blocks X through Y contain any transactions at all, so finding the last 100 transactions can only be done through a process of trial and error.

The best I can do with the current api is to choose a particular start and end height to fetch transactions between and then count how many were fetched. If there were less than 100 then try fetching some more. Or, if there were more than 100, truncate the result. With an approach like this, I might need to make thousands of calls to GetTransactions just to find the last 100 transactions since I'd effectively have to keep calling GetTransactions with a lower and lower start and end height all the way back until the start of time.

### Your environment

* version of `lnd`: 0.11
* which operating system (`uname -a` on *Nix): all
* version of `btcd`, `bitcoind`, or other backend: any
* any other relevant environment details

### Steps to reproduce

Try building a paginator that pages through transactions in batches of 100.

### Expected behaviour

I should be able to specifically ask for 100 transactions form a given offset.

### Actual behaviour

I have to do a process of trial and error in order to fetch only the last 100 transactions, or transactions 100-200 etc.



Discussion / hints:
Need this, very hard to display transactions alongside invoices and payments, as transactions do not support pagination, and transactions use block height for filter instead of dates.
This is important also to avoid timeout issues, although the workaround of block heights could help? Maybe there is also a documentation aspect to this on how to use block heights most efficiently when normal paging isn't available @saubyk 
I will be working on addressing this issue 
I have researched this issue, including how pagination controls were implemented in the `payments` [here](https://github.com/lightningnetwork/lnd/blob/fdd28c8d888792ea8fde3c557ba9f2594e0a6ec8/rpcserver.go#L6778C1-L6779C1) and `invoices` [here](https://github.com/lightningnetwork/lnd/blob/fdd28c8d888792ea8fde3c557ba9f2594e0a6ec8/rpcserver.go#L5864) RPCs. From what I have observed for `listchaintxns` RPC, transactions are retrieved from a node's wallet using an external package  [btcwallet](https://github.com/btcsuite/btcwallet). 

I have a question: would it be necessary to open a pull request in `btcwallet` to modify the [GetTransactions](https://github.com/btcsuite/btcwallet/blob/e391a1c31df220939102b26724f863925fbbcde6/wallet/wallet.go#L2393) function to accommodate the new controls that will be added to the `listchaintxns` RPC? Or is there a better way to achieve this?

cc: @mrfelton @alexbosworth @Roasbeef 
Hi @Abdulkbk you're correct that in order to make this efficient for transactions, we'd need to modify `GetTransaction` in btcwallet. Right now it's based solely on start and end height, but we also want it to be aware of the number of transaction to return, and also an offset. You should be able to use a database cursor to handle that.
> Hi @Abdulkbk you're correct that in order to make this efficient for transactions, we'd need to modify `GetTransaction` in btcwallet. Right now it's based solely on start and end height, but we also want it to be aware of the number of transaction to return, and also an offset. You should be able to use a database cursor to handle that.

Do you mean I should be able to use a database cursor to handle this without necessarily making any changes to `GetTransactions` in btcwallet? I want to make sure I understand this before proceeding. Thank you.
So you can use the database interface used in btcwallet to obtain a database cursor. 

Fix patch:
diff --git a/cmd/commands/commands.go b/cmd/commands/commands.go
index de13022f0e..2411e424b8 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 2f8a9c42ad..735f2e0822 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
@@ -18543,20 +18583,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 3d554352bd..993b944090 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 64c36d17da..50f38a5e51 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": [
@@ -7488,6 +7520,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/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/rpcserver.go b/rpcserver.go
index efb6cf54ff..b528a89f17 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


Test patch:
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/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,