diff --git a/Cargo.toml b/Cargo.toml index 18947b72f..d1fa9b420 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,17 +39,17 @@ default = [] #lightning-liquidity = { version = "0.2.0", features = ["std"] } #lightning-macros = { version = "0.2.0" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245", features = ["std"] } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245" } +lightning = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a", features = ["std"] } +lightning-types = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-invoice = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-persister = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-rapid-gossip-sync = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-block-sync = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a", features = ["std"] } +lightning-macros = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -85,7 +85,7 @@ bitcoin-payment-instructions = { git = "https://github.com/jkczyz/bitcoin-paymen winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "98501d6e5134228c41460dcf786ab53337e41245", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a", features = ["std", "_test_utils"] } rand = { version = "0.9.2", default-features = false, features = ["std", "thread_rng", "os_rng"] } proptest = "1.0.0" regex = "1.5.6" @@ -171,15 +171,15 @@ harness = false #vss-client-ng = { path = "../vss-client" } #vss-client-ng = { git = "https://github.com/lightningdevkit/vss-client", branch = "main" } # -#[patch."https://github.com/lightningdevkit/rust-lightning"] -#lightning = { path = "../rust-lightning/lightning" } -#lightning-types = { path = "../rust-lightning/lightning-types" } -#lightning-invoice = { path = "../rust-lightning/lightning-invoice" } -#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } -#lightning-persister = { path = "../rust-lightning/lightning-persister" } -#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } -#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" } -#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" } -#lightning-macros = { path = "../rust-lightning/lightning-macros" } +[patch."https://github.com/lightningdevkit/rust-lightning"] +lightning = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-types = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-invoice = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-net-tokio = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-persister = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-background-processor = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-rapid-gossip-sync = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-block-sync = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-transaction-sync = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-liquidity = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } +lightning-macros = { git = "https://github.com/tnull/rust-lightning", rev = "2cb054601134943d5c8e5a39d014e7b125c7a19a" } diff --git a/src/builder.rs b/src/builder.rs index 7641a767d..2e5192ec0 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -39,6 +39,7 @@ use lightning::util::persist::{ }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; +use lightning_liquidity::lsps2::router::LSPS2BOLT12Router; use lightning_persister::fs_store::v1::FilesystemStore; use vss_client::headers::VssHeaderProvider; @@ -1518,13 +1519,14 @@ fn build_with_store_internal( } let scoring_fee_params = ProbabilisticScoringFeeParameters::default(); - let router = Arc::new(DefaultRouter::new( + let inner_router = DefaultRouter::new( Arc::clone(&network_graph), Arc::clone(&logger), Arc::clone(&keys_manager), Arc::clone(&scorer), scoring_fee_params, - )); + ); + let router = Arc::new(LSPS2BOLT12Router::new(inner_router, Arc::clone(&keys_manager))); let mut user_config = default_user_config(&config); @@ -1681,56 +1683,64 @@ fn build_with_store_internal( }, }; - let (liquidity_source, custom_message_handler) = - if let Some(lsc) = liquidity_source_config.as_ref() { - let mut liquidity_source_builder = LiquiditySourceBuilder::new( - Arc::clone(&wallet), - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - Arc::clone(&chain_source), - Arc::clone(&tx_broadcaster), - Arc::clone(&kv_store), - Arc::clone(&config), - Arc::clone(&logger), - ); + let (liquidity_source, custom_message_handler) = if let Some(lsc) = + liquidity_source_config.as_ref() + { + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&router), + Arc::clone(&chain_source), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Some(Arc::clone(&onion_messenger) + as Arc< + dyn lightning::onion_message::messenger::OnionMessageInterceptor + Send + Sync, + >), + Arc::clone(&logger), + ); - lsc.lsps1_client.as_ref().map(|config| { - liquidity_source_builder.lsps1_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - lsc.lsps2_client.as_ref().map(|config| { - liquidity_source_builder.lsps2_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - let promise_secret = { - let lsps_xpriv = derive_xprv( - Arc::clone(&config), - &seed_bytes, - LSPS_HARDENED_CHILD_INDEX, - Arc::clone(&logger), - )?; - lsps_xpriv.private_key.secret_bytes() - }; - lsc.lsps2_service.as_ref().map(|config| { - liquidity_source_builder.lsps2_service(promise_secret, config.clone()) - }); - - let liquidity_source = runtime - .block_on(async move { liquidity_source_builder.build().await.map(Arc::new) })?; - let custom_message_handler = - Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); - (Some(liquidity_source), custom_message_handler) - } else { - (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) + let promise_secret = { + let lsps_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + LSPS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + lsps_xpriv.private_key.secret_bytes() }; + lsc.lsps2_service + .as_ref() + .map(|config| liquidity_source_builder.lsps2_service(promise_secret, config.clone())); + + let liquidity_source = runtime + .block_on(async move { liquidity_source_builder.build().await.map(Arc::new) })?; + // TODO: Rehydrate persisted `OfferId -> LSPS2Bolt12InvoiceParameters` mappings here for + // client nodes and call `router.register_offer(...)` before startup completes. + let custom_message_handler = + Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); + (Some(liquidity_source), custom_message_handler) + } else { + (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) + }; let msg_handler = match gossip_source.as_gossip_sync() { GossipSync::P2P(p2p_gossip_sync) => MessageHandler { diff --git a/src/lib.rs b/src/lib.rs index 3e5180dcb..123668e90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -897,9 +897,13 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn bolt12_payment(&self) -> Bolt12Payment { Bolt12Payment::new( + Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -913,9 +917,13 @@ impl Node { #[cfg(feature = "uniffi")] pub fn bolt12_payment(&self) -> Arc { Arc::new(Bolt12Payment::new( + Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + self.liquidity_source.clone(), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), @@ -1463,7 +1471,12 @@ impl Node { let funding_template = self .channel_manager - .splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate) + .splice_channel( + &channel_details.channel_id, + &counterparty_node_id, + fee_rate, + fee_rate, + ) .map_err(|e| { log_error!(self.logger, "Failed to splice channel: {:?}", e); Error::ChannelSplicingFailed @@ -1572,7 +1585,12 @@ impl Node { let funding_template = self .channel_manager - .splice_channel(&channel_details.channel_id, &counterparty_node_id, fee_rate) + .splice_channel( + &channel_details.channel_id, + &counterparty_node_id, + fee_rate, + fee_rate, + ) .map_err(|e| { log_error!(self.logger, "Failed to splice channel: {:?}", e); Error::ChannelSplicingFailed diff --git a/src/liquidity.rs b/src/liquidity.rs index ee9863eb6..2e6fbde0b 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -19,6 +19,8 @@ use lightning::events::HTLCHandlingFailureType; use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; use lightning::ln::types::ChannelId; +use lightning::offers::offer::OfferId; +use lightning::onion_message::messenger::OnionMessageInterceptor; use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning::sign::EntropySource; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; @@ -32,6 +34,7 @@ use lightning_liquidity::lsps1::msgs::{ use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; use lightning_liquidity::lsps2::msgs::{LSPS2OpeningFeeParams, LSPS2RawOpeningFeeParams}; +use lightning_liquidity::lsps2::router::LSPS2Bolt12InvoiceParameters; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; @@ -44,7 +47,8 @@ use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::runtime::Runtime; use crate::types::{ - Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet, + Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Router, + Wallet, }; use crate::{total_anchor_channels_reserve_sats, Config, Error}; @@ -154,11 +158,13 @@ where lsps2_service: Option, wallet: Arc, channel_manager: Arc, + router: Arc, keys_manager: Arc, chain_source: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, + onion_message_interceptor: Option>, logger: L, } @@ -168,8 +174,10 @@ where { pub(crate) fn new( wallet: Arc, channel_manager: Arc, keys_manager: Arc, - chain_source: Arc, tx_broadcaster: Arc, kv_store: Arc, - config: Arc, logger: L, + router: Arc, chain_source: Arc, tx_broadcaster: Arc, + kv_store: Arc, config: Arc, + onion_message_interceptor: Option>, + logger: L, ) -> Self { let lsps1_client = None; let lsps2_client = None; @@ -180,11 +188,13 @@ where lsps2_service, wallet, channel_manager, + router, keys_manager, chain_source, tx_broadcaster, kv_store, config, + onion_message_interceptor, logger, } } @@ -262,6 +272,7 @@ where Arc::clone(&self.tx_broadcaster), liquidity_service_config, liquidity_client_config, + self.onion_message_interceptor, ) .await .map_err(|_| BuildError::ReadFailed)?, @@ -273,6 +284,7 @@ where lsps2_service: self.lsps2_service, wallet: self.wallet, channel_manager: self.channel_manager, + router: self.router, peer_manager: RwLock::new(None), keys_manager: self.keys_manager, liquidity_manager, @@ -291,6 +303,7 @@ where lsps2_service: Option, wallet: Arc, channel_manager: Arc, + router: Arc, peer_manager: RwLock>>, keys_manager: Arc, liquidity_manager: Arc, @@ -1191,6 +1204,104 @@ where Ok((invoice, min_prop_fee_ppm_msat)) } + pub(crate) async fn lsps2_register_bolt12_payment_paths( + &self, offer_id: OfferId, amount_msat: u64, max_total_lsp_fee_limit_msat: Option, + ) -> Result { + let fee_response = self.lsps2_request_opening_fee_params().await?; + + let (min_total_fee_msat, min_opening_params) = fee_response + .opening_fee_params_menu + .into_iter() + .filter_map(|params| { + if amount_msat < params.min_payment_size_msat + || amount_msat > params.max_payment_size_msat + { + log_debug!(self.logger, + "Skipping LSP-offered JIT parameters as the payment of {}msat doesn't meet LSP limits (min: {}msat, max: {}msat)", + amount_msat, + params.min_payment_size_msat, + params.max_payment_size_msat + ); + None + } else { + compute_opening_fee(amount_msat, params.min_fee_msat, params.proportional as u64) + .map(|fee| (fee, params)) + } + }) + .min_by_key(|p| p.0) + .ok_or_else(|| { + log_error!(self.logger, "Failed to handle response from liquidity service",); + Error::LiquidityRequestFailed + })?; + + if let Some(max_total_lsp_fee_limit_msat) = max_total_lsp_fee_limit_msat { + if min_total_fee_msat > max_total_lsp_fee_limit_msat { + log_error!(self.logger, + "Failed to request inbound JIT channel as LSP's requested total opening fee of {}msat exceeds our fee limit of {}msat", + min_total_fee_msat, max_total_lsp_fee_limit_msat + ); + return Err(Error::LiquidityFeeTooHigh); + } + } + + let buy_response = + self.lsps2_send_buy_request(Some(amount_msat), min_opening_params).await?; + self.register_lsps2_bolt12_payment_paths(offer_id, buy_response)?; + + Ok(min_total_fee_msat) + } + + pub(crate) async fn lsps2_register_variable_amount_bolt12_payment_paths( + &self, offer_id: OfferId, max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let fee_response = self.lsps2_request_opening_fee_params().await?; + + let (min_prop_fee_ppm_msat, min_opening_params) = fee_response + .opening_fee_params_menu + .into_iter() + .map(|params| (params.proportional as u64, params)) + .min_by_key(|p| p.0) + .ok_or_else(|| { + log_error!(self.logger, "Failed to handle response from liquidity service",); + Error::LiquidityRequestFailed + })?; + + if let Some(max_proportional_lsp_fee_limit_ppm_msat) = + max_proportional_lsp_fee_limit_ppm_msat + { + if min_prop_fee_ppm_msat > max_proportional_lsp_fee_limit_ppm_msat { + log_error!(self.logger, + "Failed to request inbound JIT channel as LSP's requested proportional opening fee of {} ppm msat exceeds our fee limit of {} ppm msat", + min_prop_fee_ppm_msat, + max_proportional_lsp_fee_limit_ppm_msat + ); + return Err(Error::LiquidityFeeTooHigh); + } + } + + let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?; + self.register_lsps2_bolt12_payment_paths(offer_id, buy_response)?; + + Ok(min_prop_fee_ppm_msat) + } + + fn register_lsps2_bolt12_payment_paths( + &self, offer_id: OfferId, buy_response: LSPS2BuyResponse, + ) -> Result<(), Error> { + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + self.router.register_offer( + offer_id, + LSPS2Bolt12InvoiceParameters { + counterparty_node_id: lsps2_client.lsp_node_id, + intercept_scid: buy_response.intercept_scid, + cltv_expiry_delta: buy_response.cltv_expiry_delta, + }, + ); + + Ok(()) + } + async fn lsps2_request_opening_fee_params(&self) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -1303,7 +1414,14 @@ where src_node_id: lsps2_client.lsp_node_id, short_channel_id: buy_response.intercept_scid, fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, - cltv_expiry_delta: buy_response.cltv_expiry_delta as u16, + cltv_expiry_delta: u16::try_from(buy_response.cltv_expiry_delta).map_err(|_| { + log_error!( + self.logger, + "Failed to create JIT invoice as LSPS2 CLTV delta {} exceeds supported range", + buy_response.cltv_expiry_delta + ); + Error::LiquidityRequestFailed + })?, htlc_minimum_msat: None, htlc_maximum_msat: None, }]); diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 980e20696..f622518e5 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -25,10 +25,14 @@ use lightning::util::ser::{Readable, Writeable}; use lightning_types::string::UntrustedString; use crate::config::{AsyncPaymentsRole, Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::connection::ConnectionManager; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; +use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::peer_store::{PeerInfo, PeerStore}; +use crate::runtime::Runtime; use crate::types::{ChannelManager, KeysManager, PaymentStore}; #[cfg(not(feature = "uniffi"))] @@ -59,9 +63,13 @@ type HumanReadableName = Arc; /// [`Node::bolt12_payment`]: crate::Node::bolt12_payment #[cfg_attr(feature = "uniffi", derive(uniffi::Object))] pub struct Bolt12Payment { + runtime: Arc, channel_manager: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, keys_manager: Arc, payment_store: Arc, + peer_store: Arc>>, config: Arc, is_running: Arc>, logger: Arc, @@ -70,14 +78,22 @@ pub struct Bolt12Payment { impl Bolt12Payment { pub(crate) fn new( - channel_manager: Arc, keys_manager: Arc, - payment_store: Arc, config: Arc, is_running: Arc>, - logger: Arc, async_payments_role: Option, + runtime: Arc, channel_manager: Arc, + connection_manager: Arc>>, + liquidity_source: Option>>>, + keys_manager: Arc, payment_store: Arc, + peer_store: Arc>>, config: Arc, + is_running: Arc>, logger: Arc, + async_payments_role: Option, ) -> Self { Self { + runtime, channel_manager, + connection_manager, + liquidity_source, keys_manager, payment_store, + peer_store, config, is_running, logger, @@ -231,6 +247,107 @@ impl Bolt12Payment { Ok(finalized_offer) } + fn receive_variable_amount_inner( + &self, description: &str, expiry_secs: Option, + ) -> Result { + let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + + if let Some(expiry_secs) = expiry_secs { + let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) + .duration_since(UNIX_EPOCH) + .unwrap(); + offer_builder = offer_builder.absolute_expiry(absolute_expiry); + } + + offer_builder.description(description.to_string()).build().map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + }) + } + + fn connect_to_lsps2_peer( + &self, liquidity_source: Arc>>, + ) -> Result { + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; + + let peer_info = PeerInfo { node_id, address }; + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let connection_manager = Arc::clone(&self.connection_manager); + + self.runtime.block_on(async move { + connection_manager.connect_peer_if_necessary(con_node_id, con_addr).await + })?; + + log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); + + Ok(peer_info) + } + + fn receive_jit_channel_inner( + &self, amount_msat: Option, description: &str, expiry_secs: Option, + quantity: Option, max_total_lsp_fee_limit_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let peer_info = self.connect_to_lsps2_peer(Arc::clone(liquidity_source))?; + let offer = if let Some(amount_msat) = amount_msat { + self.receive_inner(amount_msat, description, expiry_secs, quantity)? + } else { + self.receive_variable_amount_inner(description, expiry_secs)? + }; + + let offer_id = offer.id(); + let liquidity_source = Arc::clone(liquidity_source); + let (lsp_total_opening_fee, lsp_prop_opening_fee) = self.runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_register_bolt12_payment_paths( + offer_id, + amount_msat, + max_total_lsp_fee_limit_msat, + ) + .await + .map(|total_fee| (Some(total_fee), None)) + } else { + liquidity_source + .lsps2_register_variable_amount_bolt12_payment_paths( + offer_id, + max_proportional_lsp_fee_limit_ppm_msat, + ) + .await + .map(|prop_fee| (None, Some(prop_fee))) + } + })?; + + if let Some(total_fee_msat) = lsp_total_opening_fee { + log_info!( + self.logger, + "JIT-channel BOLT12 offer created: {} (max total LSP opening fee: {}msat)", + offer, + total_fee_msat + ); + } + if let Some(prop_fee_ppm_msat) = lsp_prop_opening_fee { + log_info!( + self.logger, + "JIT-channel variable-amount BOLT12 offer created: {} (max proportional LSP opening fee: {}ppm msat)", + offer, + prop_fee_ppm_msat + ); + } + + self.peer_store.add_peer(peer_info)?; + + Ok(offer) + } + fn blinded_paths_for_async_recipient_internal( &self, recipient_id: Vec, ) -> Result, Error> { @@ -397,23 +514,41 @@ impl Bolt12Payment { pub fn receive_variable_amount( &self, description: &str, expiry_secs: Option, ) -> Result { - let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { - log_error!(self.logger, "Failed to create offer builder: {:?}", e); - Error::OfferCreationFailed - })?; - - if let Some(expiry_secs) = expiry_secs { - let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) - .duration_since(UNIX_EPOCH) - .unwrap(); - offer_builder = offer_builder.absolute_expiry(absolute_expiry); - } + let offer = self.receive_variable_amount_inner(description, expiry_secs)?; + Ok(maybe_wrap(offer)) + } - let offer = offer_builder.description(description.to_string()).build().map_err(|e| { - log_error!(self.logger, "Failed to create offer: {:?}", e); - Error::OfferCreationFailed - })?; + /// Returns a payable offer that can be used to request a payment of the amount given and + /// receive it via a newly created just-in-time (JIT) channel. + pub fn receive_via_jit_channel( + &self, amount_msat: u64, description: &str, expiry_secs: Option, + quantity: Option, max_total_lsp_fee_limit_msat: Option, + ) -> Result { + let offer = self.receive_jit_channel_inner( + Some(amount_msat), + description, + expiry_secs, + quantity, + max_total_lsp_fee_limit_msat, + None, + )?; + Ok(maybe_wrap(offer)) + } + /// Returns a payable offer that can be used to request a variable amount payment and receive it + /// via a newly created just-in-time (JIT) channel. + pub fn receive_variable_amount_via_jit_channel( + &self, description: &str, expiry_secs: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let offer = self.receive_jit_channel_inner( + None, + description, + expiry_secs, + None, + None, + max_proportional_lsp_fee_limit_ppm_msat, + )?; Ok(maybe_wrap(offer)) } diff --git a/src/types.rs b/src/types.rs index 381bfbd21..1265b0198 100644 --- a/src/types.rs +++ b/src/types.rs @@ -29,6 +29,7 @@ use lightning::util::persist::{ use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::GossipVerifier; +use lightning_liquidity::lsps2::router::LSPS2BOLT12Router; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; @@ -254,7 +255,7 @@ pub(crate) type Broadcaster = crate::tx_broadcaster::TransactionBroadcaster, Arc, Arc, @@ -262,6 +263,7 @@ pub(crate) type Router = DefaultRouter< ProbabilisticScoringFeeParameters, Scorer, >; +pub(crate) type Router = LSPS2BOLT12Router>; pub(crate) type Scorer = CombinedScorer, Arc>; pub(crate) type Graph = gossip::NetworkGraph>; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 3fde52dc4..cdbcc3c06 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1904,6 +1904,102 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps2_bolt12_payment_succeeds_after_lsp_restart() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm: 10_000, + channel_over_provisioning_ppm: 100_000, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + client_trusts_lsp: true, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); + service_node.start().unwrap(); + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url, Some(sync_config)); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); + payer_node.start().unwrap(); + + let service_onchain_addr = service_node.onchain_payment().new_address().unwrap(); + let client_onchain_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_onchain_addr = payer_node.onchain_payment().new_address().unwrap(); + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_onchain_addr, client_onchain_addr, payer_onchain_addr], + Amount::from_sat(10_000_000), + ) + .await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let jit_amount_msat = 100_000_000; + let offer = client_node + .bolt12_payment() + .receive_via_jit_channel(jit_amount_msat, "lsps2-bolt12-after-restart", None, Some(1), None) + .unwrap(); + + service_node.stop().unwrap(); + service_node.start().unwrap(); + + // Ensure peers are connected after the restart before paying the offer. + let _ = payer_node.connect(service_node_id, service_addr.clone(), false); + let _ = client_node.connect(service_node_id, service_addr, false); + + let payment_id = payer_node + .bolt12_payment() + .send(&offer, Some(1), Some("restart".to_string()), None) + .unwrap(); + + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * 10_000) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_received_event!(client_node, expected_received_amount_msat); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn facade_logging() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();