Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 53 additions & 17 deletions ldk-server-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ use ldk_server_client::ldk_server_protos::api::{
SpliceOutResponse, UpdateChannelConfigRequest, UpdateChannelConfigResponse,
};
use ldk_server_client::ldk_server_protos::types::{
bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, PageToken, Payment,
bolt11_invoice_description, Bolt11InvoiceDescription, ChannelConfig, PageToken,
RouteParametersConfig,
};
use serde::Serialize;
use types::CliListPaymentsResponse;

mod types;

// Having these default values as constants in the Proto file and
// importing/reusing them here might be better, but Proto3 removed
Expand Down Expand Up @@ -178,9 +181,12 @@ enum Commands {
ListPayments {
#[arg(short, long)]
#[arg(
help = "Minimum number of payments to return. If not provided, only the first page of the paginated list is returned."
help = "Fetch at least this many payments by iterating through multiple pages. Returns combined results with the last page token. If not provided, returns only a single page."
)]
number_of_payments: Option<u64>,
#[arg(long)]
#[arg(help = "Page token to continue from a previous page (format: token:index)")]
page_token: Option<String>,
},
UpdateChannelConfig {
#[arg(short, long)]
Expand Down Expand Up @@ -416,12 +422,15 @@ async fn main() {
client.list_channels(ListChannelsRequest {}).await,
);
},
Commands::ListPayments { number_of_payments } => {
handle_response_result::<_, ListPaymentsResponse>(
list_n_payments(client, number_of_payments)
.await
// todo: handle pagination properly
.map(|payments| ListPaymentsResponse { payments, next_page_token: None }),
Commands::ListPayments { number_of_payments, page_token } => {
let page_token = if let Some(token_str) = page_token {
Some(parse_page_token(&token_str).unwrap_or_else(|e| handle_error(e)))
} else {
None
};

handle_response_result::<_, CliListPaymentsResponse>(
handle_list_payments(client, number_of_payments, page_token).await,
);
},
Commands::UpdateChannelConfig {
Expand Down Expand Up @@ -475,24 +484,37 @@ fn build_open_channel_config(
})
}

async fn handle_list_payments(
client: LdkServerClient, number_of_payments: Option<u64>, initial_page_token: Option<PageToken>,
) -> Result<ListPaymentsResponse, LdkServerError> {
if let Some(count) = number_of_payments {
list_n_payments(client, count, initial_page_token).await
} else {
// Fetch single page
client.list_payments(ListPaymentsRequest { page_token: initial_page_token }).await
}
}

async fn list_n_payments(
client: LdkServerClient, number_of_payments: Option<u64>,
) -> Result<Vec<Payment>, LdkServerError> {
let mut payments = Vec::new();
let mut page_token: Option<PageToken> = None;
// If no count is specified, just list the first page.
let target_count = number_of_payments.unwrap_or(0);
client: LdkServerClient, target_count: u64, initial_page_token: Option<PageToken>,
) -> Result<ListPaymentsResponse, LdkServerError> {
let mut payments = Vec::with_capacity(target_count as usize);
let mut page_token = initial_page_token;
let mut next_page_token;

loop {
let response = client.list_payments(ListPaymentsRequest { page_token }).await?;

payments.extend(response.payments);
if payments.len() >= target_count as usize || response.next_page_token.is_none() {
next_page_token = response.next_page_token;

if payments.len() >= target_count as usize || next_page_token.is_none() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of surfacing the next_page_token back to the user is so that they can continue iterating after we've already satisfied their request to provide at least n payments ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

break;
}
page_token = response.next_page_token;
page_token = next_page_token;
}
Ok(payments)

Ok(ListPaymentsResponse { payments, next_page_token })
}

fn handle_response_result<Rs, Js>(response: Result<Rs, LdkServerError>)
Expand All @@ -517,6 +539,20 @@ where
}
}

fn parse_page_token(token_str: &str) -> Result<PageToken, LdkServerError> {
let parts: Vec<&str> = token_str.split(':').collect();
if parts.len() != 2 {
return Err(LdkServerError::new(
InternalError,
"Page token must be in format 'token:index'".to_string(),
));
}
let index = parts[1]
.parse::<i64>()
.map_err(|_| LdkServerError::new(InternalError, "Invalid page token index".to_string()))?;
Ok(PageToken { token: parts[0].to_string(), index })
}

fn handle_error(e: LdkServerError) -> ! {
let error_type = match e.error_code {
InvalidRequestError => "Invalid Request",
Expand Down
41 changes: 41 additions & 0 deletions ldk-server-cli/src/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

//! CLI-specific type wrappers for API responses.
//!
//! This file contains wrapper types that customize the serialization format
//! of API responses for CLI output. These wrappers ensure that the CLI's output
//! format matches what users expect and what the CLI can parse back as input.
use ldk_server_client::ldk_server_protos::api::ListPaymentsResponse;
use ldk_server_client::ldk_server_protos::types::{PageToken, Payment};
use serde::Serialize;

/// CLI-specific wrapper for ListPaymentsResponse that formats the page token
/// as "token:idx" instead of a JSON object.
#[derive(Debug, Clone, Serialize)]
pub struct CliListPaymentsResponse {
/// List of payments.
pub payments: Vec<Payment>,
/// Next page token formatted as "token:idx", or None if no more pages.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_page_token: Option<String>,
}

impl From<ListPaymentsResponse> for CliListPaymentsResponse {
fn from(response: ListPaymentsResponse) -> Self {
let next_page_token = response.next_page_token.map(format_page_token);

CliListPaymentsResponse { payments: response.payments, next_page_token }
}
}

fn format_page_token(token: PageToken) -> String {
format!("{}:{}", token.token, token.index)
}
Comment on lines +31 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a better fit in the fn handle_list_payments_request in ldk-server ? Pretty much delete the PageToken type in the proto definition, and switch it to optional string next_page_token = 2.

Like this people who interact with the server programmatically also no longer can mishandle these two pieces ?

Although I agree they get a single object of type PageToken, so may be good enough there.