diff --git a/ldk-server-cli/src/main.rs b/ldk-server-cli/src/main.rs index 139380a..98f5188 100644 --- a/ldk-server-cli/src/main.rs +++ b/ldk-server-cli/src/main.rs @@ -21,7 +21,9 @@ use ldk_server_client::error::LdkServerErrorCode::{ AuthError, InternalError, InternalServerError, InvalidRequestError, LightningError, }; use ldk_server_client::ldk_server_protos::api::{ - Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, + Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, + Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, + Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest, @@ -133,6 +135,41 @@ enum Commands { #[arg(short, long, help = "Invoice expiry time in seconds (default: 86400)")] expiry_secs: Option, }, + #[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")] @@ -510,21 +547,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 = @@ -534,6 +558,40 @@ async fn main() { client.bolt11_receive(request).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, @@ -928,6 +986,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 04b1dc5..3ee2d42 100644 --- a/ldk-server-client/src/client.rs +++ b/ldk-server-client/src/client.rs @@ -12,7 +12,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; use bitcoin_hashes::hmac::{Hmac, HmacEngine}; use bitcoin_hashes::{sha256, Hash, HashEngine}; use ldk_server_protos::api::{ - Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11SendRequest, Bolt11SendResponse, + Bolt11ReceiveRequest, Bolt11ReceiveResponse, Bolt11ReceiveVariableAmountViaJitChannelRequest, + Bolt11ReceiveVariableAmountViaJitChannelResponse, Bolt11ReceiveViaJitChannelRequest, + Bolt11ReceiveViaJitChannelResponse, Bolt11SendRequest, Bolt11SendResponse, Bolt12ReceiveRequest, Bolt12ReceiveResponse, Bolt12SendRequest, Bolt12SendResponse, CloseChannelRequest, CloseChannelResponse, ConnectPeerRequest, ConnectPeerResponse, DisconnectPeerRequest, DisconnectPeerResponse, ExportPathfindingScoresRequest, @@ -29,7 +31,8 @@ use ldk_server_protos::api::{ UpdateChannelConfigResponse, VerifySignatureRequest, VerifySignatureResponse, }; use ldk_server_protos::endpoints::{ - 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, @@ -142,6 +145,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( diff --git a/ldk-server-protos/src/api.rs b/ldk-server-protos/src/api.rs index cfe4550..166428a 100644 --- a/ldk-server-protos/src/api.rs +++ b/ldk-server-protos/src/api.rs @@ -174,6 +174,70 @@ pub struct Bolt11ReceiveResponse { #[prost(string, tag = "1")] pub invoice: ::prost::alloc::string::String, } +/// 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 5766d52..c4ad383 100644 --- a/ldk-server-protos/src/endpoints.rs +++ b/ldk-server-protos/src/endpoints.rs @@ -12,6 +12,9 @@ pub const GET_BALANCES_PATH: &str = "GetBalances"; pub const ONCHAIN_RECEIVE_PATH: &str = "OnchainReceive"; pub const ONCHAIN_SEND_PATH: &str = "OnchainSend"; pub const BOLT11_RECEIVE_PATH: &str = "Bolt11Receive"; +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 9e1c9fa..7fb9f51 100644 --- a/ldk-server-protos/src/proto/api.proto +++ b/ldk-server-protos/src/proto/api.proto @@ -148,6 +148,58 @@ message Bolt11ReceiveResponse { string invoice = 1; } +// 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/ldk-server-config.toml b/ldk-server/ldk-server-config.toml index 062ca08..7fed8a6 100644 --- a/ldk-server/ldk-server-config.toml +++ b/ldk-server/ldk-server-config.toml @@ -41,6 +41,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 = "127.0.0.1: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/api/bolt11_receive_via_jit_channel.rs b/ldk-server/src/api/bolt11_receive_via_jit_channel.rs new file mode 100644 index 0000000..552a182 --- /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 b4a6089..2c439b0 100644 --- a/ldk-server/src/api/mod.rs +++ b/ldk-server/src/api/mod.rs @@ -15,6 +15,7 @@ use crate::api::error::LdkServerError; use crate::api::error::LdkServerErrorCode::InvalidRequestError; pub(crate) mod bolt11_receive; +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/main.rs b/ldk-server/src/main.rs index 0b4460c..f8064fa 100644 --- a/ldk-server/src/main.rs +++ b/ldk-server/src/main.rs @@ -163,6 +163,14 @@ fn main() { }, } + 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/service.rs b/ldk-server/src/service.rs index d10d615..3333560 100644 --- a/ldk-server/src/service.rs +++ b/ldk-server/src/service.rs @@ -19,7 +19,8 @@ use ldk_node::bitcoin::hashes::hmac::{Hmac, HmacEngine}; use ldk_node::bitcoin::hashes::{sha256, Hash, HashEngine}; use ldk_node::Node; use ldk_server_protos::endpoints::{ - 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, @@ -30,6 +31,10 @@ use ldk_server_protos::endpoints::{ use prost::Message; use crate::api::bolt11_receive::handle_bolt11_receive_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; @@ -217,6 +222,20 @@ impl Service> for NodeService { api_key, handle_bolt11_receive_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, diff --git a/ldk-server/src/util/config.rs b/ldk-server/src/util/config.rs index 43fa5f7..fe2832a 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; @@ -46,13 +47,24 @@ pub struct Config { pub rest_service_addr: SocketAddr, pub storage_dir_path: Option, pub chain_source: ChainSource, + #[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, } +#[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, @@ -314,6 +326,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( @@ -341,6 +360,7 @@ impl ConfigBuilder { chain_source, rabbitmq_connection_string, rabbitmq_exchange_name, + lsps2_client_config, lsps2_service_config, log_level, log_file_path: self.log_file_path, @@ -419,9 +439,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, @@ -466,6 +494,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, @@ -606,6 +655,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; @@ -640,6 +690,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 @@ -731,6 +786,14 @@ mod tests { }, 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, @@ -756,6 +819,7 @@ mod tests { assert_eq!(config.chain_source, expected.chain_source); 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); @@ -790,6 +854,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 @@ -842,6 +910,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 @@ -901,6 +973,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 @@ -1045,6 +1121,7 @@ mod tests { }, 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()), @@ -1134,6 +1211,14 @@ mod tests { }, 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, @@ -1158,6 +1243,7 @@ mod tests { assert_eq!(config.chain_source, expected.chain_source); 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()); }