From 609547b53f3d2a0bbec61043a81516250e2f08b2 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sat, 7 Mar 2026 16:19:10 +0100 Subject: [PATCH 1/3] Add LSPS2 client config to `ldk-server` Expose LSPS2 client settings in `ldk-server-config.toml` so nodes can be configured to source just-in-time liquidity from an LSP. This also keeps the workspace green by fixing the existing `clippy` warning in `ldk-server-cli`. Generated with the assistance of AI. Co-Authored-By: HAL 9000 --- ldk-server/ldk-server-config.toml | 9 ++++ ldk-server/src/main.rs | 8 +++ ldk-server/src/util/config.rs | 86 +++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/ldk-server/ldk-server-config.toml b/ldk-server/ldk-server-config.toml index 5fc8a925..d4345bc3 100644 --- a/ldk-server/ldk-server-config.toml +++ b/ldk-server/ldk-server-config.toml @@ -43,6 +43,15 @@ server_url = "https://mempool.space/api" # Esplora endpoint connection_string = "" # RabbitMQ connection string exchange_name = "" +# LSPS2 Client Support +[liquidity.lsps2_client] +# The public key of the LSPS2 LSP we source just-in-time liquidity from. +node_pubkey = "" +# Address to connect to the LSPS2 LSP (IPv4:port, IPv6:port, OnionV3:port, or hostname:port). +address = ":9735" +# Optional token for authenticating to the LSP. +# token = "" + # Experimental LSPS2 Service Support # CAUTION: LSPS2 support is highly experimental and for testing purposes only. [liquidity.lsps2_service] diff --git a/ldk-server/src/main.rs b/ldk-server/src/main.rs index 3a65c977..3de2a408 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -172,6 +172,14 @@ fn main() { builder.set_gossip_source_rgs(rgs_server_url); } + if let Some(lsps2_client_config) = config_file.lsps2_client_config { + builder.set_liquidity_source_lsps2( + lsps2_client_config.node_id, + lsps2_client_config.address, + lsps2_client_config.token, + ); + } + // LSPS2 support is highly experimental and for testing purposes only. #[cfg(feature = "experimental-lsps2-support")] builder.set_liquidity_provider_lsps2( diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index dbc452a4..c950987c 100644 --- a/ldk-server/src/util/config.rs +++ b/ldk-server/src/util/config.rs @@ -13,6 +13,7 @@ use std::str::FromStr; use std::{fs, io}; use clap::Parser; +use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Network; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::routing::gossip::NodeAlias; @@ -47,14 +48,25 @@ pub struct Config { pub storage_dir_path: Option, pub chain_source: ChainSource, pub rgs_server_url: Option, + #[cfg_attr(not(feature = "events-rabbitmq"), allow(dead_code))] pub rabbitmq_connection_string: String, + #[cfg_attr(not(feature = "events-rabbitmq"), allow(dead_code))] pub rabbitmq_exchange_name: String, + pub lsps2_client_config: Option, + #[cfg_attr(not(feature = "experimental-lsps2-support"), allow(dead_code))] pub lsps2_service_config: Option, pub log_level: LevelFilter, pub log_file_path: Option, pub pathfinding_scores_source_url: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LSPSClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TlsConfig { pub cert_path: Option, @@ -325,6 +337,13 @@ impl ConfigBuilder { #[cfg(not(feature = "events-rabbitmq"))] let (rabbitmq_connection_string, rabbitmq_exchange_name) = (String::new(), String::new()); + let lsps2_client_config = self + .lsps2 + .as_ref() + .and_then(|liquidity| liquidity.lsps2_client.as_ref()) + .map(LSPSClientConfig::try_from) + .transpose()?; + #[cfg(feature = "experimental-lsps2-support")] let lsps2_service_config = { let liquidity = self.lsps2.ok_or_else(|| io::Error::new( @@ -355,6 +374,7 @@ impl ConfigBuilder { rgs_server_url: self.rgs_server_url, rabbitmq_connection_string, rabbitmq_exchange_name, + lsps2_client_config, lsps2_service_config, log_level, log_file_path: self.log_file_path, @@ -436,9 +456,17 @@ struct TomlTlsConfig { #[derive(Deserialize, Serialize)] struct LiquidityConfig { + lsps2_client: Option, lsps2_service: Option, } +#[derive(Deserialize, Serialize, Debug)] +struct LSPSClientTomlConfig { + node_pubkey: String, + address: String, + token: Option, +} + #[derive(Deserialize, Serialize, Debug)] struct LSPS2ServiceTomlConfig { advertise_service: bool, @@ -483,6 +511,27 @@ impl From for LSPS2ServiceConfig { } } +impl TryFrom<&LSPSClientTomlConfig> for LSPSClientConfig { + type Error = io::Error; + + fn try_from(value: &LSPSClientTomlConfig) -> Result { + let node_id = PublicKey::from_str(&value.node_pubkey).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid liquidity client node pubkey configured: {e}"), + ) + })?; + let address = SocketAddress::from_str(&value.address).map_err(|e| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid liquidity client address configured: {e}"), + ) + })?; + + Ok(Self { node_id, address, token: value.token.clone() }) + } +} + #[derive(Parser, Debug)] #[command( version, @@ -630,6 +679,7 @@ fn parse_host_port(addr: &str) -> io::Result<(String, u16)> { mod tests { use std::str::FromStr; + use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Network; use ldk_node::lightning::ln::msgs::SocketAddress; @@ -665,6 +715,11 @@ mod tests { connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" + [liquidity.lsps2_client] + node_pubkey = "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266" + address = "127.0.0.1:39735" + token = "lsps2-token" + [liquidity.lsps2_service] advertise_service = false channel_opening_fee_ppm = 1000 # 0.1% fee @@ -759,6 +814,14 @@ mod tests { rgs_server_url: Some("https://rapidsync.lightningdevkit.org/snapshot/v2/".to_string()), rabbitmq_connection_string: expected_rabbit_conn, rabbitmq_exchange_name: expected_rabbit_exchange, + lsps2_client_config: Some(LSPSClientConfig { + node_id: PublicKey::from_str( + "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266", + ) + .unwrap(), + address: SocketAddress::from_str("127.0.0.1:39735").unwrap(), + token: Some("lsps2-token".to_string()), + }), lsps2_service_config: Some(LSPS2ServiceConfig { require_token: None, advertise_service: false, @@ -786,6 +849,7 @@ mod tests { assert_eq!(config.rgs_server_url, expected.rgs_server_url); assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); assert_eq!(config.rabbitmq_exchange_name, expected.rabbitmq_exchange_name); + assert_eq!(config.lsps2_client_config, expected.lsps2_client_config); #[cfg(feature = "experimental-lsps2-support")] assert_eq!(config.lsps2_service_config.is_some(), expected.lsps2_service_config.is_some()); assert_eq!(config.log_level, expected.log_level); @@ -822,6 +886,10 @@ mod tests { connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" + [liquidity.lsps2_client] + node_pubkey = "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266" + address = "127.0.0.1:39735" + [liquidity.lsps2_service] advertise_service = false channel_opening_fee_ppm = 1000 # 0.1% fee @@ -875,6 +943,10 @@ mod tests { connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" + [liquidity.lsps2_client] + node_pubkey = "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266" + address = "127.0.0.1:39735" + [liquidity.lsps2_service] advertise_service = false channel_opening_fee_ppm = 1000 # 0.1% fee @@ -935,6 +1007,10 @@ mod tests { connection_string = "rabbitmq_connection_string" exchange_name = "rabbitmq_exchange_name" + [liquidity.lsps2_client] + node_pubkey = "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266" + address = "127.0.0.1:39735" + [liquidity.lsps2_service] advertise_service = false channel_opening_fee_ppm = 1000 # 0.1% fee @@ -1080,6 +1156,7 @@ mod tests { rgs_server_url: None, rabbitmq_connection_string: String::new(), rabbitmq_exchange_name: String::new(), + lsps2_client_config: None, lsps2_service_config: None, log_level: LevelFilter::Trace, log_file_path: Some("/var/log/ldk-server.log".to_string()), @@ -1173,6 +1250,14 @@ mod tests { rgs_server_url: Some("https://rapidsync.lightningdevkit.org/snapshot/v2/".to_string()), rabbitmq_connection_string: expected_rabbit_conn, rabbitmq_exchange_name: expected_rabbit_exchange, + lsps2_client_config: Some(LSPSClientConfig { + node_id: PublicKey::from_str( + "0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266", + ) + .unwrap(), + address: SocketAddress::from_str("127.0.0.1:39735").unwrap(), + token: Some("lsps2-token".to_string()), + }), lsps2_service_config: Some(LSPS2ServiceConfig { require_token: None, advertise_service: false, @@ -1199,6 +1284,7 @@ mod tests { assert_eq!(config.rgs_server_url, expected.rgs_server_url); assert_eq!(config.rabbitmq_connection_string, expected.rabbitmq_connection_string); assert_eq!(config.rabbitmq_exchange_name, expected.rabbitmq_exchange_name); + assert_eq!(config.lsps2_client_config, expected.lsps2_client_config); #[cfg(feature = "experimental-lsps2-support")] assert_eq!(config.lsps2_service_config.is_some(), expected.lsps2_service_config.is_some()); assert_eq!(config.pathfinding_scores_source_url, expected.pathfinding_scores_source_url); From 9b48868a3cd32bc3a51f1fe072ba17957cdea7e2 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sat, 7 Mar 2026 16:23:25 +0100 Subject: [PATCH 2/3] Add LSPS2 RPCs to `ldk-server-protos` and `ldk-server` Expose dedicated LSPS2 invoice endpoints so callers can request fixed and variable-amount JIT invoices without overloading the existing `Bolt11Receive` API surface. Generated with the assistance of AI. Co-Authored-By: HAL 9000 --- ldk-server-protos/src/api.rs | 64 +++++++++++++++++++ ldk-server-protos/src/endpoints.rs | 3 + ldk-server-protos/src/proto/api.proto | 53 +++++++++++++++ .../src/api/bolt11_receive_via_jit_channel.rs | 45 +++++++++++++ ldk-server/src/api/mod.rs | 1 + ldk-server/src/service.rs | 21 +++++- 6 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 ldk-server/src/api/bolt11_receive_via_jit_channel.rs diff --git a/ldk-server-protos/src/api.rs b/ldk-server-protos/src/api.rs index 08de4704..ab74b70e 100644 --- a/ldk-server-protos/src/api.rs +++ b/ldk-server-protos/src/api.rs @@ -259,6 +259,70 @@ pub struct Bolt11FailForHashRequest { #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct Bolt11FailForHashResponse {} +/// Return a BOLT11 payable invoice that can be used to request and receive a payment via an +/// LSPS2 just-in-time channel. +/// See more: +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bolt11ReceiveViaJitChannelRequest { + /// The amount in millisatoshi to request. + #[prost(uint64, tag = "1")] + pub amount_msat: u64, + /// An optional description to attach along with the invoice. + /// Will be set in the description field of the encoded payment request. + #[prost(message, optional, tag = "2")] + pub description: ::core::option::Option, + /// Invoice expiry time in seconds. + #[prost(uint32, tag = "3")] + pub expiry_secs: u32, + /// Optional upper bound for the total fee an LSP may deduct when opening the JIT channel. + #[prost(uint64, optional, tag = "4")] + pub max_total_lsp_fee_limit_msat: ::core::option::Option, +} +/// The response `content` for the `Bolt11ReceiveViaJitChannel` API, when HttpStatusCode is OK (200). +/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bolt11ReceiveViaJitChannelResponse { + /// An invoice for a payment within the Lightning Network. + #[prost(string, tag = "1")] + pub invoice: ::prost::alloc::string::String, +} +/// Return a variable-amount BOLT11 invoice that can be used to receive a payment via an LSPS2 +/// just-in-time channel. +/// See more: +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bolt11ReceiveVariableAmountViaJitChannelRequest { + /// An optional description to attach along with the invoice. + /// Will be set in the description field of the encoded payment request. + #[prost(message, optional, tag = "1")] + pub description: ::core::option::Option, + /// Invoice expiry time in seconds. + #[prost(uint32, tag = "2")] + pub expiry_secs: u32, + /// Optional upper bound for the proportional fee, in parts-per-million millisatoshis, that an + /// LSP may deduct when opening the JIT channel. + #[prost(uint64, optional, tag = "3")] + pub max_proportional_lsp_fee_limit_ppm_msat: ::core::option::Option, +} +/// The response `content` for the `Bolt11ReceiveVariableAmountViaJitChannel` API, when HttpStatusCode is OK (200). +/// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Bolt11ReceiveVariableAmountViaJitChannelResponse { + /// An invoice for a payment within the Lightning Network. + #[prost(string, tag = "1")] + pub invoice: ::prost::alloc::string::String, +} /// Send a payment for a BOLT11 invoice. /// See more: #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/ldk-server-protos/src/endpoints.rs b/ldk-server-protos/src/endpoints.rs index 249e19d2..73f926b9 100644 --- a/ldk-server-protos/src/endpoints.rs +++ b/ldk-server-protos/src/endpoints.rs @@ -15,6 +15,9 @@ pub const BOLT11_RECEIVE_PATH: &str = "Bolt11Receive"; pub const BOLT11_RECEIVE_FOR_HASH_PATH: &str = "Bolt11ReceiveForHash"; pub const BOLT11_CLAIM_FOR_HASH_PATH: &str = "Bolt11ClaimForHash"; pub const BOLT11_FAIL_FOR_HASH_PATH: &str = "Bolt11FailForHash"; +pub const BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH: &str = "Bolt11ReceiveViaJitChannel"; +pub const BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH: &str = + "Bolt11ReceiveVariableAmountViaJitChannel"; pub const BOLT11_SEND_PATH: &str = "Bolt11Send"; pub const BOLT12_RECEIVE_PATH: &str = "Bolt12Receive"; pub const BOLT12_SEND_PATH: &str = "Bolt12Send"; diff --git a/ldk-server-protos/src/proto/api.proto b/ldk-server-protos/src/proto/api.proto index a69f3a0c..df7061df 100644 --- a/ldk-server-protos/src/proto/api.proto +++ b/ldk-server-protos/src/proto/api.proto @@ -215,6 +215,59 @@ message Bolt11FailForHashRequest { // When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. message Bolt11FailForHashResponse {} +// Return a BOLT11 payable invoice that can be used to request and receive a payment via an +// LSPS2 just-in-time channel. +// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_via_jit_channel +message Bolt11ReceiveViaJitChannelRequest { + + // The amount in millisatoshi to request. + uint64 amount_msat = 1; + + // An optional description to attach along with the invoice. + // Will be set in the description field of the encoded payment request. + types.Bolt11InvoiceDescription description = 2; + + // Invoice expiry time in seconds. + uint32 expiry_secs = 3; + + // Optional upper bound for the total fee an LSP may deduct when opening the JIT channel. + optional uint64 max_total_lsp_fee_limit_msat = 4; +} + +// The response `content` for the `Bolt11ReceiveViaJitChannel` API, when HttpStatusCode is OK (200). +// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. +message Bolt11ReceiveViaJitChannelResponse { + + // An invoice for a payment within the Lightning Network. + string invoice = 1; +} + +// Return a variable-amount BOLT11 invoice that can be used to receive a payment via an LSPS2 +// just-in-time channel. +// See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.receive_variable_amount_via_jit_channel +message Bolt11ReceiveVariableAmountViaJitChannelRequest { + + // An optional description to attach along with the invoice. + // Will be set in the description field of the encoded payment request. + types.Bolt11InvoiceDescription description = 1; + + // Invoice expiry time in seconds. + uint32 expiry_secs = 2; + + // Optional upper bound for the proportional fee, in parts-per-million millisatoshis, that an + // LSP may deduct when opening the JIT channel. + optional uint64 max_proportional_lsp_fee_limit_ppm_msat = 3; +} + +// The response `content` for the `Bolt11ReceiveVariableAmountViaJitChannel` API, when HttpStatusCode is OK (200). +// When HttpStatusCode is not OK (non-200), the response `content` contains a serialized `ErrorResponse`. +message Bolt11ReceiveVariableAmountViaJitChannelResponse { + + // An invoice for a payment within the Lightning Network. + string invoice = 1; +} + + // Send a payment for a BOLT11 invoice. // See more: https://docs.rs/ldk-node/latest/ldk_node/payment/struct.Bolt11Payment.html#method.send message Bolt11SendRequest { diff --git a/ldk-server/src/api/bolt11_receive_via_jit_channel.rs b/ldk-server/src/api/bolt11_receive_via_jit_channel.rs new file mode 100644 index 00000000..552a1823 --- /dev/null +++ b/ldk-server/src/api/bolt11_receive_via_jit_channel.rs @@ -0,0 +1,45 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +use ldk_server_protos::api::{ + Bolt11ReceiveVariableAmountViaJitChannelRequest, + Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, + Bolt11ReceiveViaJitChannelResponse, +}; + +use crate::api::error::LdkServerError; +use crate::service::Context; +use crate::util::proto_adapter::proto_to_bolt11_description; + +pub(crate) fn handle_bolt11_receive_via_jit_channel_request( + context: Context, request: Bolt11ReceiveViaJitChannelRequest, +) -> Result { + let description = proto_to_bolt11_description(request.description)?; + let invoice = context.node.bolt11_payment().receive_via_jit_channel( + request.amount_msat, + &description, + request.expiry_secs, + request.max_total_lsp_fee_limit_msat, + )?; + + Ok(Bolt11ReceiveViaJitChannelResponse { invoice: invoice.to_string() }) +} + +pub(crate) fn handle_bolt11_receive_variable_amount_via_jit_channel_request( + context: Context, request: Bolt11ReceiveVariableAmountViaJitChannelRequest, +) -> Result { + let description = proto_to_bolt11_description(request.description)?; + let invoice = context.node.bolt11_payment().receive_variable_amount_via_jit_channel( + &description, + request.expiry_secs, + request.max_proportional_lsp_fee_limit_ppm_msat, + )?; + + Ok(Bolt11ReceiveVariableAmountViaJitChannelResponse { invoice: invoice.to_string() }) +} diff --git a/ldk-server/src/api/mod.rs b/ldk-server/src/api/mod.rs index 1c4d489a..3d6acf76 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -18,6 +18,7 @@ pub(crate) mod bolt11_claim_for_hash; pub(crate) mod bolt11_fail_for_hash; pub(crate) mod bolt11_receive; pub(crate) mod bolt11_receive_for_hash; +pub(crate) mod bolt11_receive_via_jit_channel; pub(crate) mod bolt11_send; pub(crate) mod bolt12_receive; pub(crate) mod bolt12_send; diff --git a/ldk-server/src/service.rs b/ldk-server/src/service.rs index ebfc6492..ff59c217 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -20,7 +20,8 @@ use ldk_node::bitcoin::hashes::{sha256, Hash, HashEngine}; use ldk_node::Node; use ldk_server_protos::endpoints::{ BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, - BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH, + BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, + BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, @@ -34,6 +35,10 @@ use crate::api::bolt11_claim_for_hash::handle_bolt11_claim_for_hash_request; use crate::api::bolt11_fail_for_hash::handle_bolt11_fail_for_hash_request; use crate::api::bolt11_receive::handle_bolt11_receive_request; use crate::api::bolt11_receive_for_hash::handle_bolt11_receive_for_hash_request; +use crate::api::bolt11_receive_via_jit_channel::{ + handle_bolt11_receive_variable_amount_via_jit_channel_request, + handle_bolt11_receive_via_jit_channel_request, +}; use crate::api::bolt11_send::handle_bolt11_send_request; use crate::api::bolt12_receive::handle_bolt12_receive_request; use crate::api::bolt12_send::handle_bolt12_send_request; @@ -243,6 +248,20 @@ impl Service> for NodeService { api_key, handle_bolt11_fail_for_hash_request, )), + BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH => Box::pin(handle_request( + context, + req, + auth_params, + api_key, + handle_bolt11_receive_via_jit_channel_request, + )), + BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH => Box::pin(handle_request( + context, + req, + auth_params, + api_key, + handle_bolt11_receive_variable_amount_via_jit_channel_request, + )), BOLT11_SEND_PATH => Box::pin(handle_request( context, req, From 47c2c95921111a9d4ec1d2edcb849b7a0258d9b0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sat, 7 Mar 2026 16:26:56 +0100 Subject: [PATCH 3/3] Add LSPS2 commands to `ldk-server-client` and `ldk-server-cli` Expose the new LSPS2 JIT invoice endpoints through the Rust client and CLI so callers can request either fixed or variable-amount invoices without constructing protobuf messages manually. Generated with the assistance of AI. Co-Authored-By: HAL 9000 --- ldk-server-cli/src/main.rs | 110 +++++++++++++++++++++++++++----- ldk-server-client/src/client.rs | 31 ++++++++- 2 files changed, 123 insertions(+), 18 deletions(-) diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 66332cff..4cdf30e4 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -24,7 +24,9 @@ use ldk_server_client::error::LdkServerErrorCode::{ use ldk_server_client::ldk_server_protos::api::{ Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse, Bolt11FailForHashRequest, Bolt11FailForHashResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, - Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, + Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, + Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, + Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest, @@ -178,6 +180,41 @@ enum Commands { #[arg(help = "The hex-encoded 32-byte payment hash")] payment_hash: String, }, + #[command(about = "Create a fixed-amount BOLT11 invoice to receive via an LSPS2 JIT channel")] + Bolt11ReceiveViaJitChannel { + #[arg(help = "Amount to request, e.g. 50sat or 50000msat")] + amount: Amount, + #[arg(short, long, help = "Description to attach along with the invoice")] + description: Option, + #[arg( + long, + help = "SHA-256 hash of the description (hex). Use instead of description for longer text" + )] + description_hash: Option, + #[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")] + expiry_secs: Option, + #[arg( + long, + help = "Maximum total fee an LSP may deduct for opening the JIT channel, e.g. 50sat or 50000msat" + )] + max_total_lsp_fee_limit: Option, + }, + #[command( + about = "Create a variable-amount BOLT11 invoice to receive via an LSPS2 JIT channel" + )] + Bolt11ReceiveVariableAmountViaJitChannel { + #[arg(short, long, help = "Description to attach along with the invoice")] + description: Option, + #[arg( + long, + help = "SHA-256 hash of the description (hex). Use instead of description for longer text" + )] + description_hash: Option, + #[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")] + expiry_secs: Option, + #[arg(long, help = "Maximum proportional fee the LSP may deduct in ppm-msat")] + max_proportional_lsp_fee_limit_ppm_msat: Option, + }, #[command(about = "Pay a BOLT11 invoice")] Bolt11Send { #[arg(help = "A BOLT11 invoice for a payment within the Lightning Network")] @@ -571,21 +608,8 @@ async fn main() { }, Commands::Bolt11Receive { description, description_hash, expiry_secs, amount } => { let amount_msat = amount.map(|a| a.to_msat()); - let invoice_description = match (description, description_hash) { - (Some(desc), None) => Some(Bolt11InvoiceDescription { - kind: Some(bolt11_invoice_description::Kind::Direct(desc)), - }), - (None, Some(hash)) => Some(Bolt11InvoiceDescription { - kind: Some(bolt11_invoice_description::Kind::Hash(hash)), - }), - (Some(_), Some(_)) => { - handle_error(LdkServerError::new( - InternalError, - "Only one of description or description_hash can be set.".to_string(), - )); - }, - (None, None) => None, - }; + let invoice_description = + parse_bolt11_invoice_description(description, description_hash); let expiry_secs = expiry_secs.unwrap_or(DEFAULT_EXPIRY_SECS); let request = @@ -647,6 +671,40 @@ async fn main() { client.bolt11_fail_for_hash(Bolt11FailForHashRequest { payment_hash }).await, ); }, + Commands::Bolt11ReceiveViaJitChannel { + amount, + description, + description_hash, + expiry_secs, + max_total_lsp_fee_limit, + } => { + let request = Bolt11ReceiveViaJitChannelRequest { + amount_msat: amount.to_msat(), + description: parse_bolt11_invoice_description(description, description_hash), + expiry_secs: expiry_secs.unwrap_or(DEFAULT_EXPIRY_SECS), + max_total_lsp_fee_limit_msat: max_total_lsp_fee_limit.map(|a| a.to_msat()), + }; + + handle_response_result::<_, Bolt11ReceiveViaJitChannelResponse>( + client.bolt11_receive_via_jit_channel(request).await, + ); + }, + Commands::Bolt11ReceiveVariableAmountViaJitChannel { + description, + description_hash, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + } => { + let request = Bolt11ReceiveVariableAmountViaJitChannelRequest { + description: parse_bolt11_invoice_description(description, description_hash), + expiry_secs: expiry_secs.unwrap_or(DEFAULT_EXPIRY_SECS), + max_proportional_lsp_fee_limit_ppm_msat, + }; + + handle_response_result::<_, Bolt11ReceiveVariableAmountViaJitChannelResponse>( + client.bolt11_receive_variable_amount_via_jit_channel(request).await, + ); + }, Commands::Bolt11Send { invoice, amount, @@ -1054,6 +1112,26 @@ where } } +fn parse_bolt11_invoice_description( + description: Option, description_hash: Option, +) -> Option { + match (description, description_hash) { + (Some(desc), None) => Some(Bolt11InvoiceDescription { + kind: Some(bolt11_invoice_description::Kind::Direct(desc)), + }), + (None, Some(hash)) => Some(Bolt11InvoiceDescription { + kind: Some(bolt11_invoice_description::Kind::Hash(hash)), + }), + (Some(_), Some(_)) => { + handle_error(LdkServerError::new( + InternalError, + "Only one of description or description_hash can be set.".to_string(), + )); + }, + (None, None) => None, + } +} + fn parse_page_token(token_str: &str) -> Result { let parts: Vec<&str> = token_str.split(':').collect(); if parts.len() != 2 { diff --git a/ldk-server-client/src/client.rs b/ldk-server-client/src/client.rs index 18042b31..c970b9c9 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -14,7 +14,9 @@ use bitcoin_hashes::{sha256, Hash, HashEngine}; use ldk_server_protos::api::{ Bolt11ClaimForHashRequest, Bolt11ClaimForHashResponse, Bolt11FailForHashRequest, Bolt11FailForHashResponse, Bolt11ReceiveForHashRequest, Bolt11ReceiveForHashResponse, - Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, + Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, + Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, + Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest, @@ -33,7 +35,8 @@ use ldk_server_protos::api::{ }; use ldk_server_protos::endpoints::{ BOLT11_CLAIM_FOR_HASH_PATH, BOLT11_FAIL_FOR_HASH_PATH, BOLT11_RECEIVE_FOR_HASH_PATH, - BOLT11_RECEIVE_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH, + BOLT11_RECEIVE_PATH, BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH, + BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH, BOLT11_SEND_PATH, BOLT12_RECEIVE_PATH, BOLT12_SEND_PATH, CLOSE_CHANNEL_PATH, CONNECT_PEER_PATH, DISCONNECT_PEER_PATH, EXPORT_PATHFINDING_SCORES_PATH, FORCE_CLOSE_CHANNEL_PATH, GET_BALANCES_PATH, GET_NODE_INFO_PATH, GET_PAYMENT_DETAILS_PATH, GRAPH_GET_CHANNEL_PATH, GRAPH_GET_NODE_PATH, GRAPH_LIST_CHANNELS_PATH, GRAPH_LIST_NODES_PATH, @@ -174,6 +177,30 @@ impl LdkServerClient { self.post_request(&request, &url).await } + /// Retrieve a new fixed-amount BOLT11 invoice for receiving via an LSPS2 JIT channel. + /// For API contract/usage, refer to docs for [`Bolt11ReceiveViaJitChannelRequest`] and + /// [`Bolt11ReceiveViaJitChannelResponse`]. + pub async fn bolt11_receive_via_jit_channel( + &self, request: Bolt11ReceiveViaJitChannelRequest, + ) -> Result { + let url = format!("https://{}/{BOLT11_RECEIVE_VIA_JIT_CHANNEL_PATH}", self.base_url); + self.post_request(&request, &url).await + } + + /// Retrieve a new variable-amount BOLT11 invoice for receiving via an LSPS2 JIT channel. + /// For API contract/usage, refer to docs for + /// [`Bolt11ReceiveVariableAmountViaJitChannelRequest`] and + /// [`Bolt11ReceiveVariableAmountViaJitChannelResponse`]. + pub async fn bolt11_receive_variable_amount_via_jit_channel( + &self, request: Bolt11ReceiveVariableAmountViaJitChannelRequest, + ) -> Result { + let url = format!( + "https://{}/{BOLT11_RECEIVE_VARIABLE_AMOUNT_VIA_JIT_CHANNEL_PATH}", + self.base_url, + ); + self.post_request(&request, &url).await + } + /// Send a payment for a BOLT11 invoice. /// For API contract/usage, refer to docs for [`Bolt11SendRequest`] and [`Bolt11SendResponse`]. pub async fn bolt11_send(