Skip to content

Commit ffdb37f

Browse files
committed
fixup! Use ldk-node payment pagination
Expose page tokens as one opaque string across the gRPC, CLI, and MCP interfaces. This keeps storage-specific token fields out of the public API. Keep the forwarded-payment cursor encoding inside the server and reject malformed cursors as invalid requests. AI assistance: OpenAI Codex was used for this change.
1 parent cf27bff commit ffdb37f

10 files changed

Lines changed: 79 additions & 104 deletions

File tree

docs/api-guide.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ force-closure of the channel.
268268
2. If the response includes a `next_page_token`, pass it as `page_token` in the next request.
269269
3. When `next_page_token` is absent, you have reached the end of the results.
270270

271-
Results are ordered by creation time (most recent first).
271+
The page token is one opaque string. Do not parse or modify it. Results are ordered by creation
272+
time (most recent first).
272273

273274
The CLI `--number-of-payments` option combines multiple pages. It does not set the gRPC page size.

ldk-server-cli/src/main.rs

Lines changed: 9 additions & 38 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,19 +1439,6 @@ fn parse_bolt11_invoice_description(
14461439
}
14471440
}
14481441

1449-
fn parse_page_token(token_str: &str) -> Result<PageToken, LdkServerError> {
1450-
let (token, index) = token_str.rsplit_once(':').ok_or_else(|| {
1451-
LdkServerError::new(
1452-
InvalidRequestError,
1453-
"Page token must be in format 'token:index'".to_string(),
1454-
)
1455-
})?;
1456-
let index = index.parse::<i64>().map_err(|_| {
1457-
LdkServerError::new(InvalidRequestError, "Invalid page token index".to_string())
1458-
})?;
1459-
Ok(PageToken { token: token.to_string(), index })
1460-
}
1461-
14621442
fn parse_custom_tlv(s: &str) -> Result<(u64, Vec<u8>), String> {
14631443
let (type_str, hex_str) =
14641444
s.split_once(':').ok_or_else(|| format!("expected <type_num>:<hex_value>, got '{s}'"))?;
@@ -1500,13 +1480,11 @@ mod tests {
15001480
None,
15011481
|page_token| async move {
15021482
match page_token {
1503-
None => Ok::<_, LdkServerError>((
1504-
vec![1, 2],
1505-
Some(PageToken { token: "store:v2:cursor".to_string(), index: 7 }),
1506-
)),
1483+
None => {
1484+
Ok::<_, LdkServerError>((vec![1, 2], Some("store:v2:cursor:7".to_string())))
1485+
},
15071486
Some(token) => {
1508-
assert_eq!(token.token, "store:v2:cursor");
1509-
assert_eq!(token.index, 7);
1487+
assert_eq!(token, "store:v2:cursor:7");
15101488
Ok((vec![3], None))
15111489
},
15121490
}
@@ -1520,13 +1498,6 @@ mod tests {
15201498
assert!(response.next_page_token.is_none());
15211499
}
15221500

1523-
#[test]
1524-
fn parse_page_token_accepts_colons_in_token() {
1525-
let token = parse_page_token("store:v2:cursor:7").unwrap();
1526-
assert_eq!(token.token, "store:v2:cursor");
1527-
assert_eq!(token.index, 7);
1528-
}
1529-
15301501
#[test]
15311502
fn parse_custom_tlv_accepts_valid_record() {
15321503
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
@@ -893,14 +893,14 @@ pub struct GetPaymentDetailsResponse {
893893
#[allow(clippy::derive_partial_eq_without_eq)]
894894
#[derive(Clone, PartialEq, ::prost::Message)]
895895
pub struct ListPaymentsRequest {
896-
/// `page_token` is a pagination token.
896+
/// `page_token` is an opaque pagination token string.
897897
///
898898
/// To query for the first page, `page_token` must not be specified.
899899
///
900900
/// For subsequent pages, use the value that was returned as `next_page_token` in the previous
901901
/// page's response.
902-
#[prost(message, optional, tag = "1")]
903-
pub page_token: ::core::option::Option<super::types::PageToken>,
902+
#[prost(string, optional, tag = "1")]
903+
pub page_token: ::core::option::Option<::prost::alloc::string::String>,
904904
}
905905
/// The response for the `ListPayments` RPC. On failure, a gRPC error status is returned.
906906
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -912,7 +912,8 @@ pub struct ListPaymentsResponse {
912912
/// List of payments.
913913
#[prost(message, repeated, tag = "1")]
914914
pub payments: ::prost::alloc::vec::Vec<super::types::Payment>,
915-
/// `next_page_token` is a pagination token, used to retrieve the next page of results.
915+
/// `next_page_token` is an opaque pagination token string used to retrieve the next page of
916+
/// results.
916917
/// Use this value to query for next-page of paginated operation, by specifying
917918
/// this value as the `page_token` in the next request.
918919
///
@@ -925,8 +926,8 @@ pub struct ListPaymentsResponse {
925926
///
926927
/// **Caution**: Clients must not assume a specific number of records to be present in a page for
927928
/// paginated response.
928-
#[prost(message, optional, tag = "2")]
929-
pub next_page_token: ::core::option::Option<super::types::PageToken>,
929+
#[prost(string, optional, tag = "2")]
930+
pub next_page_token: ::core::option::Option<::prost::alloc::string::String>,
930931
}
931932
/// Retrieves list of all forwarded payments.
932933
/// See more: <https://docs.rs/ldk-node/latest/ldk_node/enum.Event.html#variant.PaymentForwarded>
@@ -936,14 +937,14 @@ pub struct ListPaymentsResponse {
936937
#[allow(clippy::derive_partial_eq_without_eq)]
937938
#[derive(Clone, PartialEq, ::prost::Message)]
938939
pub struct ListForwardedPaymentsRequest {
939-
/// `page_token` is a pagination token.
940+
/// `page_token` is an opaque pagination token string.
940941
///
941942
/// To query for the first page, `page_token` must not be specified.
942943
///
943944
/// For subsequent pages, use the value that was returned as `next_page_token` in the previous
944945
/// page's response.
945-
#[prost(message, optional, tag = "1")]
946-
pub page_token: ::core::option::Option<super::types::PageToken>,
946+
#[prost(string, optional, tag = "1")]
947+
pub page_token: ::core::option::Option<::prost::alloc::string::String>,
947948
}
948949
/// The response for the `ListForwardedPayments` RPC. On failure, a gRPC error status is returned.
949950
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@@ -955,7 +956,8 @@ pub struct ListForwardedPaymentsResponse {
955956
/// List of forwarded payments.
956957
#[prost(message, repeated, tag = "1")]
957958
pub forwarded_payments: ::prost::alloc::vec::Vec<super::types::ForwardedPayment>,
958-
/// `next_page_token` is a pagination token, used to retrieve the next page of results.
959+
/// `next_page_token` is an opaque pagination token string used to retrieve the next page of
960+
/// results.
959961
/// Use this value to query for next-page of paginated operation, by specifying
960962
/// this value as the `page_token` in the next request.
961963
///
@@ -968,8 +970,8 @@ pub struct ListForwardedPaymentsResponse {
968970
///
969971
/// **Caution**: Clients must not assume a specific number of records to be present in a page for
970972
/// paginated response.
971-
#[prost(message, optional, tag = "2")]
972-
pub next_page_token: ::core::option::Option<super::types::PageToken>,
973+
#[prost(string, optional, tag = "2")]
974+
pub next_page_token: ::core::option::Option<::prost::alloc::string::String>,
973975
}
974976
/// Sign a message with the node's secret key.
975977
/// 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
@@ -647,21 +647,22 @@ message GetPaymentDetailsResponse {
647647
// Retrieves list of all payments.
648648
// See more: https://docs.rs/ldk-node/latest/ldk_node/struct.Node.html#method.list_payments
649649
message ListPaymentsRequest {
650-
// `page_token` is a pagination token.
650+
// `page_token` is an opaque pagination token string.
651651
//
652652
// To query for the first page, `page_token` must not be specified.
653653
//
654654
// For subsequent pages, use the value that was returned as `next_page_token` in the previous
655655
// page's response.
656-
optional types.PageToken page_token = 1;
656+
optional string page_token = 1;
657657
}
658658

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

664-
// `next_page_token` is a pagination token, used to retrieve the next page of results.
664+
// `next_page_token` is an opaque pagination token string used to retrieve the next page of
665+
// results.
665666
// Use this value to query for next-page of paginated operation, by specifying
666667
// this value as the `page_token` in the next request.
667668
//
@@ -674,27 +675,28 @@ message ListPaymentsResponse {
674675
//
675676
// **Caution**: Clients must not assume a specific number of records to be present in a page for
676677
// paginated response.
677-
optional types.PageToken next_page_token = 2;
678+
optional string next_page_token = 2;
678679
}
679680

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

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

697-
// `next_page_token` is a pagination token, used to retrieve the next page of results.
698+
// `next_page_token` is an opaque pagination token string used to retrieve the next page of
699+
// results.
698700
// Use this value to query for next-page of paginated operation, by specifying
699701
// this value as the `page_token` in the next request.
700702
//
@@ -707,7 +709,7 @@ message ListForwardedPaymentsResponse {
707709
//
708710
// **Caution**: Clients must not assume a specific number of records to be present in a page for
709711
// paginated response.
710-
optional types.PageToken next_page_token = 2;
712+
optional string next_page_token = 2;
711713
}
712714

713715
// 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
@@ -791,12 +791,6 @@ message AwaitingThresholdConfirmations {
791791
uint64 amount_satoshis = 5;
792792
}
793793

794-
// Token used to determine start of next page in paginated APIs.
795-
message PageToken {
796-
string token = 1;
797-
int64 index = 2;
798-
}
799-
800794
message Bolt11InvoiceDescription {
801795
oneof kind {
802796
string direct = 1;

ldk-server-grpc/src/types.rs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -938,18 +938,6 @@ pub struct AwaitingThresholdConfirmations {
938938
#[prost(uint64, tag = "5")]
939939
pub amount_satoshis: u64,
940940
}
941-
/// Token used to determine start of next page in paginated APIs.
942-
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
943-
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
944-
#[cfg_attr(feature = "serde", serde(default))]
945-
#[allow(clippy::derive_partial_eq_without_eq)]
946-
#[derive(Clone, PartialEq, ::prost::Message)]
947-
pub struct PageToken {
948-
#[prost(string, tag = "1")]
949-
pub token: ::prost::alloc::string::String,
950-
#[prost(int64, tag = "2")]
951-
pub index: i64,
952-
}
953941
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
954942
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
955943
#[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)