From 1b93d7bc74b4e6976d46a1a68c6378911c1d737f Mon Sep 17 00:00:00 2001 From: Anyitechs Date: Mon, 17 Aug 2026 18:04:07 +0100 Subject: [PATCH] Add LSPS1 server side support --- src/builder.rs | 37 ++- src/event.rs | 171 ++++++++++++ src/liquidity/mod.rs | 55 +++- src/liquidity/service/lsps1.rs | 458 ++++++++++++++++++++++++++++++++ src/liquidity/service/mod.rs | 1 + tests/integration_tests_rust.rs | 10 +- 6 files changed, 708 insertions(+), 24 deletions(-) create mode 100644 src/liquidity/service/lsps1.rs diff --git a/src/builder.rs b/src/builder.rs index f0f38783fb..4f705fd5d2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -71,6 +71,7 @@ use crate::io::{ PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; +use crate::liquidity::service::lsps1::LSPS1Service; use crate::liquidity::{LSPS2ServiceConfig, LiquiditySourceBuilder, LspConfig}; use crate::lnurl_auth::LnurlAuth; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; @@ -131,6 +132,8 @@ struct PathfindingScoresSyncConfig { struct LiquiditySourceConfig { // Acts for both LSPS1 and LSPS2 clients connecting to the given service. lsp_nodes: Vec, + // Act as an LSPS1 service. + lsps1_service: Option, // Act as an LSPS2 service. lsps2_service: Option, } @@ -509,18 +512,22 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time - /// channels to clients. + /// Configures the [`Node`] instance to provide an [LSPS1] and/or [LSPS2] service to clients. + /// + /// Allowing normal channel purchases and/or just-in-time channels respectively. /// /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. /// + /// [LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md pub fn enable_liquidity_provider( - &mut self, lsps2_service_config: LSPS2ServiceConfig, + &mut self, lsps1_service_config: Option, + lsps2_service_config: Option, ) -> &mut Self { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some(lsps2_service_config); + liquidity_source_config.lsps1_service = lsps1_service_config; + liquidity_source_config.lsps2_service = lsps2_service_config; self } @@ -1114,14 +1121,22 @@ impl ArcedNodeBuilder { ); } - /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time - /// channels to clients. + /// Configures the [`Node`] instance to provide an [LSPS1] and/or [LSPS2] service to clients. + /// + /// Allowing normal channel purchases and/or just-in-time channels respectively. /// /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. /// + /// [LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - pub fn enable_liquidity_provider(&self, lsps2_service_config: LSPS2ServiceConfig) { - self.inner.write().expect("lock").enable_liquidity_provider(lsps2_service_config); + pub fn enable_liquidity_provider( + &self, lsps1_service_config: Option, + lsps2_service_config: Option, + ) { + self.inner + .write() + .expect("lock") + .enable_liquidity_provider(lsps1_service_config, lsps2_service_config); } /// Sets the used storage directory path. @@ -2177,6 +2192,10 @@ fn build_with_store_internal( lsc.lsps2_service.as_ref().map(|config| { liquidity_source_builder.lsps2_service(promise_secret, config.clone()) }); + + lsc.lsps1_service + .as_ref() + .map(|config| liquidity_source_builder.lsps1_service(*config)); } let liquidity_source = runtime @@ -2236,6 +2255,8 @@ fn build_with_store_internal( liquidity_source.lsps2_service().set_peer_manager(Arc::downgrade(&peer_manager)); + liquidity_source.lsps1_service().set_peer_manager(Arc::downgrade(&peer_manager)); + let connection_manager = Arc::new(ConnectionManager::new( Arc::clone(&peer_manager), config.tor_config.clone(), diff --git a/src/event.rs b/src/event.rs index 0a35697552..b23b892122 100644 --- a/src/event.rs +++ b/src/event.rs @@ -44,6 +44,7 @@ use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, }; +use crate::liquidity::service::lsps1::{PendingLSPS1Channel, PendingLSPS1Order}; use crate::liquidity::LiquiditySource; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; @@ -834,6 +835,124 @@ where counterparty_skimmed_fee_msat, .. } => { + // We intercept early and check if the payment was an LSPS1 + // order payment and handle properly. + if let Ok(bytes) = self.event_queue.kv_store.read( + "lsps1_pending_orders", + "", + &payment_hash.0.to_string(), + ) { + if let Ok(pending_order) = PendingLSPS1Order::read(&mut &bytes[..]) { + let (payment_preimage, payment_method) = match purpose { + PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. } => ( + payment_preimage, + lightning_liquidity::lsps1::service::PaymentMethod::Bolt11, + ), + PaymentPurpose::Bolt12OfferPayment { payment_preimage, .. } => ( + payment_preimage, + lightning_liquidity::lsps1::service::PaymentMethod::Bolt12, + ), + _ => (None, lightning_liquidity::lsps1::service::PaymentMethod::Bolt11), + }; + + if let Some(preimage) = payment_preimage { + let expected_msat = + pending_order.order_total_amount_sat.saturating_mul(1000); + + if amount_msat < expected_msat { + log_error!( + self.logger, + "Refused LSPS1 payment: underpaid. Expected {} msat, received {} msat.", + expected_msat, + amount_msat + ); + self.channel_manager.fail_htlc_backwards(&payment_hash); + return Ok(()); + } + + self.runtime.block_on(async { + self.liquidity_source + .lsps1_service() + .handle_order_payment_received( + pending_order.counterparty_node_id, + pending_order.request_id.into(), + payment_method, + ) + .await + }); + + self.channel_manager.claim_funds(preimage); + + let mut config = self.channel_manager.get_current_config(); + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + config.channel_config.forwarding_fee_base_msat = 0; + + let channel_size_sat = pending_order.order_params.lsp_balance_sat + + pending_order.order_params.client_balance_sat; + + let push_msat = + pending_order.order_params.client_balance_sat.saturating_mul(1000); + + let user_channel_id: u128 = u128::from_ne_bytes( + self.keys_manager.get_secure_random_bytes()[..16] + .try_into() + .expect("slice is exactly 16 bytes"), + ); + + let pending_channel = PendingLSPS1Channel { + order_id: pending_order.request_id.into().clone(), + channel_expiry_blocks: pending_order + .order_params + .channel_expiry_blocks, + }; + + let _ = self.event_queue.kv_store.write( + "lsps1_pending_channels", + "", + &user_channel_id.to_string(), + pending_channel.encode(), + ); + + if let Err(e) = self.channel_manager.create_channel( + pending_order.counterparty_node_id, + channel_size_sat, + push_msat, + user_channel_id, + None, + Some(config), + ) { + log_error!( + self.logger, + "Failed to open LSPS1 channel after claiming funds: {:?}", + e + ); + self.liquidity_source + .lsps1_service() + .handle_order_failed_and_refunded( + pending_order.counterparty_node_id, + pending_order.request_id.into(), + ) + .await + } + + let _ = self.event_queue.kv_store.remove( + "lsps1_pending_orders", + "", + &payment_hash.0.to_string(), + false, + ); + } else { + log_error!( + self.logger, + "Failed to claim LSPS1 payment: preimage unknown or unsupported payment purpose." + ); + self.channel_manager.fail_htlc_backwards(&payment_hash); + } + return Ok(()); + } + } + let (payment_id, mut payment_info) = self.resolve_inbound_payment_id(payment_id, &payment_hash).await?; if let Some(info) = payment_info.as_ref() { @@ -1779,6 +1898,58 @@ where counterparty_node_id, ); + // We check if this event was triggered by an LSPS1 order and handle it properly + if let Ok(bytes) = self.event_queue.kv_store.read( + "lsps1_pending_channels", + "", + &user_channel_id.to_string(), + ) { + if let Ok(pending_channel) = PendingLSPS1Channel::read(&mut &bytes[..]) { + let now_secs = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + let funded_at = + lightning_liquidity::lsps0::ser::LSPSDateTime::from_unix_timestamp( + now_secs, + ) + .expect("Valid timestamp"); + + let expiry_secs = + now_secs + (pending_channel.channel_expiry_blocks as u64 * 600); + let expires_at = + lightning_liquidity::lsps0::ser::LSPSDateTime::from_unix_timestamp( + expiry_secs, + ) + .expect("Valid timestamp"); + + let channel_info = LSPS1ChannelInfo { + funded_at, + funding_outpoint: funding_txo.into_bitcoin_outpoint(), + expires_at, + }; + + self.runtime.block_on(async { + self.liquidity_source + .lsps1_service() + .handle_order_channel_opened( + counterparty_node_id, + pending_channel.order_id, + channel_info, + ) + .await + }); + + let _ = self.event_queue.kv_store.remove( + "lsps1_pending_channels", + "", + &user_channel_id.to_string(), + false, + ); + } + } + let former_temporary_channel_id = former_temporary_channel_id.expect( "LDK Node has only ever persisted ChannelPending events from rust-lightning 0.0.115 or later", ); diff --git a/src/liquidity/mod.rs b/src/liquidity/mod.rs index ffc1f878bc..7af5471ddd 100644 --- a/src/liquidity/mod.rs +++ b/src/liquidity/mod.rs @@ -33,6 +33,7 @@ use crate::builder::BuildError; use crate::connection::ConnectionManager; use crate::liquidity::client::lsps1::LSPS1Client; use crate::liquidity::client::lsps2::LSPS2Client; +use crate::liquidity::service::lsps1::{LSPS1Service, LSPS1ServiceLiquiditySource}; use crate::liquidity::service::lsps2::{LSPS2Service, LSPS2ServiceLiquiditySource}; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::runtime::Runtime; @@ -239,6 +240,7 @@ where L::Target: LdkLogger, { lsp_nodes: Vec, + lsps1_service: Option, lsps2_service: Option, wallet: Arc, channel_manager: Arc, @@ -258,9 +260,12 @@ where tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: L, ) -> Self { let lsp_nodes = Vec::new(); + let lsps1_service = None; let lsps2_service = None; + Self { lsp_nodes, + lsps1_service, lsps2_service, wallet, channel_manager, @@ -277,6 +282,11 @@ where self } + pub(crate) fn lsps1_service(&mut self, service_config: LSPS1Service) -> &mut Self { + self.lsps1_service = Some(service_config); + self + } + pub(crate) fn lsps2_service( &mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig, ) -> &mut Self { @@ -286,17 +296,26 @@ where } pub(crate) async fn build(self) -> Result, BuildError> { - let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { - let lsps2_service_config = Some(s.ldk_service_config.clone()); - let lsps5_service_config = None; - let advertise_service = s.service_config.advertise_service; - LiquidityServiceConfig { - lsps1_service_config: None, - lsps2_service_config, - lsps5_service_config, - advertise_service, - } - }); + let lsps1_service_config = self.lsps1_service.clone(); + + let (lsps2_service_config, advertise_service) = match &self.lsps2_service { + Some(s) => (Some(s.ldk_service_config.clone()), s.service_config.advertise_service), + None => (None, false), + }; + + let lsps5_service_config = None; + + let liquidity_service_config = + if lsps1_service_config.is_some() || lsps2_service_config.is_some() { + Some(LiquidityServiceConfig { + lsps1_service_config, + lsps2_service_config, + lsps5_service_config, + advertise_service, + }) + } else { + None + }; let (discovery_done_tx, discovery_done_rx) = tokio::sync::watch::channel(false); @@ -346,6 +365,14 @@ where liquidity_manager: Arc::clone(&liquidity_manager), logger: self.logger.clone(), }), + lsps1_service: Arc::new(LSPS1ServiceLiquiditySource { + lsps1_service_config: self.lsps1_service, + channel_manager: self.channel_manager, + peer_manager: RwLock::new(None), + liquidity_manager: Arc::clone(&liquidity_manager), + kv_store: self.kv_store, + logger: self.logger, + }), lsps2_client: Arc::new(LSPS2Client { lsp_nodes: Arc::clone(&lsp_nodes), pending_lsps2_fee_requests: Mutex::new(HashMap::new()), @@ -382,6 +409,7 @@ where { lsp_nodes: Arc>>, lsps1_client: Arc>, + lsps1_service: Arc>, lsps2_client: Arc>, lsps2_service: Arc>, pending_lsps0_discovery: Mutex>>>, @@ -403,6 +431,10 @@ where Arc::clone(&self.lsps1_client) } + pub(crate) fn lsps1_service(&self) -> Arc> { + Arc::clone(&self.lsps1_service) + } + pub(crate) fn lsps2_client(&self) -> Arc> { Arc::clone(&self.lsps2_client) } @@ -414,6 +446,7 @@ where pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { LiquidityEvent::LSPS1Client(event) => self.lsps1_client.handle_event(event).await, + LiquidityEvent::LSPS1Service(event) => self.lsps1_service.handle_event(event).await, LiquidityEvent::LSPS2Client(event) => self.lsps2_client.handle_event(event).await, LiquidityEvent::LSPS2Service(event) => self.lsps2_service.handle_event(event).await, diff --git a/src/liquidity/service/lsps1.rs b/src/liquidity/service/lsps1.rs new file mode 100644 index 0000000000..dc9a4129b6 --- /dev/null +++ b/src/liquidity/service/lsps1.rs @@ -0,0 +1,458 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use std::ops::Deref; +use std::sync::{Arc, RwLock, Weak}; +use std::time::Duration; + +use bitcoin::secp256k1::PublicKey; +use lightning::events::PaymentPurpose; +use lightning::impl_writeable_tlv_based; +use lightning::ln::channelmanager::Bolt11InvoiceParameters; +use lightning::offers::offer::Offer; +use lightning::util::persist::KVStore; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; +use lightning_liquidity::lsps1::event::LSPS1ServiceEvent; +use lightning_liquidity::lsps1::msgs::{ + LSPS1Bolt11PaymentInfo, LSPS1Bolt12PaymentInfo, LSPS1ChannelInfo, LSPS1OnchainPaymentInfo, + LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentInfo, LSPS1PaymentState, +}; +use lightning_liquidity::lsps1::service::PaymentMethod; +use lightning_types::payment::PaymentHash; + +use crate::error::Error; +use crate::logger::{log_error, LdkLogger}; +use crate::types::{ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet}; + +/// Server-side configuration options for bLIP-51 / LSPS1 channel requests. +pub(crate) struct LSPS1Service { + pub(crate) supported_options: LSPS1ServiceOptions, + pub(crate) supported_payment_options: LSPS1SupportedPaymentOptions, +} + +pub struct LSPS1SupportedPaymentOptions { + pub allow_bolt11_payment: bool, + pub allow_onchain_payment: bool, + pub allow_bolt12_payment: bool, +} + +pub(crate) struct LSPS1ServiceLiquiditySource +where + L::Target: LdkLogger, +{ + pub(crate) lsps1_service_config: Option, + pub(crate) channel_manager: Arc, + pub(crate) peer_manager: RwLock>>, + pub(crate) liquidity_manager: Arc, + pub(crate) kv_store: Arc, + pub(crate) logger: L, +} + +pub(crate) struct PendingLSPS1Order { + pub request_id: LSPSRequestId, + pub counterparty_node_id: PublicKey, + pub order_params: LSPS1OrderParams, + pub order_total_amount_sat: u64, + pub channel_expiry_blocks: u32, +} + +impl_writeable_tlv_based!(PendingLSPS1Order, { + (0, request_id, required), + (1, counterparty_node_id, required), + (2, order_params, required), + (3, order_total_amount_sat, required), + (4, channel_expiry_blocks, required), +}); + +pub(crate) struct PendingLSPS1Channel { + pub order_id: LSPS1OrderId, + pub channel_expiry_blocks: u32, +} + +impl_writeable_tlv_based!(PendingLSPS1Channel, { + (0, order_id, required), + (1, channel_expiry_blocks, required), +}); + +/// Represents the options supported by the LSP. +/// +/// See [bLIP-51 / LSPS1] for more information. +/// +/// [bLIP-51 / LSPS1]: https://github.com/lightning/blips/blob/master/blip-0051.md +#[derive(Debug, Clone)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct LSPS1ServiceOptions { + /// 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, + /// The smallest number of confirmations needed for the LSP to accept a channel as confirmed. + pub min_required_channel_confirmations: u16, + /// The smallest number of blocks in which the LSP can confirm the funding transaction. + pub min_funding_confirms_within_blocks: u16, + /// Indicates if the LSP supports zero reserve. + pub supports_zero_channel_reserve: bool, + /// The maximum number of blocks a channel can be leased for. + pub max_channel_expiry_blocks: u32, + /// The minimum number of satoshi that the client MUST request. + pub min_initial_client_balance_sat: u64, + /// The maximum number of satoshi that the client MUST request. + pub max_initial_client_balance_sat: u64, + /// The minimum number of satoshi that the LSP will provide to the channel. + pub min_initial_lsp_balance_sat: u64, + /// The maximum number of satoshi that the LSP will provide to the channel. + pub max_initial_lsp_balance_sat: u64, + /// The minimal channel size. + pub min_channel_balance_sat: u64, + /// The maximal channel size. + pub max_channel_balance_sat: u64, + /// The flat base fee charged for opening the channel, in millisatoshis. + pub channel_fee_base_msat: u64, + /// The proportional fee charged based on the requested LSP liquidity, in parts-per-million. + pub channel_fee_proportional_ppm: u32, + /// The datetime when the payment option expires. + pub payment_option_expires_at: LSPSDateTime, + /// The Bolt11 invoice expiration time. + pub bolt11_invoice_expiry_secs: Option, +} + +impl LSPS1ServiceLiquiditySource +where + L::Target: LdkLogger, +{ + pub(crate) fn set_peer_manager(&self, peer_manager: Weak) { + *self.peer_manager.write().expect("lock") = Some(peer_manager); + } + + pub(crate) fn liquidity_manager(&self) -> Arc { + Arc::clone(&self.liquidity_manager) + } + + pub(crate) async fn handle_event(&self, event: LSPS1ServiceEvent) { + match event { + LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + counterparty_node_id, + order, + refund_onchain_address, + } => { + let lsps1_service_handler = match self.liquidity_manager.lsps1_service_handler() { + Some(handler) => handler, + None => { + log_error!(self.logger, "Failed to handle LSPS1ServiceEvent as LSPS1 liquidity service was not configured.",); + return; + }, + }; + + let service_config = match self.lsps1_service_config { + Some(config) => config, + None => { + log_error!(self.logger, "Failed to handle LSPS1ServiceEvent as LSPS1 liquidity service was not configured.",); + return; + }, + }; + + if let Some(token) = service_config.supported_options.require_token { + if Some(token) != order.token { + log_error!( + self.logger, + "Rejecting LSPS1 request {:?} from counterparty {} as the client provided an invalid token.", + request_id, + counterparty_node_id + ); + lsps1_service_handler.invalid_token_provided(counterparty_node_id, request_id.clone()). + unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS1 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS1 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + return; + } + } + + let mut payment_info = + LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: None }; + let mut payment_hash_opt = None; + let mut offer_id_opt = None; + + let (fee_total_sat, order_total_sat) = self.calculate_order_amounts(&order); + + if service_config.supported_payment_options.allow_bolt11_payment { + let invoice = + match self.handle_bolt11_payment(&request_id, &order, &service_config) { + Ok(inv) => inv, + Err(e) => { + log_error!( + self.logger, + "Failed to generate LSPS1 BOLT11 invoice: {:?}", + e + ); + return; + }, + }; + + payment_hash_opt = Some(invoice.payment_hash()); + + payment_info.bolt11 = Some(LSPS1Bolt11PaymentInfo { + state: LSPS1PaymentState::ExpectPayment, + expires_at: service_config.supported_options.payment_option_expires_at, + fee_total_sat, + order_total_sat, + invoice, + }) + } + + if service_config.supported_payment_options.allow_bolt12_payment { + let offer = match self.handle_bolt12_offer(&request_id, &order) { + Ok(offer) => offer, + Err(e) => { + log_error!(self.logger, "Failed to create offer: {:?}", e); + return; + }, + }; + + offer_id_opt = Some(offer.id()); + + payment_info.bolt12 = Some(LSPS1Bolt12PaymentInfo { + state: LSPS1PaymentState::ExpectPayment, + expires_at: service_config.supported_options.payment_option_expires_at, + fee_total_sat, + order_total_sat, + offer, + }) + } + + if refund_onchain_address.is_none() + && service_config.supported_payment_options.allow_onchain_payment + { + lsps1_service_handler.onchain_payments_required(counterparty_node_id, request_id).unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS1 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS1 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + } + + // TODO: handle the onchain payment option here + if refund_onchain_address.is_some() + && service_config.supported_payment_options.allow_onchain_payment + { + payment_info.onchain = None; + } + + if payment_hash_opt.is_none() && offer_id_opt.is_none() { + log_error!( + self.logger, + "Failed to process LSPS1 request: No valid payment method was generated." + ); + return; + } + + let pending_order = PendingLSPS1Order { + counterparty_node_id, + request_id: request_id.clone(), + order_params: order.clone(), + order_total_amount_sat: order_total_sat, + channel_expiry_blocks: service_config + .supported_options + .max_channel_expiry_blocks, + }; + + let serialized_order = pending_order.encode(); + + if let Some(payment_hash) = payment_hash_opt { + if let Err(e) = self.kv_store.write( + "lsps1_pending_orders", + "", + &payment_hash.to_string(), + serialized_order.clone(), + ) { + log_error!( + self.logger, + "Failed to persist pending LSPS1 order for payment hash {}: {:?}. Aborting request.", + payment_hash, + e + ); + return; + } + } + + if let Some(offer_id) = offer_id_opt { + if let Err(e) = self.kv_store.write( + "lsps1_pending_orders", + "", + &offer_id.to_string(), + serialized_order.clone(), + ) { + log_error!( + self.logger, + "Failed to persist pending LSPS1 order for offer ID {}: {:?}. Aborting request.", + offer_id, + e + ); + return; + } + } + + if let Err(e) = lsps1_service_handler + .send_payment_details(request_id, counterparty_node_id, payment_info) + .await + { + log_error!( + self.logger, + "Failed to send LSPS1 payment details {:?} to counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + } + }, + _ => {}, + } + } + + pub(crate) fn calculate_order_amounts(&self, order: &LSPS1OrderParams) -> (u64, u64) { + let config = match &self.lsps1_service_config { + Some(cfg) => cfg, + None => return (0, 0), + }; + + let lsp_balance_msat = order.lsp_balance_sat.saturating_mul(1000); + let proportional_fee_msat = (lsp_balance_msat as u128) + .saturating_mul(config.supported_options.channel_fee_proportional_ppm as u128) + / 1_000_000; + + let service_fee_msat = config + .supported_options + .channel_fee_base_msat + .saturating_add(proportional_fee_msat as u64); + + let fee_total_sat = service_fee_msat.saturating_add(999) / 1000; + + let order_total_sat = fee_total_sat.saturating_add(order.client_balance_sat); + + (fee_total_sat, order_total_sat) + } + + fn handle_bolt11_payment( + &self, request_id: &LSPSRequestId, order: &LSPS1OrderParams, service_config: &LSPS1Service, + ) -> Result { + let (fee_total_sat, order_total_sat) = self.calculate_order_amounts(&order); + + let invoice_amount_msat = order_total_sat.saturating_mul(1000); + + let invoice_description = Bolt11InvoiceDescription::Direct( + Description::new(format!("LSPS1 Order {}", request_id.0)) + .map_err(|_| Error::InvoiceCreationFailed)?, + ); + + let invoice_params = Bolt11InvoiceParameters { + amount_msats: Some(invoice_amount_msat), + description: invoice_description, + invoice_expiry_delta_secs: service_config.supported_options.bolt11_invoice_expiry_secs, + ..Default::default() + }; + + let invoice = match self.channel_manager.create_bolt11_invoice(invoice_params) { + Ok(invoice) => invoice, + Err(e) => { + log_error!(self.logger, "Failed to generate LSPS1 BOLT11 invoice: {:?}", e); + return Err(Error::InvoiceCreationFailed); + }, + }; + + Ok(invoice) + } + + fn handle_bolt12_offer( + &self, request_id: &LSPSRequestId, order: &LSPS1OrderParams, + ) -> Result { + let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + + let (fee_total_sat, order_total_sat) = self.calculate_order_amounts(&order); + + let offer_amount_msat = order_total_sat.saturating_mul(1000); + let description = format!("LSPS1 Order {}", request_id.to_string()); + + let mut offer = offer_builder.amount_msats(offer_amount_msat).description(description); + + let finalized_offer = offer.build().map_err(|e| { + log_error!(self.logger, "Failed to create offer: {:?}", e); + Error::OfferCreationFailed + })?; + + Ok(finalized_offer) + } + + pub(crate) async fn handle_order_payment_received( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, method: PaymentMethod, + ) { + if let Some(lsps1_service_handler) = self.liquidity_manager.lsps1_service_handler() { + if let Err(e) = lsps1_service_handler + .order_payment_received(counterparty_node_id, order_id, method) + .await + { + log_error!( + self.logger, + "Failed to handle and mark the order {:?} as paid due to: {:?}.", + order_id, + e + ); + } + } + } + + pub(crate) async fn handle_order_channel_opened( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + channel_info: LSPS1ChannelInfo, + ) { + if let Some(lsps1_service_handler) = self.liquidity_manager.lsps1_service_handler() { + if let Err(e) = lsps1_service_handler + .order_channel_opened(counterparty_node_id, order_id, channel_info) + .await + { + log_error!( + self.logger, + "Failed to handle and mark the order {:?} as completed due to: {:?}.", + order_id, + e + ); + } + } + } + + pub(crate) async fn handle_order_failed_and_refunded( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + ) { + if let Some(lsps1_service_handler) = self.liquidity_manager.lsps1_service_handler() { + if let Err(e) = lsps1_service_handler + .order_failed_and_refunded(counterparty_node_id, order_id) + .await + { + log_error!( + self.logger, + "Failed to handle and mark the order {:?} as failed and refunded due to: {:?}", + order_id, + e + ); + } + } + } +} diff --git a/src/liquidity/service/mod.rs b/src/liquidity/service/mod.rs index cdbaf54265..2a236d492b 100644 --- a/src/liquidity/service/mod.rs +++ b/src/liquidity/service/mod.rs @@ -5,4 +5,5 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +pub(crate) mod lsps1; pub(crate) mod lsps2; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fd247f74cf..2ae1e352d9 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -3450,7 +3450,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.enable_liquidity_provider(lsps2_service_config); + service_builder.enable_liquidity_provider(None, Some(lsps2_service_config)); let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); @@ -3778,7 +3778,7 @@ async fn lsps2_client_trusts_lsp() { let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.enable_liquidity_provider(lsps2_service_config); + service_builder.enable_liquidity_provider(None, Some(lsps2_service_config)); let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); let service_node_id = service_node.node_id(); @@ -3955,7 +3955,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - service_builder.enable_liquidity_provider(lsps2_service_config); + service_builder.enable_liquidity_provider(None, Some(lsps2_service_config)); let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); service_node.start().unwrap(); @@ -4861,7 +4861,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { let cheap_node_config = random_config(); setup_builder!(cheap_builder, cheap_node_config.node_config); cheap_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - cheap_builder.enable_liquidity_provider(cheap_cfg); + cheap_builder.enable_liquidity_provider(None, Some(cheap_cfg)); let cheap = cheap_builder.build(cheap_node_config.node_entropy.into()).unwrap(); cheap.start().unwrap(); let cheap_id = cheap.node_id(); @@ -4884,7 +4884,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { let expensive_node_config = random_config(); setup_builder!(expensive_builder, expensive_node_config.node_config); expensive_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); - expensive_builder.enable_liquidity_provider(expensive_cfg); + expensive_builder.enable_liquidity_provider(None, Some(expensive_cfg)); let expensive = expensive_builder.build(expensive_node_config.node_entropy.into()).unwrap(); expensive.start().unwrap(); let expensive_id = expensive.node_id();