Skip to content

Commit 9244a3b

Browse files
committed
Migrate payment history to node pagination
Read payment history directly from LDK Node and stop duplicating payment records in the server database. Retain the server store for forwarded-payment history. Replace structured page tokens with opaque strings across both listing APIs and adapt CLI pagination to pass them through. Existing clients must update for the token schema change. AI assistance: OpenAI Codex was used for this change.
1 parent 3e9a07d commit 9244a3b

14 files changed

Lines changed: 125 additions & 179 deletions

File tree

docs/api-guide.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,11 @@ force-closure of the channel.
271271

272272
`ListPayments` and `ListForwardedPayments` support cursor-based pagination:
273273

274-
1. Make the first request with your desired `number_of_payments` page size.
274+
1. Make the first request without a `page_token`. The server controls the page size.
275275
2. If the response includes a `next_page_token`, pass it as `page_token` in the next request.
276276
3. When `next_page_token` is absent, you have reached the end of the results.
277277

278-
Results are ordered by creation time (most recent first).
278+
The page token is one opaque string. Do not parse or modify it. Results are ordered by creation
279+
time (most recent first).
280+
281+
The CLI `--number-of-payments` option combines multiple pages. It does not set the gRPC page size.

docs/configuration.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,12 @@ Two resolution methods are supported via the `mode` field:
209209
<network>/ # e.g., bitcoin/, regtest/, signet/
210210
api_key # API key
211211
ldk-server.log # Log file
212-
ldk_node_data.sqlite # LDK Node state (channels, on-chain wallet)
213-
ldk_server_data.sqlite # Payment and forwarding history
212+
ldk_node_data.sqlite # LDK Node state (channels, wallet, payments)
213+
ldk_server_data.sqlite # Forwarded-payment history
214214
```
215215

216216
The mnemonic is the node's master secret, required to recover on-chain funds. On first start,
217217
ldk-server generates a fresh 24-word BIP39 mnemonic at `<storage_dir>/keys_mnemonic` if the file
218-
does not already exist. `ldk_node_data.sqlite` holds channel state, both are required to recover
219-
channel funds. See [Operations - Backups](operations.md#backups) for backup guidance.
218+
does not already exist. `ldk_node_data.sqlite` holds channel state and payment history. Both files
219+
are required to recover channel funds. See [Operations - Backups](operations.md#backups) for backup
220+
guidance.

docs/operations.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ the following config to `/etc/logrotate.d/ldk-server` (adjust the log path to ma
5353
| File | Priority | Description |
5454
| -------------------------------------- | ------------ | -------------------------------------------------------------------------- |
5555
| `<storage_dir>/keys_mnemonic` | **Critical** | BIP39 mnemonic. Required to recover on-chain funds. Default for new installs. |
56-
| `<network_dir>/ldk_node_data.sqlite` | **Critical** | Channel state and on-chain wallet data. Required to recover channel funds. |
57-
| `<network_dir>/ldk_server_data.sqlite` | Nice-to-have | Payment and forwarding history |
56+
| `<network_dir>/ldk_node_data.sqlite` | **Critical** | Channel state, on-chain wallet data, and payment history. Required to recover channel funds. |
57+
| `<network_dir>/ldk_server_data.sqlite` | Nice-to-have | Forwarded-payment history |
5858

5959
### What is Reconstructable
6060

ldk-server-cli/src/main.rs

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ use ldk_server_client::ldk_server_grpc::api::{
5050
};
5151
use ldk_server_client::ldk_server_grpc::types::{
5252
bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, CustomTlvRecord,
53-
PageToken, PayerProofOptions, RouteParametersConfig,
53+
PayerProofOptions, RouteParametersConfig,
5454
};
5555
use ldk_server_client::{
5656
DEFAULT_EXPIRY_SECS, DEFAULT_MAX_CHANNEL_SATURATION_POWER_OF_HALF, DEFAULT_MAX_PATH_COUNT,
@@ -540,7 +540,7 @@ enum Commands {
540540
)]
541541
number_of_payments: Option<u64>,
542542
#[arg(long)]
543-
#[arg(help = "Page token to continue from a previous page (format: token:index)")]
543+
#[arg(help = "Opaque page token returned by a previous request")]
544544
page_token: Option<String>,
545545
},
546546
#[command(about = "Get details of a specific payment by its payment ID")]
@@ -556,7 +556,7 @@ enum Commands {
556556
help = "Fetch at least this many forwarded payments by iterating through multiple pages. Returns combined results with the last page token. If not provided, returns only a single page."
557557
)]
558558
number_of_payments: Option<u64>,
559-
#[arg(long, help = "Page token to continue from a previous page (format: token:index)")]
559+
#[arg(long, help = "Opaque page token returned by a previous request")]
560560
page_token: Option<String>,
561561
},
562562
#[command(about = "Update the forwarding fees and CLTV expiry delta for an existing channel")]
@@ -1174,9 +1174,6 @@ async fn main() {
11741174
);
11751175
},
11761176
Commands::ListPayments { number_of_payments, page_token } => {
1177-
let page_token = page_token
1178-
.map(|token_str| parse_page_token(&token_str).unwrap_or_else(|e| handle_error(e)));
1179-
11801177
handle_response_result::<_, CliListPaymentsResponse>(
11811178
fetch_paginated(
11821179
number_of_payments,
@@ -1193,9 +1190,6 @@ async fn main() {
11931190
);
11941191
},
11951192
Commands::ListForwardedPayments { number_of_payments, page_token } => {
1196-
let page_token = page_token
1197-
.map(|token_str| parse_page_token(&token_str).unwrap_or_else(|e| handle_error(e)));
1198-
11991193
handle_response_result::<_, CliListForwardedPaymentsResponse>(
12001194
fetch_paginated(
12011195
number_of_payments,
@@ -1334,9 +1328,8 @@ fn build_open_channel_config(
13341328
}
13351329

13361330
async fn fetch_paginated<T, R, Fut>(
1337-
target_count: Option<u64>, initial_page_token: Option<PageToken>,
1338-
fetch_page: impl Fn(Option<PageToken>) -> Fut,
1339-
extract: impl Fn(R) -> (Vec<T>, Option<PageToken>),
1331+
target_count: Option<u64>, initial_page_token: Option<String>,
1332+
fetch_page: impl Fn(Option<String>) -> Fut, extract: impl Fn(R) -> (Vec<T>, Option<String>),
13401333
) -> Result<CliPaginatedResponse<T>, LdkServerError>
13411334
where
13421335
Fut: std::future::Future<Output = Result<R, LdkServerError>>,
@@ -1446,20 +1439,6 @@ fn parse_bolt11_invoice_description(
14461439
}
14471440
}
14481441

1449-
fn parse_page_token(token_str: &str) -> Result<PageToken, LdkServerError> {
1450-
let parts: Vec<&str> = token_str.split(':').collect();
1451-
if parts.len() != 2 {
1452-
return Err(LdkServerError::new(
1453-
InvalidRequestError,
1454-
"Page token must be in format 'token:index'".to_string(),
1455-
));
1456-
}
1457-
let index = parts[1].parse::<i64>().map_err(|_| {
1458-
LdkServerError::new(InvalidRequestError, "Invalid page token index".to_string())
1459-
})?;
1460-
Ok(PageToken { token: parts[0].to_string(), index })
1461-
}
1462-
14631442
fn parse_custom_tlv(s: &str) -> Result<(u64, Vec<u8>), String> {
14641443
let (type_str, hex_str) =
14651444
s.split_once(':').ok_or_else(|| format!("expected <type_num>:<hex_value>, got '{s}'"))?;
@@ -1494,6 +1473,31 @@ fn handle_error(e: LdkServerError) -> ! {
14941473
mod tests {
14951474
use super::*;
14961475

1476+
#[tokio::test]
1477+
async fn fetch_paginated_collects_multiple_pages() {
1478+
let response = fetch_paginated(
1479+
Some(3),
1480+
None,
1481+
|page_token| async move {
1482+
match page_token {
1483+
None => {
1484+
Ok::<_, LdkServerError>((vec![1, 2], Some("store:v2:cursor:7".to_string())))
1485+
},
1486+
Some(token) => {
1487+
assert_eq!(token, "store:v2:cursor:7");
1488+
Ok((vec![3], None))
1489+
},
1490+
}
1491+
},
1492+
|response| response,
1493+
)
1494+
.await
1495+
.unwrap();
1496+
1497+
assert_eq!(response.list, vec![1, 2, 3]);
1498+
assert!(response.next_page_token.is_none());
1499+
}
1500+
14971501
#[test]
14981502
fn parse_custom_tlv_accepts_valid_record() {
14991503
let (type_num, value) = parse_custom_tlv("65537:deadbeef").unwrap();

ldk-server-cli/src/types.rs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ use std::fmt;
1717
use std::str::FromStr;
1818

1919
use hex_conservative::{DisplayHex, FromHex};
20-
use ldk_server_client::ldk_server_grpc::types::{ForwardedPayment, PageToken, Payment};
20+
use ldk_server_client::ldk_server_grpc::types::{ForwardedPayment, Payment};
2121
use serde::Serialize;
2222

23-
/// CLI-specific wrapper for paginated responses that formats the page token
24-
/// as "token:idx" instead of a JSON object.
23+
/// CLI-specific wrapper for paginated responses that keeps the page token as
24+
/// one opaque string.
2525
#[derive(Debug, Clone, Serialize)]
2626
pub struct CliPaginatedResponse<T> {
2727
/// List of items.
@@ -32,18 +32,14 @@ pub struct CliPaginatedResponse<T> {
3232
}
3333

3434
impl<T> CliPaginatedResponse<T> {
35-
pub fn new(list: Vec<T>, next_page_token: Option<PageToken>) -> Self {
36-
Self { list, next_page_token: next_page_token.map(format_page_token) }
35+
pub fn new(list: Vec<T>, next_page_token: Option<String>) -> Self {
36+
Self { list, next_page_token }
3737
}
3838
}
3939

4040
pub type CliListPaymentsResponse = CliPaginatedResponse<Payment>;
4141
pub type CliListForwardedPaymentsResponse = CliPaginatedResponse<ForwardedPayment>;
4242

43-
fn format_page_token(token: PageToken) -> String {
44-
format!("{}:{}", token.token, token.index)
45-
}
46-
4743
/// A denomination-aware amount that stores its value internally in millisatoshis.
4844
///
4945
/// Accepts the following formats when parsed from a string:

ldk-server-grpc/src/api.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -896,14 +896,14 @@ pub struct GetPaymentDetailsResponse {
896896
#[allow(clippy::derive_partial_eq_without_eq)]
897897
#[derive(Clone, PartialEq, ::prost::Message)]
898898
pub struct ListPaymentsRequest {
899-
/// `page_token` is a pagination token.
899+
/// `page_token` is an opaque pagination token string.
900900
///
901901
/// To query for the first page, `page_token` must not be specified.
902902
///
903903
/// For subsequent pages, use the value that was returned as `next_page_token` in the previous
904904
/// page's response.
905-
#[prost(message, optional, tag = "1")]
906-
pub page_token: ::core::option::Option<super::types::PageToken>,
905+
#[prost(string, optional, tag = "1")]
906+
pub page_token: ::core::option::Option<::prost::alloc::string::String>,
907907
}
908908
/// The response for the `ListPayments` RPC. On failure, a gRPC error status is returned.
909909
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -915,7 +915,8 @@ pub struct ListPaymentsResponse {
915915
/// List of payments.
916916
#[prost(message, repeated, tag = "1")]
917917
pub payments: ::prost::alloc::vec::Vec<super::types::Payment>,
918-
/// `next_page_token` is a pagination token, used to retrieve the next page of results.
918+
/// `next_page_token` is an opaque pagination token string used to retrieve the next page of
919+
/// results.
919920
/// Use this value to query for next-page of paginated operation, by specifying
920921
/// this value as the `page_token` in the next request.
921922
///
@@ -928,8 +929,8 @@ pub struct ListPaymentsResponse {
928929
///
929930
/// **Caution**: Clients must not assume a specific number of records to be present in a page for
930931
/// paginated response.
931-
#[prost(message, optional, tag = "2")]
932-
pub next_page_token: ::core::option::Option<super::types::PageToken>,
932+
#[prost(string, optional, tag = "2")]
933+
pub next_page_token: ::core::option::Option<::prost::alloc::string::String>,
933934
}
934935
/// Retrieves list of all forwarded payments.
935936
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/enum.Event.html#variant.PaymentForwarded>
@@ -939,14 +940,14 @@ pub struct ListPaymentsResponse {
939940
#[allow(clippy::derive_partial_eq_without_eq)]
940941
#[derive(Clone, PartialEq, ::prost::Message)]
941942
pub struct ListForwardedPaymentsRequest {
942-
/// `page_token` is a pagination token.
943+
/// `page_token` is an opaque pagination token string.
943944
///
944945
/// To query for the first page, `page_token` must not be specified.
945946
///
946947
/// For subsequent pages, use the value that was returned as `next_page_token` in the previous
947948
/// page's response.
948-
#[prost(message, optional, tag = "1")]
949-
pub page_token: ::core::option::Option<super::types::PageToken>,
949+
#[prost(string, optional, tag = "1")]
950+
pub page_token: ::core::option::Option<::prost::alloc::string::String>,
950951
}
951952
/// The response for the `ListForwardedPayments` RPC. On failure, a gRPC error status is returned.
952953
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -958,7 +959,8 @@ pub struct ListForwardedPaymentsResponse {
958959
/// List of forwarded payments.
959960
#[prost(message, repeated, tag = "1")]
960961
pub forwarded_payments: ::prost::alloc::vec::Vec<super::types::ForwardedPayment>,
961-
/// `next_page_token` is a pagination token, used to retrieve the next page of results.
962+
/// `next_page_token` is an opaque pagination token string used to retrieve the next page of
963+
/// results.
962964
/// Use this value to query for next-page of paginated operation, by specifying
963965
/// this value as the `page_token` in the next request.
964966
///
@@ -971,8 +973,8 @@ pub struct ListForwardedPaymentsResponse {
971973
///
972974
/// **Caution**: Clients must not assume a specific number of records to be present in a page for
973975
/// paginated response.
974-
#[prost(message, optional, tag = "2")]
975-
pub next_page_token: ::core::option::Option<super::types::PageToken>,
976+
#[prost(string, optional, tag = "2")]
977+
pub next_page_token: ::core::option::Option<::prost::alloc::string::String>,
976978
}
977979
/// Sign a message with the node's secret key.
978980
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.sign_message>

ldk-server-grpc/src/proto/api.proto

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -650,21 +650,22 @@ message GetPaymentDetailsResponse {
650650
// Retrieves list of all payments.
651651
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.list_payments
652652
message ListPaymentsRequest {
653-
// `page_token` is a pagination token.
653+
// `page_token` is an opaque pagination token string.
654654
//
655655
// To query for the first page, `page_token` must not be specified.
656656
//
657657
// For subsequent pages, use the value that was returned as `next_page_token` in the previous
658658
// page's response.
659-
optional types.PageToken page_token = 1;
659+
optional string page_token = 1;
660660
}
661661

662662
// The response for the `ListPayments` RPC. On failure, a gRPC error status is returned.
663663
message ListPaymentsResponse {
664664
// List of payments.
665665
repeated types.Payment payments = 1;
666666

667-
// `next_page_token` is a pagination token, used to retrieve the next page of results.
667+
// `next_page_token` is an opaque pagination token string used to retrieve the next page of
668+
// results.
668669
// Use this value to query for next-page of paginated operation, by specifying
669670
// this value as the `page_token` in the next request.
670671
//
@@ -677,27 +678,28 @@ message ListPaymentsResponse {
677678
//
678679
// **Caution**: Clients must not assume a specific number of records to be present in a page for
679680
// paginated response.
680-
optional types.PageToken next_page_token = 2;
681+
optional string next_page_token = 2;
681682
}
682683

683684
// Retrieves list of all forwarded payments.
684685
// See more: https://docs.rs/ldk-node/latest/ldk_node/enum.Event.html#variant.PaymentForwarded
685686
message ListForwardedPaymentsRequest {
686-
// `page_token` is a pagination token.
687+
// `page_token` is an opaque pagination token string.
687688
//
688689
// To query for the first page, `page_token` must not be specified.
689690
//
690691
// For subsequent pages, use the value that was returned as `next_page_token` in the previous
691692
// page's response.
692-
optional types.PageToken page_token = 1;
693+
optional string page_token = 1;
693694
}
694695

695696
// The response for the `ListForwardedPayments` RPC. On failure, a gRPC error status is returned.
696697
message ListForwardedPaymentsResponse {
697698
// List of forwarded payments.
698699
repeated types.ForwardedPayment forwarded_payments = 1;
699700

700-
// `next_page_token` is a pagination token, used to retrieve the next page of results.
701+
// `next_page_token` is an opaque pagination token string used to retrieve the next page of
702+
// results.
701703
// Use this value to query for next-page of paginated operation, by specifying
702704
// this value as the `page_token` in the next request.
703705
//
@@ -710,7 +712,7 @@ message ListForwardedPaymentsResponse {
710712
//
711713
// **Caution**: Clients must not assume a specific number of records to be present in a page for
712714
// paginated response.
713-
optional types.PageToken next_page_token = 2;
715+
optional string next_page_token = 2;
714716
}
715717

716718
// Sign a message with the node's secret key.

ldk-server-grpc/src/proto/types.proto

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -880,12 +880,6 @@ message AwaitingThresholdConfirmations {
880880
uint64 amount_satoshis = 5;
881881
}
882882

883-
// Token used to determine start of next page in paginated APIs.
884-
message PageToken {
885-
string token = 1;
886-
int64 index = 2;
887-
}
888-
889883
message Bolt11InvoiceDescription {
890884
oneof kind {
891885
string direct = 1;

ldk-server-grpc/src/types.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,18 +1087,6 @@ pub struct AwaitingThresholdConfirmations {
10871087
#[prost(uint64, tag = "5")]
10881088
pub amount_satoshis: u64,
10891089
}
1090-
/// Token used to determine start of next page in paginated APIs.
1091-
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1092-
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
1093-
#[cfg_attr(feature = "serde", serde(default))]
1094-
#[allow(clippy::derive_partial_eq_without_eq)]
1095-
#[derive(Clone, PartialEq, ::prost::Message)]
1096-
pub struct PageToken {
1097-
#[prost(string, tag = "1")]
1098-
pub token: ::prost::alloc::string::String,
1099-
#[prost(int64, tag = "2")]
1100-
pub index: i64,
1101-
}
11021090
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11031091
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
11041092
#[cfg_attr(feature = "serde", serde(default))]

ldk-server-mcp/src/tools/schema.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,8 @@ fn channel_config_schema() -> Value {
132132

133133
fn page_token_schema() -> Value {
134134
json!({
135-
"type": "object",
136-
"description": "Pagination token from a previous response",
137-
"properties": {
138-
"token": { "type": "string" },
139-
"index": { "type": "integer" }
140-
},
141-
"required": ["token", "index"]
135+
"type": "string",
136+
"description": "Opaque pagination token from a previous response"
142137
})
143138
}
144139

0 commit comments

Comments
 (0)