Repository: lightningnetwork/lnd (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.


Pull request:
pagination: add pagination to wallet transactions
fixes #4719 

## Change Description
This PR adds pagination to getting transactions RPC calls by introducing index_offset and max_transactions
to specify the number of transactions to skip and the maximum number of transactions to return respectively. 

## Steps to Test
Steps for reviewers to follow to test the change.
- Start LND
- Run `lncli listchaintxns -index_offset=n -max_transactions=m ` where n and m are any numbers
- The result will be a list skipping `n` numbers of transactions and with `m` transactions

## Pull Request Checklist
### Testing
- [ ] Your PR passes all CI checks.
- [ ] Tests covering the positive and negative (error paths) are included.
- [ ] Bug fixes contain tests triggering the bug to prevent regressions.

### Code Style and Documentation
- [ ] The change obeys the [Code Documentation and Commenting](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#CodeDocumentation) guidelines, and lines wrap at 80.
- [ ] Commits follow the [Ideal Git Commit Structure](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#IdealGitCommitStructure).
- [ ] Any new logging statements use an appropriate subsystem and logging level.
- [ ] Any new lncli commands have appropriate tags in the comments for the rpc in the proto file.
- [ ] [There is a change description in the release notes](https://github.com/lightningnetwork/lnd/tree/master/docs/release-notes), or `[skip ci]` in the commit message for small changes.

📝 Please see our [Contribution Guidelines](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md) for further guidance.


Patch:
diff --git a/cmd/commands/commands.go b/cmd/commands/commands.go
index de13022f0..2411e424b 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 12049b40c..6fa3c0f06 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/fn/go.mod b/fn/go.mod
index 89dc280df..1780b9968 100644
--- a/fn/go.mod
+++ b/fn/go.mod
@@ -1,4 +1,4 @@
-module github.com/lightningnetwork/lnd/fn/v2
+module github.com/lightningnetwork/lnd/fn
 
 go 1.19
 
diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go
index 2f8a9c42a..735f2e082 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 3d554352b..993b94409 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 64c36d17d..50f38a5e5 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 eff0a5a53..15e3f2f76 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 5acafa67d..30d806af9 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 b15012f0c..3b16f4da7 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 d0444edd5..ee62bdb27 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/channel.go b/lnwallet/channel.go
index 74ecd1745..1e61abe3f 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -3595,13 +3595,11 @@ func (b BufferType) String() string {
 
 // applyCommitFee applies the commitFee including a buffer to the balance amount
 // and verifies that it does not become negative. This function returns the new
-// balance, the exact buffer amount (excluding the commitment fee) and the
-// commitment fee.
+// balance and the exact buffer amount (excluding the commitment fee).
 func (lc *LightningChannel) applyCommitFee(
 	balance lnwire.MilliSatoshi, commitWeight lntypes.WeightUnit,
 	feePerKw chainfee.SatPerKWeight,
-	buffer BufferType) (lnwire.MilliSatoshi, lnwire.MilliSatoshi,
-	lnwire.MilliSatoshi, error) {
+	buffer BufferType) (lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
 
 	commitFee := feePerKw.FeeForWeight(commitWeight)
 	commitFeeMsat := lnwire.NewMSatFromSatoshis(commitFee)
@@ -3616,16 +3614,14 @@ func (lc *LightningChannel) applyCommitFee(
 		// Make sure that we are the initiator of the channel before we
 		// apply the FeeBuffer.
 		if !lc.channelState.IsInitiator {
-			return 0, 0, 0, ErrFeeBufferNotInitiator
+			return 0, 0, ErrFeeBufferNotInitiator
 		}
 
 		// The FeeBuffer already includes the commitFee.
 		bufferAmt = CalcFeeBuffer(feePerKw, commitWeight)
 		if bufferAmt < balance {
 			newBalance := balance - bufferAmt
-
-			return newBalance, bufferAmt - commitFeeMsat,
-				commitFeeMsat, nil
+			return newBalance, bufferAmt - commitFeeMsat, nil
 		}
 
 	// The AdditionalHtlc buffer type does NOT keep a FeeBuffer but solely
@@ -3639,7 +3635,7 @@ func (lc *LightningChannel) applyCommitFee(
 		bufferAmt = commitFeeMsat + additionalHtlcFee
 		newBalance := balance - bufferAmt
 		if bufferAmt < balance {
-			return newBalance, additionalHtlcFee, commitFeeMsat, nil
+			return newBalance, additionalHtlcFee, nil
 		}
 
 	// The default case does not account for any buffer on the local balance
@@ -3647,13 +3643,13 @@ func (lc *LightningChannel) applyCommitFee(
 	default:
 		if commitFeeMsat < balance {
 			newBalance := balance - commitFeeMsat
-			return newBalance, 0, commitFeeMsat, nil
+			return newBalance, 0, nil
 		}
 	}
 
 	// We still return the amount and bufferAmt here to log them at a later
 	// stage.
-	return balance, bufferAmt, commitFeeMsat, ErrBelowChanReserve
+	return balance, bufferAmt, ErrBelowChanReserve
 }
 
 // validateCommitmentSanity is used to validate the current state of the
@@ -3711,21 +3707,17 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
 	// includes also a buffer type. Depending on whether we are the opener
 	// of the channel we either want to enforce a buffer on the local
 	// amount.
-	var (
-		bufferAmt lnwire.MilliSatoshi
-		commitFee lnwire.MilliSatoshi
-	)
-
+	var bufferAmt lnwire.MilliSatoshi
 	if lc.channelState.IsInitiator {
-		ourBalance, bufferAmt, commitFee, err = lc.applyCommitFee(
-			ourBalance, commitWeight, feePerKw, buffer,
-		)
+		ourBalance, bufferAmt, err = lc.applyCommitFee(
+			ourBalance, commitWeight, feePerKw, buffer)
 		if err != nil {
+			commitFee := feePerKw.FeeForWeight(commitWeight)
 			lc.log.Errorf("Cannot pay for the CommitmentFee of "+
 				"the ChannelState: ourBalance is negative "+
 				"after applying the fee: ourBalance=%v, "+
 				"commitFee=%v, feeBuffer=%v (type=%v) "+
-				"local_chan_initiator", ourBalance,
+				"local_chan_initiator", int64(ourBalance),
 				commitFee, bufferAmt, buffer)
 
 			return err
@@ -3738,16 +3730,17 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
 		// stuck because locally we will never put another outgoing HTLC
 		// on the channel state. The FeeBuffer should ONLY be enforced
 		// if we locally pay for the commitment transaction.
-		theirBalance, bufferAmt, commitFee, err = lc.applyCommitFee(
-			theirBalance, commitWeight, feePerKw, NoBuffer,
-		)
+		theirBalance, bufferAmt, err = lc.applyCommitFee(
+			theirBalance, commitWeight, feePerKw, NoBuffer)
 		if err != nil {
+			commitFee := feePerKw.FeeForWeight(commitWeight)
 			lc.log.Errorf("Cannot pay for the CommitmentFee "+
 				"of the ChannelState: theirBalance is "+
 				"negative after applying the fee: "+
-				"theirBalance=%v, commitFee=%v, feeBuffer=%v "+
+				"theiBalance=%v, commitFee=%v, feeBuffer=%v "+
 				"(type=%v) remote_chan_initiator",
-				theirBalance, commitFee, bufferAmt, buffer)
+				int64(theirBalance), commitFee, bufferAmt,
+				buffer)
 
 			return err
 		}
@@ -3764,6 +3757,10 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
 		lc.channelState.RemoteChanCfg.ChanReserve,
 	)
 
+	// Calculate the commitment fee to log the information if needed.
+	commitFee := feePerKw.FeeForWeight(commitWeight)
+	commitFeeMsat := lnwire.NewMSatFromSatoshis(commitFee)
+
 	switch {
 	// TODO(ziggie): Allow the peer dip us below the channel reserve when
 	// our local balance would increase during this commitment dance or
@@ -3777,7 +3774,7 @@ func (lc *LightningChannel) validateCommitmentSanity(theirLogCounter,
 		lc.log.Debugf("Funds below chan reserve: ourBalance=%v, "+
 			"ourReserve=%v, commitFee=%v, feeBuffer=%v "+
 			"chan_initiator=%v", ourBalance, ourReserve,
-			commitFee, bufferAmt, lc.channelState.IsInitiator)
+			commitFeeMsat, bufferAmt, lc.channelState.IsInitiator)
 
 		return fmt.Errorf("%w: our balance below chan reserve",
 			ErrBelowChanReserve)
@@ -8799,7 +8796,7 @@ func (lc *LightningChannel) availableCommitmentBalance(view *HtlcView,
 		// Make sure we do not overwrite `ourBalance` that's why we
 		// declare bufferAmt beforehand.
 		var bufferAmt lnwire.MilliSatoshi
-		ourBalance, bufferAmt, commitFee, err = lc.applyCommitFee(
+		ourBalance, bufferAmt, err = lc.applyCommitFee(
 			ourBalance, futureCommitWeight, feePerKw, buffer,
 		)
 		if err != nil {
@@ -8809,7 +8806,8 @@ func (lc *LightningChannel) availableCommitmentBalance(view *HtlcView,
 				"after applying the fee: ourBalance=%v, "+
 				"current commitFee(w/o additional htlc)=%v, "+
 				"feeBuffer=%v (type=%v) local_chan_initiator",
-				ourBalance, commitFee, bufferAmt, buffer)
+				int64(ourBalance), commitFee,
+				bufferAmt, buffer)
 
 			return 0, commitWeight
 		}
diff --git a/lnwallet/interface.go b/lnwallet/interface.go
index e2e491c73..c9dee9202 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 6623d8014..a8610dc77 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 efb6cf54f..b528a89f1 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
diff --git a/server.go b/server.go
index c13175912..c46c1a5f9 100644
--- a/server.go
+++ b/server.go
@@ -2481,9 +2481,6 @@ func (s *server) Stop() error {
 		if err := s.chanRouter.Stop(); err != nil {
 			srvrLog.Warnf("failed to stop chanRouter: %v", err)
 		}
-		if err := s.graphBuilder.Stop(); err != nil {
-			srvrLog.Warnf("failed to stop graphBuilder %v", err)
-		}
 		if err := s.chainArb.Stop(); err != nil {
 			srvrLog.Warnf("failed to stop chainArb: %v", err)
 		}


Test patch:
diff --git a/lntest/mock/walletcontroller.go b/lntest/mock/walletcontroller.go
index fd4a43348..8b7ef5538 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/channel_test.go b/lnwallet/channel_test.go
index 8fc3d8b46..2e037c90b 100644
--- a/lnwallet/channel_test.go
+++ b/lnwallet/channel_test.go
@@ -10299,7 +10299,6 @@ func TestApplyCommitmentFee(t *testing.T) {
 		balance           lnwire.MilliSatoshi
 		expectedBalance   lnwire.MilliSatoshi
 		expectedBufferAmt lnwire.MilliSatoshi
-		expectedCommitFee lnwire.MilliSatoshi
 		bufferAmt         lnwire.MilliSatoshi
 		expectedErr       error
 	}{
@@ -10310,7 +10309,6 @@ func TestApplyCommitmentFee(t *testing.T) {
 			balance:           balance,
 			expectedBalance:   balance - feeBuffer,
 			expectedBufferAmt: feeBuffer - commitFee,
-			expectedCommitFee: commitFee,
 		},
 		{
 			name:        "apply feebuffer remote initiator",
@@ -10326,7 +10324,6 @@ func TestApplyCommitmentFee(t *testing.T) {
 			balance:           balance,
 			expectedBalance:   balance - commitFee - additionalHtlc,
 			expectedBufferAmt: additionalHtlc,
-			expectedCommitFee: commitFee,
 		},
 		{
 			name:              "apply NoBuffer",
@@ -10335,7 +10332,6 @@ func TestApplyCommitmentFee(t *testing.T) {
 			balance:           balance,
 			expectedBalance:   balance - commitFee,
 			expectedBufferAmt: 0,
-			expectedCommitFee: commitFee,
 		},
 		{
 			name:              "apply FeeBuffer balance negative",
@@ -10345,22 +10341,18 @@ func TestApplyCommitmentFee(t *testing.T) {
 			expectedBalance:   balanceBelowReserve,
 			expectedErr:       ErrBelowChanReserve,
 			expectedBufferAmt: feeBuffer,
-			expectedCommitFee: commitFee,
 		},
 	}
 
 	for _, tc := range testCases {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			//nolint:lll
-			balance, bufferAmt, commitFee, err := tc.channel.applyCommitFee(
-				tc.balance, commitWeight, feePerKw, tc.buffer,
-			)
+			balance, bufferAmt, err := tc.channel.applyCommitFee(
+				tc.balance, commitWeight, feePerKw, tc.buffer)
 
 			require.ErrorIs(t, err, tc.expectedErr)
 			require.Equal(t, tc.expectedBalance, balance)
 			require.Equal(t, tc.expectedBufferAmt, bufferAmt)
-			require.Equal(t, tc.expectedCommitFee, commitFee)
 		})
 	}
 }
diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go
index 9910caefe..c85aa2066 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,