From 8a58454ddf4f4911f7584c593c9f867e4aa6e2c1 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 23 Jul 2024 10:50:38 +0200 Subject: [PATCH 1/6] Reorder LSPS2 API params .. to align with the rest of the APIs where we usually go `node_id`, `address`, etc. --- bindings/ldk_node.udl | 2 +- src/builder.rs | 16 ++++++++-------- src/liquidity.rs | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b0ff44b0a..3756b47d3 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -58,7 +58,7 @@ interface Builder { void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); - void set_liquidity_source_lsps2(SocketAddress address, PublicKey node_id, string? token); + void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token); void set_storage_dir_path(string storage_dir_path); void set_filesystem_logger(string? log_file_path, LogLevel? log_level); void set_log_facade_logger(LogLevel log_level); diff --git a/src/builder.rs b/src/builder.rs index bcd91eeb8..5e3b7795d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -99,8 +99,8 @@ enum GossipSourceConfig { #[derive(Debug, Clone)] struct LiquiditySourceConfig { - // LSPS2 service's (address, node_id, token) - lsps2_service: Option<(SocketAddress, PublicKey, Option)>, + // LSPS2 service's (node_id, address, token) + lsps2_service: Option<(PublicKey, SocketAddress, Option)>, } impl Default for LiquiditySourceConfig { @@ -330,14 +330,14 @@ impl NodeBuilder { /// /// The given `token` will be used by the LSP to authenticate the user. pub fn set_liquidity_source_lsps2( - &mut self, address: SocketAddress, node_id: PublicKey, token: Option, + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, ) -> &mut Self { // Mark the LSP as trusted for 0conf self.config.trusted_peers_0conf.push(node_id.clone()); let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some((address, node_id, token)); + liquidity_source_config.lsps2_service = Some((node_id, address, token)); self } @@ -662,9 +662,9 @@ impl ArcedNodeBuilder { /// /// The given `token` will be used by the LSP to authenticate the user. pub fn set_liquidity_source_lsps2( - &self, address: SocketAddress, node_id: PublicKey, token: Option, + &self, node_id: PublicKey, address: SocketAddress, token: Option, ) { - self.inner.write().unwrap().set_liquidity_source_lsps2(address, node_id, token); + self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token); } /// Sets the used storage directory path. @@ -1130,7 +1130,7 @@ fn build_with_store_internal( }; let liquidity_source = liquidity_source_config.as_ref().and_then(|lsc| { - lsc.lsps2_service.as_ref().map(|(address, node_id, token)| { + lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { let lsps2_client_config = Some(LSPS2ClientConfig {}); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config }); @@ -1143,8 +1143,8 @@ fn build_with_store_internal( liquidity_client_config, )); Arc::new(LiquiditySource::new_lsps2( - address.clone(), *node_id, + address.clone(), token.clone(), Arc::clone(&channel_manager), Arc::clone(&keys_manager), diff --git a/src/liquidity.rs b/src/liquidity.rs index 9e9450f8f..8c8ed66c2 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -32,8 +32,8 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; struct LSPS2Service { - address: SocketAddress, node_id: PublicKey, + address: SocketAddress, token: Option, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, @@ -56,15 +56,15 @@ where L::Target: LdkLogger, { pub(crate) fn new_lsps2( - address: SocketAddress, node_id: PublicKey, token: Option, + node_id: PublicKey, address: SocketAddress, token: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, config: Arc, logger: L, ) -> Self { let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); let lsps2_service = Some(LSPS2Service { - address, node_id, + address, token, pending_fee_requests, pending_buy_requests, From ba875c3ebbc900082b765e69924b35bda5521a85 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 23 Jul 2024 10:58:21 +0200 Subject: [PATCH 2/6] Allow to set LSPS1 liquidity source We add support for LSPS1 liquidity sources. To this end we slightly refactor our logic to first create a `LiquiditySourceBuilder` that then can be used to `build()` the `LiquiditySource` with the configured services. --- src/builder.rs | 97 ++++++++++++++++++++++++-------------- src/liquidity.rs | 105 ++++++++++++++++++++++++++++++++++++++---- src/payment/bolt11.rs | 2 +- 3 files changed, 160 insertions(+), 44 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 5e3b7795d..731a55d54 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,7 +18,7 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{read_node_metrics, write_node_metrics}; use crate::io::vss_store::VssStore; -use crate::liquidity::LiquiditySource; +use crate::liquidity::LiquiditySourceBuilder; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -54,9 +54,6 @@ use lightning::util::sweep::OutputSweeper; use lightning_persister::fs_store::FilesystemStore; -use lightning_liquidity::lsps2::client::LSPS2ClientConfig; -use lightning_liquidity::{LiquidityClientConfig, LiquidityManager}; - use bdk_wallet::template::Bip84; use bdk_wallet::KeychainKind; use bdk_wallet::Wallet as BdkWallet; @@ -99,13 +96,15 @@ enum GossipSourceConfig { #[derive(Debug, Clone)] struct LiquiditySourceConfig { + // LSPS1 service's (node_id, address, token) + lsps1_service: Option<(PublicKey, SocketAddress, Option)>, // LSPS2 service's (node_id, address, token) lsps2_service: Option<(PublicKey, SocketAddress, Option)>, } impl Default for LiquiditySourceConfig { fn default() -> Self { - Self { lsps2_service: None } + Self { lsps1_service: None, lsps2_service: None } } } @@ -322,13 +321,34 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to source its inbound liquidity from the given - /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) - /// service. + /// Configures the [`Node`] instance to source inbound liquidity from the given + /// [bLIP-51 / LSPS1] service. /// /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. /// /// The given `token` will be used by the LSP to authenticate the user. + /// + /// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md + pub fn set_liquidity_source_lsps1( + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + ) -> &mut Self { + // Mark the LSP as trusted for 0conf + self.config.trusted_peers_0conf.push(node_id.clone()); + + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + liquidity_source_config.lsps1_service = Some((node_id, address, token)); + self + } + + /// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given + /// [bLIP-52 / LSPS2] service. + /// + /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// + /// The given `token` will be used by the LSP to authenticate the user. + /// + /// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md pub fn set_liquidity_source_lsps2( &mut self, node_id: PublicKey, address: SocketAddress, token: Option, ) -> &mut Self { @@ -654,13 +674,28 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_gossip_source_rgs(rgs_server_url); } - /// Configures the [`Node`] instance to source its inbound liquidity from the given - /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) - /// service. + /// Configures the [`Node`] instance to source inbound liquidity from the given + /// [bLIP-51 / LSPS1] service. /// /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. /// /// The given `token` will be used by the LSP to authenticate the user. + /// + /// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md + pub fn set_liquidity_source_lsps1( + &self, node_id: PublicKey, address: SocketAddress, token: Option, + ) { + self.inner.write().unwrap().set_liquidity_source_lsps1(node_id, address, token); + } + + /// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given + /// [bLIP-52 / LSPS2] service. + /// + /// Will mark the LSP as trusted for 0-confirmation channels, see [`Config::trusted_peers_0conf`]. + /// + /// The given `token` will be used by the LSP to authenticate the user. + /// + /// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md pub fn set_liquidity_source_lsps2( &self, node_id: PublicKey, address: SocketAddress, token: Option, ) { @@ -1129,30 +1164,24 @@ fn build_with_store_internal( }, }; - let liquidity_source = liquidity_source_config.as_ref().and_then(|lsc| { + let liquidity_source = liquidity_source_config.as_ref().map(|lsc| { + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&chain_source), + Arc::clone(&config), + Arc::clone(&logger), + ); + + lsc.lsps1_service.as_ref().map(|(node_id, address, token)| { + liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone()) + }); + lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { - let lsps2_client_config = Some(LSPS2ClientConfig {}); - let liquidity_client_config = - Some(LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config }); - let liquidity_manager = Arc::new(LiquidityManager::new( - Arc::clone(&keys_manager), - Arc::clone(&channel_manager), - Some(Arc::clone(&chain_source)), - None, - None, - liquidity_client_config, - )); - Arc::new(LiquiditySource::new_lsps2( - *node_id, - address.clone(), - token.clone(), - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - liquidity_manager, - Arc::clone(&config), - Arc::clone(&logger), - )) - }) + liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone()) + }); + + Arc::new(liquidity_source_builder.build()) }); let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() { diff --git a/src/liquidity.rs b/src/liquidity.rs index 8c8ed66c2..526accee7 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -5,6 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use crate::chain::ChainSource; use crate::logger::{log_debug, log_error, log_info, LdkLogger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager}; use crate::{Config, Error}; @@ -15,9 +16,12 @@ use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; +use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; use lightning_liquidity::lsps2::utils::compute_opening_fee; +use lightning_liquidity::LiquidityClientConfig; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; @@ -31,47 +35,126 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; +struct LSPS1Service { + node_id: PublicKey, + address: SocketAddress, + token: Option, + client_config: LSPS1ClientConfig, +} + struct LSPS2Service { node_id: PublicKey, address: SocketAddress, token: Option, + client_config: LSPS2ClientConfig, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, } -pub(crate) struct LiquiditySource +pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, { + lsps1_service: Option, lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, - liquidity_manager: Arc, + chain_source: Arc, config: Arc, logger: L, } -impl LiquiditySource +impl LiquiditySourceBuilder where L::Target: LdkLogger, { - pub(crate) fn new_lsps2( - node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn new( channel_manager: Arc, keys_manager: Arc, - liquidity_manager: Arc, config: Arc, logger: L, + chain_source: Arc, config: Arc, logger: L, ) -> Self { + let lsps1_service = None; + let lsps2_service = None; + Self { + lsps1_service, + lsps2_service, + channel_manager, + keys_manager, + chain_source, + config, + logger, + } + } + + pub(crate) fn lsps1_service( + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + ) -> &mut Self { + // TODO: allow to set max_channel_fees_msat + let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + self.lsps1_service = Some(LSPS1Service { node_id, address, token, client_config }); + self + } + + pub(crate) fn lsps2_service( + &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + ) -> &mut Self { + let client_config = LSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); - let lsps2_service = Some(LSPS2Service { + self.lsps2_service = Some(LSPS2Service { node_id, address, token, + client_config, pending_fee_requests, pending_buy_requests, }); - Self { lsps2_service, channel_manager, keys_manager, liquidity_manager, config, logger } + self } + pub(crate) fn build(self) -> LiquiditySource { + let lsps1_client_config = self.lsps1_service.as_ref().map(|s| s.client_config.clone()); + let lsps2_client_config = self.lsps2_service.as_ref().map(|s| s.client_config.clone()); + let liquidity_client_config = + Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); + + let liquidity_manager = Arc::new(LiquidityManager::new( + Arc::clone(&self.keys_manager), + Arc::clone(&self.channel_manager), + Some(Arc::clone(&self.chain_source)), + None, + None, + liquidity_client_config, + )); + + LiquiditySource { + lsps1_service: self.lsps1_service, + lsps2_service: self.lsps2_service, + channel_manager: self.channel_manager, + keys_manager: self.keys_manager, + liquidity_manager, + config: self.config, + logger: self.logger, + } + } +} + +pub(crate) struct LiquiditySource +where + L::Target: LdkLogger, +{ + lsps1_service: Option, + lsps2_service: Option, + channel_manager: Arc, + keys_manager: Arc, + liquidity_manager: Arc, + config: Arc, + logger: L, +} + +impl LiquiditySource +where + L::Target: LdkLogger, +{ pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { let process_msgs_callback = move || peer_manager.process_events(); self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); @@ -81,7 +164,11 @@ where self.liquidity_manager.as_ref() } - pub(crate) fn get_liquidity_source_details(&self) -> Option<(PublicKey, SocketAddress)> { + pub(crate) fn get_lsps1_service_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps1_service.as_ref().map(|s| (s.node_id, s.address.clone())) + } + + pub(crate) fn get_lsps2_service_details(&self) -> Option<(PublicKey, SocketAddress)> { self.lsps2_service.as_ref().map(|s| (s.node_id, s.address.clone())) } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 386da30df..2c1b19143 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -597,7 +597,7 @@ impl Bolt11Payment { self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let (node_id, address) = liquidity_source - .get_liquidity_source_details() + .get_lsps2_service_details() .ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); From e8741a9771d1e94fae336d19ca32eab6a9561461 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Dec 2024 14:00:52 +0100 Subject: [PATCH 3/6] Add `create_order` logic We add the logic required to send `create_order` requests and check on their status. --- src/liquidity.rs | 369 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 368 insertions(+), 1 deletion(-) diff --git a/src/liquidity.rs b/src/liquidity.rs index 526accee7..86e86fc9b 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -17,6 +17,10 @@ use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps1::event::LSPS1ClientEvent; +use lightning_liquidity::lsps1::msgs::{ + ChannelInfo, LSPS1Options, OrderId, OrderParameters, PaymentInfo, +}; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; @@ -40,6 +44,11 @@ struct LSPS1Service { address: SocketAddress, token: Option, client_config: LSPS1ClientConfig, + pending_opening_params_requests: + Mutex>>, + pending_create_order_requests: Mutex>>, + pending_check_order_status_requests: + Mutex>>, } struct LSPS2Service { @@ -90,7 +99,18 @@ where ) -> &mut Self { // TODO: allow to set max_channel_fees_msat let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; - self.lsps1_service = Some(LSPS1Service { node_id, address, token, client_config }); + let pending_opening_params_requests = Mutex::new(HashMap::new()); + let pending_create_order_requests = Mutex::new(HashMap::new()); + let pending_check_order_status_requests = Mutex::new(HashMap::new()); + self.lsps1_service = Some(LSPS1Service { + node_id, + address, + token, + client_config, + pending_opening_params_requests, + pending_create_order_requests, + pending_check_order_status_requests, + }); self } @@ -174,6 +194,175 @@ where pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { + Event::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + request_id, + counterparty_node_id, + supported_options, + }) => { + if let Some(lsps1_service) = self.lsps1_service.as_ref() { + if counterparty_node_id != lsps1_service.node_id { + debug_assert!( + false, + "Received response from unexpected LSP counterparty. This should never happen." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_service + .pending_opening_params_requests + .lock() + .unwrap() + .remove(&request_id) + { + let response = LSPS1OpeningParamsResponse { supported_options }; + + match sender.send(response) { + Ok(()) => (), + Err(e) => { + log_error!( + self.logger, + "Failed to handle response from liquidity service: {:?}", + e + ); + }, + } + } else { + debug_assert!( + false, + "Received response from liquidity service for unknown request." + ); + log_error!( + self.logger, + "Received response from liquidity service for unknown request." + ); + } + } else { + log_error!( + self.logger, + "Received unexpected LSPS1Client::SupportedOptionsReady event!" + ); + } + }, + Event::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) => { + if let Some(lsps1_service) = self.lsps1_service.as_ref() { + if counterparty_node_id != lsps1_service.node_id { + debug_assert!( + false, + "Received response from unexpected LSP counterparty. This should never happen." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_service + .pending_create_order_requests + .lock() + .unwrap() + .remove(&request_id) + { + let response = LSPS1OrderStatus { + order_id, + order_params: order, + payment_options: payment, + channel_state: channel, + }; + + match sender.send(response) { + Ok(()) => (), + Err(e) => { + log_error!( + self.logger, + "Failed to handle response from liquidity service: {:?}", + e + ); + }, + } + } else { + debug_assert!( + false, + "Received response from liquidity service for unknown request." + ); + log_error!( + self.logger, + "Received response from liquidity service for unknown request." + ); + } + } else { + log_error!(self.logger, "Received unexpected LSPS1Client::OrderCreated event!"); + } + }, + Event::LSPS1Client(LSPS1ClientEvent::OrderStatus { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) => { + if let Some(lsps1_service) = self.lsps1_service.as_ref() { + if counterparty_node_id != lsps1_service.node_id { + debug_assert!( + false, + "Received response from unexpected LSP counterparty. This should never happen." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_service + .pending_check_order_status_requests + .lock() + .unwrap() + .remove(&request_id) + { + let response = LSPS1OrderStatus { + order_id, + order_params: order, + payment_options: payment, + channel_state: channel, + }; + + match sender.send(response) { + Ok(()) => (), + Err(e) => { + log_error!( + self.logger, + "Failed to handle response from liquidity service: {:?}", + e + ); + }, + } + } else { + debug_assert!( + false, + "Received response from liquidity service for unknown request." + ); + log_error!( + self.logger, + "Received response from liquidity service for unknown request." + ); + } + } else { + log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); + } + }, Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, @@ -282,6 +471,166 @@ where } } + pub(crate) async fn lsps1_request_opening_params( + &self, + ) -> Result { + let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { + log_error!(self.logger, "LSPS1 liquidity client was not configured.",); + Error::LiquiditySourceUnavailable + })?; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_opening_params_requests_lock = + lsps1_service.pending_opening_params_requests.lock().unwrap(); + let request_id = client_handler.request_supported_options(lsps1_service.node_id); + pending_opening_params_requests_lock.insert(request_id, request_sender); + } + + tokio::time::timeout(Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), request_receiver) + .await + .map_err(|e| { + log_error!(self.logger, "Liquidity request timed out: {}", e); + Error::LiquidityRequestFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to handle response from liquidity service: {}", e); + Error::LiquidityRequestFailed + }) + } + + pub(crate) async fn lsps1_request_channel( + &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, + announce_channel: bool, refund_address: bitcoin::Address, + ) -> Result { + let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { + log_error!(self.logger, "LSPS1 liquidity client was not configured.",); + Error::LiquiditySourceUnavailable + })?; + + let lsp_limits = self.lsps1_request_opening_params().await?.supported_options; + let channel_size_sat = lsp_balance_sat + client_balance_sat; + + if channel_size_sat < lsp_limits.min_channel_balance_sat + || channel_size_sat > lsp_limits.max_channel_balance_sat + { + log_error!( + self.logger, + "Requested channel size doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).", + lsp_limits.min_channel_balance_sat, + lsp_limits.max_channel_balance_sat + ); + return Err(Error::LiquidityRequestFailed); + } + + if lsp_balance_sat < lsp_limits.min_initial_lsp_balance_sat + || lsp_balance_sat > lsp_limits.max_initial_lsp_balance_sat + { + log_error!( + self.logger, + "Requested LSP-side balance doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).", + lsp_limits.min_initial_lsp_balance_sat, + lsp_limits.max_initial_lsp_balance_sat + ); + return Err(Error::LiquidityRequestFailed); + } + + if client_balance_sat < lsp_limits.min_initial_client_balance_sat + || client_balance_sat > lsp_limits.max_initial_client_balance_sat + { + log_error!( + self.logger, + "Requested client-side balance doesn't meet the LSP-provided limits (min: {}sat, max: {}sat).", + lsp_limits.min_initial_client_balance_sat, + lsp_limits.max_initial_client_balance_sat + ); + return Err(Error::LiquidityRequestFailed); + } + + let order_params = OrderParameters { + lsp_balance_sat, + client_balance_sat, + required_channel_confirmations: lsp_limits.min_required_channel_confirmations, + funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks, + channel_expiry_blocks, + token: lsps1_service.token.clone(), + announce_channel, + }; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_create_order_requests_lock = + lsps1_service.pending_create_order_requests.lock().unwrap(); + let request_id = client_handler.create_order( + &lsps1_service.node_id, + order_params.clone(), + Some(refund_address), + ); + pending_create_order_requests_lock.insert(request_id, request_sender); + } + + let response = tokio::time::timeout( + Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), + request_receiver, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Liquidity request timed out: {}", e); + Error::LiquidityRequestFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to handle response from liquidity service: {}", e); + Error::LiquidityRequestFailed + })?; + + if response.order_params != order_params { + log_error!( + self.logger, + "Aborting LSPS1 request as LSP-provided parameters don't match our order. Expected: {:?}, Received: {:?}", order_params, response.order_params + ); + return Err(Error::LiquidityRequestFailed); + } + + Ok(response) + } + + pub(crate) async fn lsps1_check_order_status( + &self, order_id: OrderId, + ) -> Result { + let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { + log_error!(self.logger, "LSPS1 liquidity client was not configured.",); + Error::LiquiditySourceUnavailable + })?; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_check_order_status_requests_lock = + lsps1_service.pending_check_order_status_requests.lock().unwrap(); + let request_id = client_handler.check_order_status(&lsps1_service.node_id, order_id); + pending_check_order_status_requests_lock.insert(request_id, request_sender); + } + + let response = tokio::time::timeout( + Duration::from_secs(LIQUIDITY_REQUEST_TIMEOUT_SECS), + request_receiver, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Liquidity request timed out: {}", e); + Error::LiquidityRequestFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to handle response from liquidity service: {}", e); + Error::LiquidityRequestFailed + })?; + + Ok(response) + } + pub(crate) async fn lsps2_receive_to_jit_channel( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, @@ -515,6 +864,24 @@ where } } +#[derive(Debug, Clone)] +pub(crate) struct LSPS1OpeningParamsResponse { + supported_options: LSPS1Options, +} + +/// Represents the status of an LSPS1 channel request. +#[derive(Debug, Clone)] +pub struct LSPS1OrderStatus { + /// The id of the channel order. + pub order_id: OrderId, + /// The parameters of channel order. + pub order_params: OrderParameters, + /// Contains details about how to pay for the order. + pub payment_options: PaymentInfo, + /// Contains information about the channel state. + pub channel_state: Option, +} + #[derive(Debug, Clone)] pub(crate) struct LSPS2FeeResponse { opening_fee_params_menu: Vec, From f368ac2f94996b90a4278ce407550830b37d69cb Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Dec 2024 14:02:26 +0100 Subject: [PATCH 4/6] Add LSPS1 API We add an `Lsps1Liquidity` API object, mirroring the approach we took with the `payment` APIs. --- src/lib.rs | 33 ++++++++++++- src/liquidity.rs | 119 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3df6e4234..381be81f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,7 @@ mod gossip; pub mod graph; mod hex_utils; pub mod io; -mod liquidity; +pub mod liquidity; pub mod logger; mod message_handler; pub mod payment; @@ -100,6 +100,7 @@ pub use bip39; pub use bitcoin; pub use lightning; pub use lightning_invoice; +pub use lightning_liquidity; pub use lightning_types; pub use vss_client; @@ -130,7 +131,7 @@ use event::{EventHandler, EventQueue}; use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; -use liquidity::LiquiditySource; +use liquidity::{LSPS1Liquidity, LiquiditySource}; use payment::store::PaymentStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, @@ -958,6 +959,34 @@ impl Node { )) } + /// Returns a liquidity handler allowing to request channels via the [bLIP-51 / LSPS1] protocol. + /// + /// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md + #[cfg(not(feature = "uniffi"))] + pub fn lsps1_liquidity(&self) -> LSPS1Liquidity { + LSPS1Liquidity::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), + Arc::clone(&self.logger), + ) + } + + /// Returns a liquidity handler allowing to request channels via the [bLIP-51 / LSPS1] protocol. + /// + /// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md + #[cfg(feature = "uniffi")] + pub fn lsps1_liquidity(&self) -> Arc { + Arc::new(LSPS1Liquidity::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), + Arc::clone(&self.logger), + )) + } + /// Retrieve a list of known channels. pub fn list_channels(&self) -> Vec { self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect() diff --git a/src/liquidity.rs b/src/liquidity.rs index 86e86fc9b..4609a6ef8 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -5,9 +5,12 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +//! Objects related to liquidity management. + use crate::chain::ChainSource; -use crate::logger::{log_debug, log_error, log_info, LdkLogger}; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager}; +use crate::connection::ConnectionManager; +use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; +use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{Config, Error}; use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; @@ -34,7 +37,7 @@ use tokio::sync::oneshot; use std::collections::HashMap; use std::ops::Deref; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; @@ -892,3 +895,113 @@ pub(crate) struct LSPS2BuyResponse { intercept_scid: u64, cltv_expiry_delta: u32, } + +/// A liquidity handler allowing to request channels via the [bLIP-51 / LSPS1] protocol. +/// +/// Should be retrieved by calling [`Node::lsps1_liquidity`]. +/// +/// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md +/// [`Node::lsps1_liquidity`]: crate::Node::lsps1_liquidity +#[derive(Clone)] +pub struct LSPS1Liquidity { + runtime: Arc>>>, + wallet: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, + logger: Arc, +} + +impl LSPS1Liquidity { + pub(crate) fn new( + runtime: Arc>>>, wallet: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, logger: Arc, + ) -> Self { + Self { runtime, wallet, connection_manager, liquidity_source, logger } + } + + /// Connects to the configured LSP and places an order for an inbound channel. + /// + /// The channel will be opened after one of the returned payment options has successfully been + /// paid. + pub fn request_channel( + &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, + announce_channel: bool, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let (lsp_node_id, lsp_address) = liquidity_source + .get_lsps1_service_details() + .ok_or(Error::LiquiditySourceUnavailable)?; + + let rt_lock = self.runtime.read().unwrap(); + let runtime = rt_lock.as_ref().unwrap(); + + let con_node_id = lsp_node_id; + let con_addr = lsp_address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; + + log_info!(self.logger, "Connected to LSP {}@{}. ", lsp_node_id, lsp_address); + + let refund_address = self.wallet.get_new_address()?; + + let liquidity_source = Arc::clone(&liquidity_source); + let response = tokio::task::block_in_place(move || { + runtime.block_on(async move { + liquidity_source + .lsps1_request_channel( + lsp_balance_sat, + client_balance_sat, + channel_expiry_blocks, + announce_channel, + refund_address, + ) + .await + }) + })?; + + Ok(response) + } + + /// Connects to the configured LSP and checks for the status of a previously-placed order. + pub fn check_order_status(&self, order_id: OrderId) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let (lsp_node_id, lsp_address) = liquidity_source + .get_lsps1_service_details() + .ok_or(Error::LiquiditySourceUnavailable)?; + + let rt_lock = self.runtime.read().unwrap(); + let runtime = rt_lock.as_ref().unwrap(); + + let con_node_id = lsp_node_id; + let con_addr = lsp_address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; + + let liquidity_source = Arc::clone(&liquidity_source); + let response = tokio::task::block_in_place(move || { + runtime + .block_on(async move { liquidity_source.lsps1_check_order_status(order_id).await }) + })?; + + Ok(response) + } +} From 78bc4fbc12c43203101a38f265eab6ee49efbbf5 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 4 Dec 2024 15:37:56 +0100 Subject: [PATCH 5/6] Add Uniffi bindings for LSPS1 API --- bindings/ldk_node.udl | 70 +++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 6 ++++ src/liquidity.rs | 69 ++++++++++++++++++++++++++++++++++++++---- src/uniffi_types.rs | 32 ++++++++++++++++++++ 4 files changed, 172 insertions(+), 5 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 3756b47d3..42b8d2522 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -58,6 +58,7 @@ interface Builder { void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); + void set_liquidity_source_lsps1(PublicKey node_id, SocketAddress address, string? token); void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token); void set_storage_dir_path(string storage_dir_path); void set_filesystem_logger(string? log_file_path, LogLevel? log_level); @@ -100,6 +101,7 @@ interface Node { SpontaneousPayment spontaneous_payment(); OnchainPayment onchain_payment(); UnifiedQrPayment unified_qr_payment(); + LSPS1Liquidity lsps1_liquidity(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -211,6 +213,13 @@ interface UnifiedQrPayment { QrPaymentResult send([ByRef]string uri_str); }; +interface LSPS1Liquidity { + [Throws=NodeError] + LSPS1OrderStatus request_channel(u64 lsp_balance_sat, u64 client_balance_sat, u32 channel_expiry_blocks, boolean announce_channel); + [Throws=NodeError] + LSPS1OrderStatus check_order_status(OrderId order_id); +}; + [Error] enum NodeError { "AlreadyRunning", @@ -258,6 +267,8 @@ enum NodeError { "InvalidUri", "InvalidQuantity", "InvalidNodeAlias", + "InvalidDateTime", + "InvalidFeeRate", "DuplicatePayment", "UnsupportedCurrency", "InsufficientFunds", @@ -410,6 +421,59 @@ dictionary CustomTlvRecord { sequence value; }; +dictionary LSPS1OrderStatus { + OrderId order_id; + OrderParameters order_params; + PaymentInfo payment_options; + ChannelOrderInfo? channel_state; +}; + +dictionary OrderParameters { + u64 lsp_balance_sat; + u64 client_balance_sat; + u16 required_channel_confirmations; + u16 funding_confirms_within_blocks; + u32 channel_expiry_blocks; + string? token; + boolean announce_channel; +}; + +dictionary PaymentInfo { + Bolt11PaymentInfo? bolt11; + OnchainPaymentInfo? onchain; +}; + +dictionary Bolt11PaymentInfo { + PaymentState state; + DateTime expires_at; + u64 fee_total_sat; + u64 order_total_sat; + Bolt11Invoice invoice; +}; + +dictionary OnchainPaymentInfo { + PaymentState state; + DateTime expires_at; + u64 fee_total_sat; + u64 order_total_sat; + Address address; + u16? min_onchain_payment_confirmations; + FeeRate min_fee_for_0conf; + Address? refund_onchain_address; +}; + +dictionary ChannelOrderInfo { + DateTime funded_at; + OutPoint funding_outpoint; + DateTime expires_at; +}; + +enum PaymentState { + "ExpectPayment", + "Paid", + "Refunded", +}; + [Enum] interface MaxTotalRoutingFeeLimit { None (); @@ -656,3 +720,9 @@ typedef string UntrustedString; [Custom] typedef string NodeAlias; + +[Custom] +typedef string OrderId; + +[Custom] +typedef string DateTime; diff --git a/src/error.rs b/src/error.rs index ec1182c87..2cb71186d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -106,6 +106,10 @@ pub enum Error { InvalidQuantity, /// The given node alias is invalid. InvalidNodeAlias, + /// The given date time is invalid. + InvalidDateTime, + /// The given fee rate is invalid. + InvalidFeeRate, /// A payment with the given hash has already been initiated. DuplicatePayment, /// The provided offer was denonminated in an unsupported currency. @@ -172,6 +176,8 @@ impl fmt::Display for Error { Self::InvalidUri => write!(f, "The given URI is invalid."), Self::InvalidQuantity => write!(f, "The given quantity is invalid."), Self::InvalidNodeAlias => write!(f, "The given node alias is invalid."), + Self::InvalidDateTime => write!(f, "The given date time is invalid."), + Self::InvalidFeeRate => write!(f, "The given fee rate is invalid."), Self::DuplicatePayment => { write!(f, "A payment with the given hash has already been initiated.") }, diff --git a/src/liquidity.rs b/src/liquidity.rs index 4609a6ef8..d1cd6136e 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -21,9 +21,7 @@ use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; use lightning_liquidity::lsps1::client::LSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; -use lightning_liquidity::lsps1::msgs::{ - ChannelInfo, LSPS1Options, OrderId, OrderParameters, PaymentInfo, -}; +use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, OrderParameters}; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; @@ -280,7 +278,7 @@ where let response = LSPS1OrderStatus { order_id, order_params: order, - payment_options: payment, + payment_options: payment.into(), channel_state: channel, }; @@ -338,7 +336,7 @@ where let response = LSPS1OrderStatus { order_id, order_params: order, - payment_options: payment, + payment_options: payment.into(), channel_state: channel, }; @@ -885,6 +883,67 @@ pub struct LSPS1OrderStatus { pub channel_state: Option, } +#[cfg(not(feature = "uniffi"))] +type PaymentInfo = lightning_liquidity::lsps1::msgs::PaymentInfo; + +/// Details regarding how to pay for an order. +#[cfg(feature = "uniffi")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentInfo { + /// A Lightning payment using BOLT 11. + pub bolt11: Option, + /// An onchain payment. + pub onchain: Option, +} + +#[cfg(feature = "uniffi")] +impl From for PaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::PaymentInfo) -> Self { + PaymentInfo { bolt11: value.bolt11, onchain: value.onchain.map(|o| o.into()) } + } +} + +/// An onchain payment. +#[cfg(feature = "uniffi")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OnchainPaymentInfo { + /// Indicates the current state of the payment. + pub state: lightning_liquidity::lsps1::msgs::PaymentState, + /// The datetime when the payment option expires. + pub expires_at: chrono::DateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel + /// opened. + pub address: bitcoin::Address, + /// The minimum number of block confirmations that are required for the on-chain payment to be + /// considered confirmed. + pub min_onchain_payment_confirmations: Option, + /// The minimum fee rate for the on-chain payment in case the client wants the payment to be + /// confirmed without a confirmation. + pub min_fee_for_0conf: Arc, + /// The address where the LSP will send the funds if the order fails. + pub refund_onchain_address: Option, +} + +#[cfg(feature = "uniffi")] +impl From for OnchainPaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::OnchainPaymentInfo) -> Self { + Self { + state: value.state, + expires_at: value.expires_at, + fee_total_sat: value.fee_total_sat, + order_total_sat: value.order_total_sat, + address: value.address, + min_onchain_payment_confirmations: value.min_onchain_payment_confirmations, + min_fee_for_0conf: Arc::new(value.min_fee_for_0conf), + refund_onchain_address: value.refund_onchain_address, + } + } +} + #[derive(Debug, Clone)] pub(crate) struct LSPS2FeeResponse { opening_fee_params_menu: Vec, diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 2747503aa..74d253a90 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -14,6 +14,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; +pub use crate::liquidity::{LSPS1OrderStatus, OnchainPaymentInfo, PaymentInfo}; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; @@ -30,12 +31,19 @@ pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_invoice::{Bolt11Invoice, Description}; +pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; +pub use lightning_liquidity::lsps1::msgs::{ + Bolt11PaymentInfo, OrderId, OrderParameters, PaymentState, +}; + pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; pub use bip39::Mnemonic; pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; +pub type DateTime = chrono::DateTime; + use crate::UniffiCustomTypeConverter; use crate::builder::sanitize_alias; @@ -393,6 +401,30 @@ impl From for Bolt11InvoiceDescript } } +impl UniffiCustomTypeConverter for OrderId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Self(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.0 + } +} + +impl UniffiCustomTypeConverter for DateTime { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(DateTime::from_str(&val).map_err(|_| Error::InvalidDateTime)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_rfc3339() + } +} + #[cfg(test)] mod tests { use super::*; From e3725b402d0653701a02b0223242abf1ab44dc7b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 30 Jan 2025 14:40:29 +0100 Subject: [PATCH 6/6] Update LSPS2 link in payment store --- src/payment/store.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/payment/store.rs b/src/payment/store.rs index 9ae137de9..e75fb2169 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -187,10 +187,10 @@ pub enum PaymentKind { /// The secret used by the payment. secret: Option, }, - /// A [BOLT 11] payment intended to open an [LSPS 2] just-in-time channel. + /// A [BOLT 11] payment intended to open an [bLIP-52 / LSPS 2] just-in-time channel. /// /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md - /// [LSPS 2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md Bolt11Jit { /// The payment hash, i.e., the hash of the `preimage`. hash: PaymentHash,