diff --git a/Cargo.toml b/Cargo.toml index f22092cb5..6dcd38a61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,8 @@ lightning-background-processor = { version = "0.0.125", features = ["futures"] } lightning-rapid-gossip-sync = { version = "0.0.125" } lightning-block-sync = { version = "0.0.125", features = ["rpc-client", "tokio"] } lightning-transaction-sync = { version = "0.0.125", features = ["esplora-async-https", "time"] } -lightning-liquidity = { version = "0.1.0-alpha.6", features = ["std"] } +#lightning-liquidity = { version = "0.1.0-alpha.6", features = ["std"] } +lightning-liquidity = { git = "https://github.com/tnull/lightning-liquidity", branch="2024-07-update-lsps1", features = ["std"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] } #lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index bc99c1783..fc5a91796 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -39,7 +39,8 @@ 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_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_network(Network network); [Throws=BuildError] @@ -78,6 +79,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] @@ -173,6 +175,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", @@ -220,6 +229,8 @@ enum NodeError { "InvalidUri", "InvalidQuantity", "InvalidNodeAlias", + "InvalidDateTime", + "InvalidFeeRate", "DuplicatePayment", "UnsupportedCurrency", "InsufficientFunds", @@ -370,6 +381,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 (); @@ -622,3 +686,12 @@ typedef string UntrustedString; [Custom] typedef string NodeAlias; + +[Custom] +typedef string OrderId; + +[Custom] +typedef string DateTime; + +[Custom] +typedef string FeeRate; diff --git a/src/builder.rs b/src/builder.rs index fac2ae0c5..9944e6e2a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -15,7 +15,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, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -51,9 +51,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; @@ -75,6 +72,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,16 +95,34 @@ enum GossipSourceConfig { RapidGossipSync(String), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] struct LiquiditySourceConfig { - // LSPS2 service's (address, node_id, token) - lsps2_service: Option<(SocketAddress, PublicKey, Option)>, + // 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, } -impl Default for LiquiditySourceConfig { - fn default() -> Self { - Self { lsps2_service: None } - } +#[derive(Debug, Clone)] +struct LSPS1ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, +} + +#[derive(Debug, Clone)] +struct LSPS2ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, +} + +#[derive(Debug, Clone)] +struct LSPS2ServiceConfig { + token: Option, + advertise_service: bool, } /// An error encountered during building a [`Node`]. @@ -273,7 +292,27 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to source its inbound liquidity from the given + /// Configures the [`Node`] instance to source inbound liquidity from the given + /// [LSPS1](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS1/README.md) + /// 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. + 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()); + let lsps1_client_config = LSPS1ClientConfig { node_id, address, token }; + liquidity_source_config.lsps1_client = Some(lsps1_client_config); + self + } + + /// Configures the [`Node`] instance to source just-in-time inbound liquidity from the given /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. /// @@ -281,14 +320,35 @@ 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)); + 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. + /// + /// If a `token` is provided, only requests matching this token will be accepted. + /// + /// If `advertise_service` is set, the LSPS service will be announced via the gossip network. + /// + /// **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, token: Option, advertise_service: bool, + ) -> &mut Self { + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + let lsps2_service_config = LSPS2ServiceConfig { token, advertise_service }; + liquidity_source_config.lsps2_service = Some(lsps2_service_config); self } @@ -401,10 +461,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 @@ -466,7 +530,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(); @@ -592,7 +661,20 @@ 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 + /// Configures the [`Node`] instance to source inbound liquidity from the given + /// [LSPS1](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS1/README.md) + /// 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. + 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 /// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md) /// service. /// @@ -600,9 +682,23 @@ 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); + } + + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// If a `token` is provided, only requests matching this token will be accepted. + /// + /// If `advertise_service` is set, the LSPS service will be announced via the gossip network. + /// + /// **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, token: Option, advertise_service: bool) { + self.inner.write().unwrap().set_liquidity_provider_lsps2(token, advertise_service); } /// Sets the used storage directory path. @@ -924,7 +1020,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; @@ -1052,36 +1148,56 @@ 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)| { - let lsps2_client_config = Some(LSPS2ClientConfig {}); - let liquidity_client_config = Some(LiquidityClientConfig { 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( - address.clone(), - *node_id, - 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(&channel_manager), Arc::clone(&keys_manager), - liquidity_manager, + Arc::clone(&chain_source), Arc::clone(&config), Arc::clone(&logger), - )) - }) - }); + ); - 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()) - }; + 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(), + ) + }); + + 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.token.clone(), + config.advertise_service, + ) + }); + + 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 { @@ -1268,8 +1384,9 @@ 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; @@ -1278,10 +1395,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/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/event.rs b/src/event.rs index 5f5812cdb..fd28aea65 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,6 +14,7 @@ use crate::{ use crate::connection::ConnectionManager; use crate::fee_estimator::ConfirmationTarget; +use crate::liquidity::LiquiditySource; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, @@ -24,7 +25,7 @@ use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, }; -use crate::logger::{log_debug, log_error, log_info, Logger}; +use crate::logger::{log_debug, log_error, log_info, FilesystemLogger, Logger}; use lightning::events::bump_transaction::BumpTransactionEvent; use lightning::events::{ClosureReason, PaymentPurpose, ReplayEvent}; @@ -432,6 +433,7 @@ where connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, @@ -448,6 +450,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 { @@ -459,6 +462,7 @@ where connection_manager, output_sweeper, network_graph, + liquidity_source, payment_store, peer_store, logger, @@ -994,7 +998,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; @@ -1144,21 +1152,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), - 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(); @@ -1191,14 +1184,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 { @@ -1207,10 +1199,29 @@ 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), + 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, @@ -1284,6 +1295,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), @@ -1322,7 +1341,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 363812292..c6f07dede 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; 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 vss_client; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; @@ -129,7 +130,7 @@ use event::{EventHandler, EventQueue}; use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; -use liquidity::LiquiditySource; +use liquidity::{LiquiditySource, Lsps1Liquidity}; use payment::store::PaymentStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, @@ -533,6 +534,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), @@ -960,6 +962,34 @@ impl Node { )) } + /// Returns a liquidity handler allowing to request channels via the [LSPS1] protocol. + /// + /// [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 + #[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 [LSPS1] protocol. + /// + /// [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 + #[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 1dfb5453a..85df83747 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -5,73 +5,212 @@ // 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::logger::{log_debug, log_error, log_info, Logger}; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager}; +//! Objects related to liquidity management. + +use crate::chain::ChainSource; +use crate::connection::ConnectionManager; +use crate::logger::{log_debug, log_error, log_info, FilesystemLogger, Logger}; +use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{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::ln::PaymentHash; use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; -use lightning_liquidity::lsps2::event::LSPS2ClientEvent; -use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +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, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::msgs::{OpeningFeeParams, RawOpeningFeeParams}; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; +use chrono::{DateTime, Utc}; + 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; -struct LSPS2Service { - address: SocketAddress, - node_id: PublicKey, +const LSPS1_DEFAULT_REQUIRED_CHANNEL_CONF: u16 = 0; + +struct LSPS1Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, + token: Option, + client_config: LSPS1ClientConfig, + pending_opening_params_requests: + Mutex>>, + pending_create_order_requests: Mutex>>, + pending_check_order_status_requests: + Mutex>>, +} + +struct LSPS2Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, + client_config: LSPS2ClientConfig, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, } -pub(crate) struct LiquiditySource +struct LSPS2Service { + token: Option, + service_config: LSPS2ServiceConfig, + advertise_service: bool, +} + +pub(crate) struct LiquiditySourceBuilder where L::Target: Logger, { + lsps1_client: Option, + lsps2_client: 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: Logger, { - pub(crate) fn new_lsps2( - address: SocketAddress, node_id: PublicKey, 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_client = None; + let lsps2_client = None; + let lsps2_service = None; + Self { + lsps1_client, + lsps2_client, + lsps2_service, + channel_manager, + keys_manager, + chain_source, + config, + logger, + } + } + + 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 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_client = Some(LSPS1Client { + lsp_node_id, + lsp_address, + token, + client_config, + pending_opening_params_requests, + pending_create_order_requests, + pending_check_order_status_requests, + }); + self + } + + pub(crate) fn lsps2_client( + &mut self, lsp_node_id: PublicKey, lsp_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 { - address, - node_id, + self.lsps2_client = Some(LSPS2Client { + lsp_node_id, + lsp_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 lsps2_service( + &mut self, promise_secret: [u8; 32], token: Option, advertise_service: bool, + ) -> &mut Self { + let service_config = LSPS2ServiceConfig { promise_secret }; + self.lsps2_service = Some(LSPS2Service { token, service_config, advertise_service }); + self + } + + pub(crate) fn build(self) -> LiquiditySource { + let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { + let lsps2_service_config = Some(s.service_config.clone()); + let advertise_service = s.advertise_service; + LiquidityServiceConfig { lsps2_service_config, advertise_service } + }); + + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.client_config.clone()); + let lsps2_client_config = self.lsps2_client.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, + liquidity_service_config, + liquidity_client_config, + )); + + LiquiditySource { + lsps1_client: self.lsps1_client, + lsps2_client: self.lsps2_client, + 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: Logger, +{ + lsps1_client: Option, + lsps2_client: Option, + lsps2_service: Option, + channel_manager: Arc, + keys_manager: Arc, + liquidity_manager: Arc, + config: Arc, + logger: L, +} +impl LiquiditySource +where + L::Target: Logger, +{ 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,19 +220,297 @@ where self.liquidity_manager.as_ref() } - pub(crate) fn get_liquidity_source_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps2_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_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) { match self.liquidity_manager.next_event_async().await { + Event::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + request_id, + counterparty_node_id, + supported_options, + }) => { + 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." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_client + .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_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." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_client + .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_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." + ); + log_error!( + self.logger, + "Received response from unexpected LSP counterparty. This should never happen." + ); + return; + } + + if let Some(sender) = lsps1_client + .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::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token: _, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let min_fee_msat = 0; + let proportional = 0; + let mut valid_until: DateTime = Utc::now(); + valid_until += chrono::Duration::minutes(10); + let min_lifetime = 1008; + let max_client_to_self_delay = 144; + let min_payment_size_msat = 1000; + let max_payment_size_msat = 10_000_000_000; + + let opening_fee_params = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until, + min_lifetime, + max_client_to_self_delay, + min_payment_size_msat, + 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 user_channel_id = 0; + let scid = self.channel_manager.get_intercept_scid(); + let cltv_expiry_delta = 72; + let client_trusts_lsp = true; + + if let Err(e) = lsps2_service_handler.invoice_parameters_generated( + &counterparty_node_id, + request_id, + scid, + cltv_expiry_delta, + client_trusts_lsp, + user_channel_id, + ) { + log_error!(self.logger, "Failed to provide invoice parameters: {:?}", e); + } + } 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 channel_size_sats = (amt_to_forward_msat / 1000) * 4; + let mut config = *self.channel_manager.get_current_default_configuration(); + config + .channel_handshake_config + .max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_config.forwarding_fee_base_msat = 0; + config.channel_config.forwarding_fee_proportional_millionths = 0; + + if let Err(e) = self.channel_manager.create_channel( + their_network_key, + channel_size_sats, + 0, + user_channel_id, + None, + Some(config), + ) { + log_error!(self.logger, "Failed to open jit channel: {:?}", e); + } + }, 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." @@ -106,7 +523,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 }; @@ -144,8 +561,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." @@ -158,7 +575,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 }; @@ -195,6 +612,166 @@ where } } + pub(crate) async fn lsps1_request_opening_params( + &self, + ) -> Result { + 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 + })?; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_opening_params_requests_lock = + 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); + } + + 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_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 + })?; + + 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: LSPS1_DEFAULT_REQUIRED_CHANNEL_CONF, + funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks, + channel_expiry_blocks, + token: lsps1_client.token.clone(), + announce_channel, + }; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_create_order_requests_lock = + lsps1_client.pending_create_order_requests.lock().unwrap(); + let request_id = client_handler.create_order( + &lsps1_client.lsp_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." + ); + return Err(Error::LiquidityRequestFailed); + } + + Ok(response) + } + + pub(crate) async fn lsps1_check_order_status( + &self, order_id: OrderId, + ) -> Result { + 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 + })?; + + let (request_sender, request_receiver) = oneshot::channel(); + { + let mut pending_check_order_status_requests_lock = + 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); + } + + 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: &str, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, @@ -299,7 +876,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.",); @@ -308,9 +885,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); } @@ -332,7 +909,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.",); @@ -341,9 +918,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, @@ -376,7 +953,7 @@ where &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &str, 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; @@ -389,7 +966,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, @@ -426,6 +1003,72 @@ 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, "Errored processing 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, "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, "Errored processing 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, "Failed to handle PaymentForwarded: {:?}", e); + } + } + } + } +} + +#[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)] @@ -438,3 +1081,112 @@ pub(crate) struct LSPS2BuyResponse { intercept_scid: u64, cltv_expiry_delta: u32, } + +/// A liquidity handler allowing to request channels via the [LSPS1] protocol. +/// +/// Should be retrieved by calling [`Node::lsps1_liquidity`]. +/// +/// [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 +/// [`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_lsp_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_lsp_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) + } +} diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 708c127bd..899563dbd 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -601,9 +601,8 @@ impl Bolt11Payment { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (node_id, address) = liquidity_source - .get_liquidity_source_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 9cb88597d..744411458 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -14,9 +14,9 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; +pub use crate::liquidity::LSPS1OrderStatus; pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus}; pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; -pub use crate::types::CustomTlvRecord; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; @@ -29,12 +29,19 @@ pub use lightning::util::string::UntrustedString; pub use lightning_invoice::Bolt11Invoice; -pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid}; +pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; +pub use lightning_liquidity::lsps1::msgs::{ + Bolt11PaymentInfo, OnchainPaymentInfo, OrderId, OrderParameters, PaymentInfo, 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; @@ -344,3 +351,40 @@ impl UniffiCustomTypeConverter for NodeAlias { obj.to_string() } } + +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() + } +} + +/// FIXME TODO +impl UniffiCustomTypeConverter for FeeRate { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(FeeRate::from_str(&val).map_err(|_| Error::InvalidFeeRate)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +}