diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 335bac5ff..506f8891b 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -25,6 +25,18 @@ dictionary EsploraSyncConfig { u64 fee_rate_cache_update_interval_secs; }; +dictionary LSPS2ServiceConfig { + string? require_token; + boolean advertise_service; + u32 channel_opening_fee_ppm; + u32 channel_over_provisioning_ppm; + u64 min_channel_opening_fee_msat; + u32 min_channel_lifetime; + u32 max_client_to_self_delay; + u64 min_payment_size_msat; + u64 max_payment_size_msat; +}; + enum LogLevel { "Gossip", "Trace", diff --git a/src/builder.rs b/src/builder.rs index 67b493675..4edd27397 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,7 +18,9 @@ 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::LiquiditySourceBuilder; +use crate::liquidity::{ + LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, +}; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -75,6 +77,10 @@ use std::sync::{Arc, Mutex, RwLock}; use std::time::SystemTime; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; +const VSS_HARDENED_CHILD_INDEX: u32 = 877; +const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; +const LSPS_HARDENED_CHILD_INDEX: u32 = 577; + #[derive(Debug, Clone)] enum ChainDataSourceConfig { Esplora { server_url: String, sync_config: Option }, @@ -94,18 +100,14 @@ enum GossipSourceConfig { RapidGossipSync(String), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] 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 { lsps1_service: None, lsps2_service: None } - } + // Act as an LSPS1 client connecting to the given service. + lsps1_client: Option, + // Act as an LSPS2 client connecting to the given service. + lsps2_client: Option, + // Act as an LSPS2 service. + lsps2_service: Option, } #[derive(Clone)] @@ -317,7 +319,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps1_service = Some((node_id, address, token)); + let lsps1_client_config = LSPS1ClientConfig { node_id, address, token }; + liquidity_source_config.lsps1_client = Some(lsps1_client_config); self } @@ -337,7 +340,23 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some((node_id, address, token)); + let lsps2_client_config = LSPS2ClientConfig { node_id, address, token }; + liquidity_source_config.lsps2_client = Some(lsps2_client_config); + self + } + + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2( + &mut self, service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + liquidity_source_config.lsps2_service = Some(service_config); self } @@ -464,10 +483,14 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config, &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = + derive_xprv(config, &seed_bytes, VSS_HARDENED_CHILD_INDEX, Arc::clone(&logger))?; let lnurl_auth_xprv = vss_xprv - .derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 138 }]) + .derive_priv( + &Secp256k1::new(), + &[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }], + ) .map_err(|e| { log_error!(logger, "Failed to derive VSS secret: {}", e); BuildError::KVStoreSetupFailed @@ -529,7 +552,12 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config.clone(), &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = derive_xprv( + config.clone(), + &seed_bytes, + VSS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; let vss_seed_bytes: [u8; 32] = vss_xprv.private_key.secret_bytes(); @@ -684,6 +712,16 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token); } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2(&self, service_config: LSPS2ServiceConfig) { + self.inner.write().unwrap().set_liquidity_provider_lsps2(service_config); + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&self, storage_dir_path: String) { self.inner.write().unwrap().set_storage_dir_path(storage_dir_path); @@ -1027,7 +1065,7 @@ fn build_with_store_internal( }; let mut user_config = default_user_config(&config); - if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { + if liquidity_source_config.and_then(|lsc| lsc.lsps2_client.as_ref()).is_some() { // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll // check that they don't take too much before claiming. user_config.channel_config.accept_underpaying_htlcs = true; @@ -1039,6 +1077,12 @@ fn build_with_store_internal( 100; } + if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { + // If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the + // information to the service handler. + user_config.accept_intercept_htlcs = true; + } + let message_router = Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); @@ -1159,31 +1203,53 @@ fn build_with_store_internal( }, }; - 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()) - }); + 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(&config), + Arc::clone(&logger), + ); - lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone()) - }); + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - Arc::new(liquidity_source_builder.build()) - }); + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() { - Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))) - } else { - 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 = Arc::new(liquidity_source_builder.build()); + 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 { @@ -1382,8 +1448,8 @@ fn seed_bytes_from_config( } } -fn derive_vss_xprv( - config: Arc, seed_bytes: &[u8; 64], logger: Arc, +fn derive_xprv( + config: Arc, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc, ) -> Result { use bitcoin::key::Secp256k1; @@ -1392,10 +1458,11 @@ fn derive_vss_xprv( BuildError::InvalidSeedBytes })?; - xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 877 }]).map_err(|e| { - log_error!(logger, "Failed to derive VSS secret: {}", e); - BuildError::KVStoreSetupFailed - }) + xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }]) + .map_err(|e| { + log_error!(logger, "Failed to derive hardened child secret: {}", e); + BuildError::InvalidSeedBytes + }) } /// Sanitize the user-provided node alias to ensure that it is a valid protocol-specified UTF-8 string. diff --git a/src/event.rs b/src/event.rs index edaa561dd..cd9b74215 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,6 +14,8 @@ use crate::{ use crate::connection::ConnectionManager; use crate::fee_estimator::ConfirmationTarget; +use crate::liquidity::LiquiditySource; +use crate::logger::Logger; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, @@ -445,6 +447,7 @@ where connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, @@ -461,6 +464,7 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, logger: L, config: Arc, ) -> Self { @@ -472,6 +476,7 @@ where connection_manager, output_sweeper, network_graph, + liquidity_source, payment_store, peer_store, logger, @@ -1009,7 +1014,11 @@ where LdkEvent::PaymentPathFailed { .. } => {}, LdkEvent::ProbeSuccessful { .. } => {}, LdkEvent::ProbeFailed { .. } => {}, - LdkEvent::HTLCHandlingFailed { .. } => {}, + LdkEvent::HTLCHandlingFailed { failed_next_destination, .. } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_handling_failed(failed_next_destination); + } + }, LdkEvent::PendingHTLCsForwardable { time_forwardable } => { let forwarding_channel_manager = self.channel_manager.clone(); let min = time_forwardable.as_millis() as u64; @@ -1161,23 +1170,6 @@ where claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { - let event = Event::PaymentForwarded { - prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), - next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), - prev_user_channel_id: prev_user_channel_id.map(UserChannelId), - next_user_channel_id: next_user_channel_id.map(UserChannelId), - prev_node_id, - next_node_id, - total_fee_earned_msat, - skimmed_fee_msat, - claim_from_onchain_tx, - outbound_amount_forwarded_msat, - }; - self.event_queue.add_event(event).map_err(|e| { - log_error!(self.logger, "Failed to push to event queue: {}", e); - ReplayEvent() - })?; - let read_only_network_graph = self.network_graph.read_only(); let nodes = read_only_network_graph.nodes(); let channels = self.channel_manager.list_channels(); @@ -1210,14 +1202,13 @@ where format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); let fee_earned = total_fee_earned_msat.unwrap_or(0); - let outbound_amount_forwarded_msat = outbound_amount_forwarded_msat.unwrap_or(0); if claim_from_onchain_tx { log_info!( self.logger, "Forwarded payment{}{} of {}msat, earning {}msat in fees from claiming onchain.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } else { @@ -1226,10 +1217,31 @@ where "Forwarded payment{}{} of {}msat, earning {}msat in fees.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } + + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_payment_forwarded(next_channel_id); + } + + let event = Event::PaymentForwarded { + prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), + next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), + prev_user_channel_id: prev_user_channel_id.map(UserChannelId), + next_user_channel_id: next_user_channel_id.map(UserChannelId), + prev_node_id, + next_node_id, + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx, + outbound_amount_forwarded_msat, + }; + self.event_queue.add_event(event).map_err(|e| { + log_error!(self.logger, "Failed to push to event queue: {}", e); + ReplayEvent() + })?; }, LdkEvent::ChannelPending { channel_id, @@ -1303,6 +1315,14 @@ where counterparty_node_id, ); + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_channel_ready( + user_channel_id, + &channel_id, + &counterparty_node_id, + ); + } + let event = Event::ChannelReady { channel_id, user_channel_id: UserChannelId(user_channel_id), @@ -1341,7 +1361,22 @@ where }; }, LdkEvent::DiscardFunding { .. } => {}, - LdkEvent::HTLCIntercepted { .. } => {}, + LdkEvent::HTLCIntercepted { + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + .. + } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_intercepted( + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ); + } + }, LdkEvent::InvoiceReceived { .. } => { debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); }, diff --git a/src/lib.rs b/src/lib.rs index 1cddf9a31..d6652693c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -533,6 +533,7 @@ impl Node { Arc::clone(&self.connection_manager), Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), + self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), diff --git a/src/liquidity.rs b/src/liquidity.rs index cbc19954f..a7751026b 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -11,28 +11,39 @@ use crate::chain::ChainSource; 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 crate::{total_anchor_channels_reserve_sats, Config, Error}; -use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; +use lightning::events::HTLCDestination; +use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; +use lightning::ln::types::ChannelId; 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::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; 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; +use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; +use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::msgs::{OpeningFeeParams, RawOpeningFeeParams}; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; -use lightning_liquidity::LiquidityClientConfig; +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; + +use lightning_types::payment::PaymentHash; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; +use chrono::{DateTime, Utc}; + +use rand::Rng; + use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Mutex, RwLock}; @@ -40,11 +51,15 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; -struct LSPS1Service { - node_id: PublicKey, - address: SocketAddress, +const LSPS2_GETINFO_REQUEST_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); +const LSPS2_CLIENT_TRUSTS_LSP_MODE: bool = true; +const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; + +struct LSPS1Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, - client_config: LSPS1ClientConfig, + ldk_client_config: LdkLSPS1ClientConfig, pending_opening_params_requests: Mutex>>, pending_create_order_requests: Mutex>>, @@ -52,21 +67,80 @@ struct LSPS1Service { Mutex>>, } -struct LSPS2Service { - node_id: PublicKey, - address: SocketAddress, +#[derive(Debug, Clone)] +pub(crate) struct LSPS1ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + +struct LSPS2Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, - client_config: LSPS2ClientConfig, + ldk_client_config: LdkLSPS2ClientConfig, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, } +#[derive(Debug, Clone)] +pub(crate) struct LSPS2ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + +struct LSPS2Service { + service_config: LSPS2ServiceConfig, + ldk_service_config: LdkLSPS2ServiceConfig, +} + +/// Represents the configuration of the LSPS2 service. +/// +/// See [bLIP-52 / LSPS2] for more information. +/// +/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md +#[derive(Debug, Clone)] +pub struct LSPS2ServiceConfig { + /// A token we may require to be sent by the clients. + /// + /// If set, only requests matching this token will be accepted. + pub require_token: Option, + /// Indicates whether the LSPS service will be announced via the gossip network. + pub advertise_service: bool, + /// The fee we withhold for the channel open from the initial payment. + /// + /// This fee is proportional to the client-requested amount, in parts-per-million. + pub channel_opening_fee_ppm: u32, + /// The proportional overprovisioning for the channel. + /// + /// This determines, in parts-per-million, how much value we'll provision on top of the amount + /// we need to forward the payment to the client. + /// + /// For example, setting this to `100_000` will result in a channel being opened that is 10% + /// larger than then the to-be-forwarded amount (i.e., client-requested amount minus the + /// channel opening fee fee). + pub channel_over_provisioning_ppm: u32, + /// The minimum fee required for opening a channel. + pub min_channel_opening_fee_msat: u64, + /// The minimum number of blocks after confirmation we promise to keep the channel open. + pub min_channel_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The minimum payment size that we will accept when opening a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size that we will accept when opening a channel. + pub max_payment_size_msat: u64, +} + pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, { - lsps1_service: Option, + lsps1_client: Option, + lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -79,14 +153,17 @@ where L::Target: LdkLogger, { pub(crate) fn new( - channel_manager: Arc, keys_manager: Arc, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, config: Arc, logger: L, ) -> Self { - let lsps1_service = None; + let lsps1_client = None; + let lsps2_client = None; let lsps2_service = None; Self { - lsps1_service, + lsps1_client, + lsps2_client, lsps2_service, + wallet, channel_manager, keys_manager, chain_source, @@ -95,19 +172,19 @@ where } } - pub(crate) fn lsps1_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps1_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { // TODO: allow to set max_channel_fees_msat - let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let ldk_client_config = LdkLSPS1ClientConfig { max_channel_fees_msat: None }; 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, + self.lsps1_client = Some(LSPS1Client { + lsp_node_id, + lsp_address, token, - client_config, + ldk_client_config, pending_opening_params_requests, pending_create_order_requests, pending_check_order_status_requests, @@ -115,26 +192,40 @@ where self } - pub(crate) fn lsps2_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps2_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { - let client_config = LSPS2ClientConfig {}; + let ldk_client_config = LdkLSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); - self.lsps2_service = Some(LSPS2Service { - node_id, - address, + self.lsps2_client = Some(LSPS2Client { + lsp_node_id, + lsp_address, token, - client_config, + ldk_client_config, pending_fee_requests, pending_buy_requests, }); self } + pub(crate) fn lsps2_service( + &mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let ldk_service_config = LdkLSPS2ServiceConfig { promise_secret }; + self.lsps2_service = Some(LSPS2Service { service_config, ldk_service_config }); + 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_service_config = self.lsps2_service.as_ref().map(|s| { + let lsps2_service_config = Some(s.ldk_service_config.clone()); + let advertise_service = s.service_config.advertise_service; + LiquidityServiceConfig { lsps2_service_config, advertise_service } + }); + + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone()); + let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.ldk_client_config.clone()); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); @@ -143,14 +234,17 @@ where Arc::clone(&self.channel_manager), Some(Arc::clone(&self.chain_source)), None, - None, + liquidity_service_config, liquidity_client_config, )); LiquiditySource { - lsps1_service: self.lsps1_service, + lsps1_client: self.lsps1_client, + lsps2_client: self.lsps2_client, lsps2_service: self.lsps2_service, + wallet: self.wallet, channel_manager: self.channel_manager, + peer_manager: RwLock::new(None), keys_manager: self.keys_manager, liquidity_manager, config: self.config, @@ -163,9 +257,12 @@ pub(crate) struct LiquiditySource where L::Target: LdkLogger, { - lsps1_service: Option, + lsps1_client: Option, + lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, + peer_manager: RwLock>>, keys_manager: Arc, liquidity_manager: Arc, config: Arc, @@ -177,6 +274,7 @@ where L::Target: LdkLogger, { pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { + *self.peer_manager.write().unwrap() = Some(Arc::clone(&peer_manager)); let process_msgs_callback = move || peer_manager.process_events(); self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); } @@ -185,12 +283,12 @@ where self.liquidity_manager.as_ref() } - 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_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps1_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_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())) + pub(crate) fn get_lsps2_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } pub(crate) async fn handle_next_event(&self) { @@ -200,8 +298,8 @@ where counterparty_node_id, supported_options, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -213,7 +311,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_opening_params_requests .lock() .unwrap() @@ -256,8 +354,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -269,7 +367,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_create_order_requests .lock() .unwrap() @@ -314,8 +412,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -327,7 +425,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_check_order_status_requests .lock() .unwrap() @@ -364,13 +462,261 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); } }, + Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + if let Some(required) = service_config.require_token { + if token != Some(required) { + log_error!( + self.logger, + "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", + request_id, + counterparty_node_id + ); + lsps2_service_handler.invalid_token_provided(&counterparty_node_id, request_id.clone()).unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS2 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS2 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + return; + } + } + + let mut valid_until: DateTime = Utc::now(); + valid_until += LSPS2_GETINFO_REQUEST_EXPIRY; + + let opening_fee_params = RawOpeningFeeParams { + min_fee_msat: service_config.min_channel_opening_fee_msat, + proportional: service_config.channel_opening_fee_ppm, + valid_until, + min_lifetime: service_config.min_channel_lifetime, + max_client_to_self_delay: service_config.max_client_to_self_delay, + min_payment_size_msat: service_config.min_payment_size_msat, + max_payment_size_msat: service_config.max_payment_size_msat, + }; + + let opening_fee_params_menu = vec![opening_fee_params]; + + if let Err(e) = lsps2_service_handler.opening_fee_params_generated( + &counterparty_node_id, + request_id, + opening_fee_params_menu, + ) { + log_error!( + self.logger, + "Failed to handle generated opening fee params: {:?}", + e + ); + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: _, + payment_size_msat, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let user_channel_id: u128 = rand::thread_rng().gen::(); + let intercept_scid = self.channel_manager.get_intercept_scid(); + + if let Some(payment_size_msat) = payment_size_msat { + // We already check this in `lightning-liquidity`, but better safe than + // sorry. + // + // TODO: We might want to eventually send back an error here, but we + // currently can't and have to trust `lightning-liquidity` is doing the + // right thing. + // + // TODO: Eventually we also might want to make sure that we have sufficient + // liquidity for the channel opening here. + if payment_size_msat > service_config.max_payment_size_msat + || payment_size_msat < service_config.min_payment_size_msat + { + log_error!( + self.logger, + "Rejecting to handle LSPS2 buy request {:?} from counterparty {} as the client requested an invalid payment size.", + request_id, + counterparty_node_id + ); + return; + } + } + + match lsps2_service_handler.invoice_parameters_generated( + &counterparty_node_id, + request_id, + intercept_scid, + LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, + LSPS2_CLIENT_TRUSTS_LSP_MODE, + user_channel_id, + ) { + Ok(()) => {}, + Err(e) => { + log_error!( + self.logger, + "Failed to provide invoice parameters: {:?}", + e + ); + return; + }, + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + their_network_key, + amt_to_forward_msat, + opening_fee_msat: _, + user_channel_id, + intercept_scid: _, + }) => { + if self.liquidity_manager.lsps2_service_handler().is_none() { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let init_features = if let Some(peer_manager) = + self.peer_manager.read().unwrap().as_ref() + { + // Fail if we're not connected to the prospective channel partner. + if let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) { + peer.init_features + } else { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {} due to peer not being not connected.", + their_network_key, + ); + return; + } + } else { + debug_assert!(false, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + return; + }; + + // Fail if we have insufficient onchain funds available. + let over_provisioning_msat = (amt_to_forward_msat + * service_config.channel_over_provisioning_ppm as u64) + / 1_000_000; + let channel_amount_sats = (amt_to_forward_msat + over_provisioning_msat) / 1000; + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let required_funds_sats = channel_amount_sats + + self.config.anchor_channels_config.as_ref().map_or(0, |c| { + if init_features.requires_anchors_zero_fee_htlc_tx() + && !c.trusted_peers_no_reserve.contains(&their_network_key) + { + c.per_channel_reserve_sats + } else { + 0 + } + }); + if spendable_amount_sats < required_funds_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, channel_amount_sats + ); + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + return; + } + + let mut config = *self.channel_manager.get_current_default_configuration(); + + // Set the HTLC-value-in-flight to 100% of the channel value to ensure we can + // forward the payment. + config + .channel_handshake_config + .max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + // + // TODO: revisit this decision eventually. + config.channel_config.forwarding_fee_base_msat = 0; + config.channel_config.forwarding_fee_proportional_millionths = 0; + + match self.channel_manager.create_channel( + their_network_key, + channel_amount_sats, + 0, + user_channel_id, + None, + Some(config), + ) { + Ok(_) => {}, + Err(e) => { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {}: {:?}", + their_network_key, + e + ); + return; + }, + } + }, Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, opening_fee_params_menu, }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -383,7 +729,7 @@ where } if let Some(sender) = - lsps2_service.pending_fee_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_fee_requests.lock().unwrap().remove(&request_id) { let response = LSPS2FeeResponse { opening_fee_params_menu }; @@ -421,8 +767,8 @@ where cltv_expiry_delta, .. }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -435,7 +781,7 @@ where } if let Some(sender) = - lsps2_service.pending_buy_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_buy_requests.lock().unwrap().remove(&request_id) { let response = LSPS2BuyResponse { intercept_scid, cltv_expiry_delta }; @@ -475,7 +821,7 @@ where pub(crate) async fn lsps1_request_opening_params( &self, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.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.",); @@ -485,8 +831,8 @@ where 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); + lsps1_client.pending_opening_params_requests.lock().unwrap(); + let request_id = client_handler.request_supported_options(lsps1_client.lsp_node_id); pending_opening_params_requests_lock.insert(request_id, request_sender); } @@ -506,7 +852,7 @@ where &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 lsps1_client = self.lsps1_client.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 @@ -560,7 +906,7 @@ where 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(), + token: lsps1_client.token.clone(), announce_channel, }; @@ -568,9 +914,9 @@ where let request_id; { let mut pending_create_order_requests_lock = - lsps1_service.pending_create_order_requests.lock().unwrap(); + lsps1_client.pending_create_order_requests.lock().unwrap(); request_id = client_handler.create_order( - &lsps1_service.node_id, + &lsps1_client.lsp_node_id, order_params.clone(), Some(refund_address), ); @@ -605,7 +951,7 @@ where 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 lsps1_client = self.lsps1_client.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 @@ -614,8 +960,8 @@ where 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); + lsps1_client.pending_check_order_status_requests.lock().unwrap(); + let request_id = client_handler.check_order_status(&lsps1_client.lsp_node_id, order_id); pending_check_order_status_requests_lock.insert(request_id, request_sender); } @@ -740,7 +1086,7 @@ where } async fn lsps2_request_opening_fee_params(&self) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -749,9 +1095,9 @@ where let (fee_request_sender, fee_request_receiver) = oneshot::channel(); { - let mut pending_fee_requests_lock = lsps2_service.pending_fee_requests.lock().unwrap(); + let mut pending_fee_requests_lock = lsps2_client.pending_fee_requests.lock().unwrap(); let request_id = client_handler - .request_opening_params(lsps2_service.node_id, lsps2_service.token.clone()); + .request_opening_params(lsps2_client.lsp_node_id, lsps2_client.token.clone()); pending_fee_requests_lock.insert(request_id, fee_request_sender); } @@ -773,7 +1119,7 @@ where async fn lsps2_send_buy_request( &self, amount_msat: Option, opening_fee_params: OpeningFeeParams, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -782,9 +1128,9 @@ where let (buy_request_sender, buy_request_receiver) = oneshot::channel(); { - let mut pending_buy_requests_lock = lsps2_service.pending_buy_requests.lock().unwrap(); + let mut pending_buy_requests_lock = lsps2_client.pending_buy_requests.lock().unwrap(); let request_id = client_handler - .select_opening_params(lsps2_service.node_id, amount_msat, opening_fee_params) + .select_opening_params(lsps2_client.lsp_node_id, amount_msat, opening_fee_params) .map_err(|e| { log_error!( self.logger, @@ -817,7 +1163,7 @@ where &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; @@ -830,7 +1176,7 @@ where })?; let route_hint = RouteHint(vec![RouteHintHop { - src_node_id: lsps2_service.node_id, + 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, @@ -867,6 +1213,70 @@ where Error::InvoiceCreationFailed }) } + + pub(crate) fn handle_channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.channel_ready( + user_channel_id, + channel_id, + counterparty_node_id, + ) { + log_error!( + self.logger, + "LSPS2 service failed to handle ChannelReady event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_intercepted( + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCIntercepted event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_htlc_handling_failed(&self, failed_next_destination: HTLCDestination) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failed_next_destination) { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_payment_forwarded(&self, next_channel_id: Option) { + if let Some(next_channel_id) = next_channel_id { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.payment_forwarded(next_channel_id) { + log_error!( + self.logger, + "LSPS2 service failed to handle PaymentForwarded: {:?}", + e + ); + } + } + } + } } #[derive(Debug, Clone)] @@ -999,9 +1409,8 @@ impl LSPS1Liquidity { 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 (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); @@ -1045,9 +1454,8 @@ impl LSPS1Liquidity { 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 (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 2c1b19143..d00e14c7f 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -596,9 +596,8 @@ impl Bolt11Payment { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (node_id, address) = liquidity_source - .get_lsps2_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index c7a8960f7..58c577f8e 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -14,7 +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::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; pub use crate::logger::{LogLevel, LogRecord, LogWriter}; pub use crate::payment::store::{ ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 2dc74cea9..29ed48ec0 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -8,13 +8,14 @@ mod common; use common::{ - do_channel_full_cycle, expect_channel_ready_event, expect_event, expect_payment_received_event, - expect_payment_successful_event, generate_blocks_and_wait, open_channel, - premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, + expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + open_channel, premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, + setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; use ldk_node::config::EsploraSyncConfig; +use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, SendingParameters, @@ -990,3 +991,108 @@ fn unified_qr_send_receive() { assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } + +#[test] +fn lsps2_client_service_integration() { + 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.onchain_wallet_sync_interval_secs = 100000; + sync_config.lightning_wallet_sync_interval_secs = 100000; + + // Setup three nodes: service, client, and payer + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + 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, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_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().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); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build().unwrap(); + payer_node.start().unwrap(); + + let service_addr = service_node.onchain_payment().new_address().unwrap(); + let client_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_addr = payer_node.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 10_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr, client_addr, payer_addr], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + // Open a channel payer -> service that will allow paying the JIT invoice + println!("Opening channel payer_node -> service_node!"); + open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + 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 invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_amount_msat = 100_000_000; + + println!("Generating JIT invoice!"); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, 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 * channel_opening_fee_ppm as u64) / 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); + + let expected_channel_overprovisioning_msat = + (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; + let expected_channel_size_sat = + (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; + let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; + assert_eq!(channel_value_sats, expected_channel_size_sat); +}