diff --git a/Cargo.toml b/Cargo.toml index bf8bed08c..d09f59382 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,27 +28,28 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.1.0", features = ["std"] } -lightning-types = { version = "0.2.0" } -lightning-invoice = { version = "0.33.0", features = ["std"] } -lightning-net-tokio = { version = "0.1.0" } -lightning-persister = { version = "0.1.0" } -lightning-background-processor = { version = "0.1.0", features = ["futures"] } -lightning-rapid-gossip-sync = { version = "0.1.0" } -lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "tokio"] } -lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum"] } -lightning-liquidity = { version = "0.1.0", features = ["std"] } - -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } -#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } -#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["futures"] } -#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rpc-client", "tokio"] } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "electrum", "time"] } -#lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning = { version = "0.1.0", features = ["std"] } +#lightning-types = { version = "0.2.0" } +#lightning-invoice = { version = "0.33.0", features = ["std"] } +#lightning-net-tokio = { version = "0.1.0" } +#lightning-persister = { version = "0.1.0" } +#lightning-background-processor = { version = "0.1.0", features = ["futures"] } +#lightning-rapid-gossip-sync = { version = "0.1.0" } +#lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "tokio"] } +#lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum"] } +#lightning-liquidity = { version = "0.1.0", features = ["std"] } + +lightning = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["std"] } +lightning-types = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-invoice = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-persister = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-background-processor = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-rapid-gossip-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-block-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["esplora-async-https", "electrum", "time"] } +lightning-liquidity = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } +lightning-macros = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -96,8 +97,8 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { version = "0.1.0", features = ["std", "_test_utils"] } -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +#lightning = { version = "0.1.0", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/martinsaposnic/rust-lightning", branch="client-trusts-lsp", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e914fd00e..3beee9a18 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -12,7 +12,7 @@ dictionary Config { sequence trusted_peers_0conf; u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; - SendingParameters? sending_parameters; + RouteParametersConfig? route_parameters; }; dictionary AnchorChannelsConfig { @@ -160,19 +160,19 @@ interface Node { [Enum] interface Bolt11InvoiceDescription { - Hash(string hash); - Direct(string description); + Hash(string hash); + Direct(string description); }; interface Bolt11Payment { [Throws=NodeError] - PaymentId send([ByRef]Bolt11Invoice invoice, SendingParameters? sending_parameters); + PaymentId send([ByRef]Bolt11Invoice invoice, RouteParametersConfig? route_parameters); [Throws=NodeError] - PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, SendingParameters? sending_parameters); + PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, RouteParametersConfig? route_parameters); [Throws=NodeError] - void send_probes([ByRef]Bolt11Invoice invoice); + void send_probes([ByRef]Bolt11Invoice invoice, RouteParametersConfig? route_parameters); [Throws=NodeError] - void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, RouteParametersConfig? route_parameters); [Throws=NodeError] void claim_for_hash(PaymentHash payment_hash, u64 claimable_amount_msat, PaymentPreimage preimage); [Throws=NodeError] @@ -208,9 +208,9 @@ interface Bolt12Payment { interface SpontaneousPayment { [Throws=NodeError] - PaymentId send(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters); + PaymentId send(u64 amount_msat, PublicKey node_id, RouteParametersConfig? route_parameters); [Throws=NodeError] - PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters, sequence custom_tlvs); + PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, RouteParametersConfig? route_parameters, sequence custom_tlvs); [Throws=NodeError] void send_probes(u64 amount_msat, PublicKey node_id); }; @@ -245,7 +245,7 @@ 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); + LSPS1OrderStatus check_order_status(LSPS1OrderId order_id); }; [Error] @@ -446,11 +446,11 @@ dictionary PaymentDetails { u64 latest_update_timestamp; }; -dictionary SendingParameters { - MaxTotalRoutingFeeLimit? max_total_routing_fee_msat; - u32? max_total_cltv_expiry_delta; - u8? max_path_count; - u8? max_channel_saturation_power_of_half; +dictionary RouteParametersConfig { + u64? max_total_routing_fee_msat; + u32 max_total_cltv_expiry_delta; + u8 max_path_count; + u8 max_channel_saturation_power_of_half; }; dictionary CustomTlvRecord { @@ -459,13 +459,13 @@ dictionary CustomTlvRecord { }; dictionary LSPS1OrderStatus { - OrderId order_id; - OrderParameters order_params; - PaymentInfo payment_options; - ChannelOrderInfo? channel_state; + LSPS1OrderId order_id; + LSPS1OrderParams order_params; + LSPS1PaymentInfo payment_options; + LSPS1ChannelInfo? channel_state; }; -dictionary OrderParameters { +dictionary LSPS1OrderParams { u64 lsp_balance_sat; u64 client_balance_sat; u16 required_channel_confirmations; @@ -475,22 +475,22 @@ dictionary OrderParameters { boolean announce_channel; }; -dictionary PaymentInfo { - Bolt11PaymentInfo? bolt11; - OnchainPaymentInfo? onchain; +dictionary LSPS1PaymentInfo { + LSPS1Bolt11PaymentInfo? bolt11; + LSPS1OnchainPaymentInfo? onchain; }; -dictionary Bolt11PaymentInfo { - PaymentState state; - DateTime expires_at; +dictionary LSPS1Bolt11PaymentInfo { + LSPS1PaymentState state; + LSPSDateTime expires_at; u64 fee_total_sat; u64 order_total_sat; Bolt11Invoice invoice; }; -dictionary OnchainPaymentInfo { - PaymentState state; - DateTime expires_at; +dictionary LSPS1OnchainPaymentInfo { + LSPS1PaymentState state; + LSPSDateTime expires_at; u64 fee_total_sat; u64 order_total_sat; Address address; @@ -499,24 +499,18 @@ dictionary OnchainPaymentInfo { Address? refund_onchain_address; }; -dictionary ChannelOrderInfo { - DateTime funded_at; +dictionary LSPS1ChannelInfo { + LSPSDateTime funded_at; OutPoint funding_outpoint; - DateTime expires_at; + LSPSDateTime expires_at; }; -enum PaymentState { +enum LSPS1PaymentState { "ExpectPayment", "Paid", "Refunded", }; -[Enum] -interface MaxTotalRoutingFeeLimit { - None (); - Some ( u64 amount_msat ); -}; - [NonExhaustive] enum Network { "Bitcoin", @@ -851,7 +845,7 @@ typedef string UntrustedString; typedef string NodeAlias; [Custom] -typedef string OrderId; +typedef string LSPS1OrderId; [Custom] -typedef string DateTime; +typedef string LSPSDateTime; diff --git a/src/builder.rs b/src/builder.rs index 31a0fee45..0a7ca4506 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1187,17 +1187,6 @@ fn build_with_store_internal( }; let mut user_config = default_user_config(&config); - 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; - - // FIXME: When we're an LSPS2 client, set maximum allowed inbound HTLC value in flight - // to 100%. We should eventually be able to set this on a per-channel basis, but for - // now we just bump the default for all channels. - user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = - 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 @@ -1275,8 +1264,8 @@ fn build_with_store_internal( // Give ChannelMonitors to ChainMonitor for (_blockhash, channel_monitor) in channel_monitors.into_iter() { - let funding_outpoint = channel_monitor.get_funding_txo().0; - chain_monitor.watch_channel(funding_outpoint, channel_monitor).map_err(|e| { + let channel_id = channel_monitor.channel_id(); + chain_monitor.watch_channel(channel_id, channel_monitor).map_err(|e| { log_error!(logger, "Failed to watch channel monitor: {:?}", e); BuildError::InvalidChannelMonitor })?; diff --git a/src/chain/mod.rs b/src/chain/mod.rs index fac8b0e6c..097cd0d82 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -452,7 +452,7 @@ impl ChainSource { if let Some(worst_channel_monitor_block_hash) = chain_monitor .list_monitors() .iter() - .flat_map(|(txo, _)| chain_monitor.get_monitor(*txo)) + .flat_map(|channel_id| chain_monitor.get_monitor(*channel_id)) .map(|m| m.current_best_block()) .min_by_key(|b| b.height) .map(|b| b.block_hash) diff --git a/src/config.rs b/src/config.rs index 4a39c1b56..a87a31924 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,10 +8,10 @@ //! Objects for configuring the node. use crate::logger::LogLevel; -use crate::payment::SendingParameters; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; +use lightning::routing::router::RouteParametersConfig; use lightning::util::config::ChannelConfig as LdkChannelConfig; use lightning::util::config::MaxDustHTLCExposure as LdkMaxDustHTLCExposure; use lightning::util::config::UserConfig; @@ -102,9 +102,9 @@ pub const WALLET_KEYS_SEED_LEN: usize = 64; /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | -/// | `sending_parameters` | None | +/// | `route_parameters` | None | /// -/// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their +/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. /// /// [`Node`]: crate::Node @@ -161,12 +161,12 @@ pub struct Config { pub anchor_channels_config: Option, /// Configuration options for payment routing and pathfinding. /// - /// Setting the `SendingParameters` provides flexibility to customize how payments are routed, + /// Setting the [`RouteParametersConfig`] provides flexibility to customize how payments are routed, /// including setting limits on routing fees, CLTV expiry, and channel utilization. /// /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. - pub sending_parameters: Option, + pub route_parameters: Option, } impl Default for Config { @@ -179,7 +179,7 @@ impl Default for Config { trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, anchor_channels_config: Some(AnchorChannelsConfig::default()), - sending_parameters: None, + route_parameters: None, node_alias: None, } } diff --git a/src/event.rs b/src/event.rs index 22848bec1..a42a950c0 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,6 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use crate::payment::PaymentDetails; use crate::types::{CustomTlvRecord, DynStore, PaymentStore, Sweeper, Wallet}; use crate::{ @@ -19,9 +20,7 @@ use crate::fee_estimator::ConfirmationTarget; use crate::liquidity::LiquiditySource; use crate::logger::Logger; -use crate::payment::store::{ - PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, -}; +use crate::payment::store::{PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus}; use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, @@ -36,6 +35,9 @@ use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; use lightning::ln::types::ChannelId; use lightning::routing::gossip::NodeId; +use lightning::util::config::{ + ChannelConfigOverrides, ChannelConfigUpdate, ChannelHandshakeConfigUpdate, +}; use lightning::util::errors::APIError; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; @@ -493,7 +495,7 @@ where counterparty_node_id, channel_value_satoshis, output_script, - .. + user_channel_id, } => { // Construct the raw transaction with the output that is paid the amount of the // channel. @@ -512,12 +514,39 @@ where locktime, ) { Ok(final_tx) => { - // Give the funding transaction back to LDK for opening the channel. - match self.channel_manager.funding_transaction_generated( - temporary_channel_id, - counterparty_node_id, - final_tx, - ) { + let needs_manual_broadcast = self + .liquidity_source + .as_ref() + .map(|ls| { + ls.as_ref().lsps2_channel_needs_manual_broadcast( + counterparty_node_id, + user_channel_id, + ) + }) + .unwrap_or(false); + + let result = if needs_manual_broadcast { + self.liquidity_source.as_ref().map(|ls| { + ls.lsps2_store_funding_transaction( + user_channel_id, + counterparty_node_id, + final_tx.clone(), + ); + }); + self.channel_manager.funding_transaction_generated_manual_broadcast( + temporary_channel_id, + counterparty_node_id, + final_tx, + ) + } else { + self.channel_manager.funding_transaction_generated( + temporary_channel_id, + counterparty_node_id, + final_tx, + ) + }; + + match result { Ok(()) => {}, Err(APIError::APIMisuseError { err }) => { log_error!(self.logger, "Panicking due to APIMisuseError: {}", err); @@ -556,16 +585,17 @@ where }, } }, - LdkEvent::FundingTxBroadcastSafe { .. } => { - debug_assert!(false, "We currently only support safe funding, so this event should never be emitted."); + LdkEvent::FundingTxBroadcastSafe { user_channel_id, counterparty_node_id, .. } => { + self.liquidity_source.as_ref().map(|ls| { + ls.lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id); + }); }, LdkEvent::PaymentClaimable { payment_hash, purpose, amount_msat, receiver_node_id: _, - via_channel_id: _, - via_user_channel_id: _, + via_channel_ids: _, claim_deadline, onion_fields, counterparty_skimmed_fee_msat, @@ -683,8 +713,9 @@ where // the payment has been registered via `_for_hash` variants and needs to be manually claimed via // user interaction. match info.kind { - PaymentKind::Bolt11 { preimage, .. } => { - if purpose.preimage().is_none() { + PaymentKind::Bolt11 { preimage, .. } + | PaymentKind::Bolt11Jit { preimage, .. } => { + if preimage.is_none() || purpose.preimage().is_none() { debug_assert!( preimage.is_none(), "We would have registered the preimage if we knew" @@ -1040,9 +1071,9 @@ where LdkEvent::PaymentPathFailed { .. } => {}, LdkEvent::ProbeSuccessful { .. } => {}, LdkEvent::ProbeFailed { .. } => {}, - LdkEvent::HTLCHandlingFailed { failed_next_destination, .. } => { + LdkEvent::HTLCHandlingFailed { failure_type, .. } => { if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_htlc_handling_failed(failed_next_destination); + liquidity_source.handle_htlc_handling_failed(failure_type); } }, LdkEvent::PendingHTLCsForwardable { time_forwardable } => { @@ -1159,17 +1190,44 @@ where let user_channel_id: u128 = rand::thread_rng().gen::(); let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id); + let mut channel_override_config = None; + if let Some((lsp_node_id, _)) = self + .liquidity_source + .as_ref() + .and_then(|ls| ls.as_ref().get_lsps2_lsp_details()) + { + if lsp_node_id == counterparty_node_id { + // When we're an LSPS2 client, allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll + // check that they don't take too much before claiming. + // + // We also set maximum allowed inbound HTLC value in flight + // to 100%. We should eventually be able to set this on a per-channel basis, but for + // now we just bump the default for all channels. + channel_override_config = Some(ChannelConfigOverrides { + handshake_overrides: Some(ChannelHandshakeConfigUpdate { + max_inbound_htlc_value_in_flight_percent_of_channel: Some(100), + ..Default::default() + }), + update_overrides: Some(ChannelConfigUpdate { + accept_underpaying_htlcs: Some(true), + ..Default::default() + }), + }); + } + } let res = if allow_0conf { self.channel_manager.accept_inbound_channel_from_trusted_peer_0conf( &temporary_channel_id, &counterparty_node_id, user_channel_id, + channel_override_config, ) } else { self.channel_manager.accept_inbound_channel( &temporary_channel_id, &counterparty_node_id, user_channel_id, + channel_override_config, ) }; diff --git a/src/lib.rs b/src/lib.rs index e80ca964d..534f56000 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -550,6 +550,9 @@ impl Node { let background_chan_man = Arc::clone(&self.channel_manager); let background_gossip_sync = self.gossip_source.as_gossip_sync(); let background_peer_man = Arc::clone(&self.peer_manager); + let background_liquidity_man_opt = + self.liquidity_source.as_ref().map(|ls| Arc::clone(&ls.liquidity_manager())); + let background_sweeper = Arc::clone(&self.output_sweeper); let background_onion_messenger = Arc::clone(&self.onion_messenger); let background_logger = Arc::clone(&self.logger); let background_error_logger = Arc::clone(&self.logger); @@ -586,6 +589,8 @@ impl Node { Some(background_onion_messenger), background_gossip_sync, background_peer_man, + background_liquidity_man_opt, + Some(background_sweeper), background_logger, Some(background_scorer), sleeper, @@ -1425,12 +1430,10 @@ impl Node { let mut total_lightning_balance_sats = 0; let mut lightning_balances = Vec::new(); - for (funding_txo, channel_id) in self.chain_monitor.list_monitors() { - match self.chain_monitor.get_monitor(funding_txo) { + for channel_id in self.chain_monitor.list_monitors() { + match self.chain_monitor.get_monitor(channel_id) { Ok(monitor) => { - // unwrap safety: `get_counterparty_node_id` will always be `Some` after 0.0.110 and - // LDK Node 0.1 depended on 0.0.115 already. - let counterparty_node_id = monitor.get_counterparty_node_id().unwrap(); + let counterparty_node_id = monitor.get_counterparty_node_id(); for ldk_balance in monitor.get_claimable_balances() { total_lightning_balance_sats += ldk_balance.claimable_amount_satoshis(); lightning_balances.push(LightningBalance::from_ldk_balance( diff --git a/src/liquidity.rs b/src/liquidity.rs index a4516edd0..4e10f27b2 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -13,7 +13,8 @@ use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{total_anchor_channels_reserve_sats, Config, Error}; -use lightning::events::HTLCDestination; +use bitcoin::Transaction; +use lightning::events::HTLCHandlingFailureType; use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; use lightning::ln::types::ChannelId; @@ -21,14 +22,16 @@ 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::events::LiquidityEvent; +use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; 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::lsps1::msgs::{ + LSPS1ChannelInfo, LSPS1Options, LSPS1OrderId, LSPS1OrderParams, +}; 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::msgs::{LSPS2OpeningFeeParams, LSPS2RawOpeningFeeParams}; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; @@ -40,7 +43,7 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; -use chrono::{DateTime, Utc}; +use chrono::Utc; use rand::Rng; @@ -52,7 +55,6 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; 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 { @@ -61,10 +63,10 @@ struct LSPS1Client { token: Option, ldk_client_config: LdkLSPS1ClientConfig, pending_opening_params_requests: - Mutex>>, - pending_create_order_requests: Mutex>>, + Mutex>>, + pending_create_order_requests: Mutex>>, pending_check_order_status_requests: - Mutex>>, + Mutex>>, } #[derive(Debug, Clone)] @@ -79,8 +81,8 @@ struct LSPS2Client { lsp_address: SocketAddress, token: Option, ldk_client_config: LdkLSPS2ClientConfig, - pending_fee_requests: Mutex>>, - pending_buy_requests: Mutex>>, + pending_fee_requests: Mutex>>, + pending_buy_requests: Mutex>>, } #[derive(Debug, Clone)] @@ -131,6 +133,8 @@ pub struct LSPS2ServiceConfig { pub min_payment_size_msat: u64, /// The maximum payment size that we will accept when opening a channel. pub max_payment_size_msat: u64, + /// Use the client trusts lsp model + pub client_trusts_lsp: bool, } pub(crate) struct LiquiditySourceBuilder @@ -275,12 +279,10 @@ where { 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); } - pub(crate) fn liquidity_manager(&self) -> &LiquidityManager { - self.liquidity_manager.as_ref() + pub(crate) fn liquidity_manager(&self) -> &Arc { + &self.liquidity_manager } pub(crate) fn get_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { @@ -291,9 +293,84 @@ where self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } + pub(crate) fn lsps2_channel_needs_manual_broadcast( + &self, counterparty_node_id: PublicKey, user_channel_id: u128, + ) -> bool { + // if we are not in a client_trusts_lsp model, we don't check and just return false + if !self.is_client_trusts_lsp() { + log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP."); + return false; + } + + // if we are in a client_trusts_lsp model, then we check if the LSP has an LSPS2 operation in progress + self.lsps2_service.as_ref().map_or(false, |_| { + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler + .channel_needs_manual_broadcast(user_channel_id, &counterparty_node_id) + .unwrap_or(false) + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + false + } + }) + } + + pub(crate) fn lsps2_store_funding_transaction( + &self, user_channel_id: u128, counterparty_node_id: PublicKey, funding_tx: Transaction, + ) { + if !self.is_client_trusts_lsp() { + log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP."); + return; + } + self.lsps2_service.as_ref().map(|_| { + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler + .store_funding_transaction(user_channel_id, &counterparty_node_id, funding_tx) + .unwrap_or_else(|e| { + debug_assert!(false, "Failed to store funding transaction: {:?}", e); + log_error!(self.logger, "Failed to store funding transaction: {:?}", e); + }); + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + } + }); + } + + pub(crate) fn lsps2_funding_tx_broadcast_safe( + &self, user_channel_id: u128, counterparty_node_id: PublicKey, + ) { + if !self.is_client_trusts_lsp() { + log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP."); + return; + } + self.lsps2_service.as_ref().map(|_| { + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler + .funding_tx_broadcast_safe(user_channel_id, &counterparty_node_id) + .unwrap_or_else(|e| { + debug_assert!(false, "Failed to store funding transaction: {:?}", e); + log_error!(self.logger, "Failed to store funding transaction: {:?}", e); + }); + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + } + }); + } + + fn is_client_trusts_lsp(&self) -> bool { + if let Some(lsps2_service) = self.lsps2_service.as_ref() { + lsps2_service.service_config.client_trusts_lsp + } else { + false + } + } + pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { - Event::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { request_id, counterparty_node_id, supported_options, @@ -346,7 +423,7 @@ where ); } }, - Event::LSPS1Client(LSPS1ClientEvent::OrderCreated { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { request_id, counterparty_node_id, order_id, @@ -404,7 +481,7 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderCreated event!"); } }, - Event::LSPS1Client(LSPS1ClientEvent::OrderStatus { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { request_id, counterparty_node_id, order_id, @@ -462,7 +539,7 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); } }, - Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { request_id, counterparty_node_id, token, @@ -501,10 +578,8 @@ where } } - let mut valid_until: DateTime = Utc::now(); - valid_until += LSPS2_GETINFO_REQUEST_EXPIRY; - - let opening_fee_params = RawOpeningFeeParams { + let valid_until = LSPSDateTime(Utc::now() + LSPS2_GETINFO_REQUEST_EXPIRY); + let opening_fee_params = LSPS2RawOpeningFeeParams { min_fee_msat: service_config.min_channel_opening_fee_msat, proportional: service_config.channel_opening_fee_ppm, valid_until, @@ -532,7 +607,7 @@ where return; } }, - Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BuyRequest { request_id, counterparty_node_id, opening_fee_params: _, @@ -581,7 +656,7 @@ where request_id, intercept_scid, LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, - LSPS2_CLIENT_TRUSTS_LSP_MODE, + service_config.client_trusts_lsp, user_channel_id, ) { Ok(()) => {}, @@ -599,7 +674,7 @@ where return; } }, - Event::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { their_network_key, amt_to_forward_msat, opening_fee_msat: _, @@ -673,7 +748,7 @@ where return; } - let mut config = *self.channel_manager.get_current_default_configuration(); + let mut config = self.channel_manager.get_current_default_configuration().clone(); // We set these LSP-specific values during Node building, here we're making sure it's actually set. debug_assert_eq!( @@ -713,7 +788,7 @@ where }, } }, - Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, opening_fee_params_menu, @@ -763,7 +838,7 @@ where ); } }, - Event::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { request_id, counterparty_node_id, intercept_scid, @@ -903,7 +978,7 @@ where return Err(Error::LiquidityRequestFailed); } - let order_params = OrderParameters { + let order_params = LSPS1OrderParams { lsp_balance_sat, client_balance_sat, required_channel_confirmations: lsp_limits.min_required_channel_confirmations, @@ -952,7 +1027,7 @@ where } pub(crate) async fn lsps1_check_order_status( - &self, order_id: OrderId, + &self, order_id: LSPS1OrderId, ) -> 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(|| { @@ -1120,7 +1195,7 @@ where } async fn lsps2_send_buy_request( - &self, amount_msat: Option, opening_fee_params: OpeningFeeParams, + &self, amount_msat: Option, opening_fee_params: LSPS2OpeningFeeParams, ) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -1255,9 +1330,9 @@ where } } - pub(crate) fn handle_htlc_handling_failed(&self, failed_next_destination: HTLCDestination) { + pub(crate) fn handle_htlc_handling_failed(&self, failure_type: HTLCHandlingFailureType) { 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) { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failure_type) { log_error!( self.logger, "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", @@ -1291,82 +1366,24 @@ pub(crate) struct LSPS1OpeningParamsResponse { #[derive(Debug, Clone)] pub struct LSPS1OrderStatus { /// The id of the channel order. - pub order_id: OrderId, + pub order_id: LSPS1OrderId, /// The parameters of channel order. - pub order_params: OrderParameters, + pub order_params: LSPS1OrderParams, /// Contains details about how to pay for the order. - pub payment_options: PaymentInfo, + pub payment_options: LSPS1PaymentInfo, /// Contains information about the channel state. - pub channel_state: Option, + pub channel_state: Option, } #[cfg(not(feature = "uniffi"))] -type PaymentInfo = lightning_liquidity::lsps1::msgs::PaymentInfo; - -/// Details regarding how to pay for an order. -#[cfg(feature = "uniffi")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PaymentInfo { - /// A Lightning payment using BOLT 11. - pub bolt11: Option, - /// An onchain payment. - pub onchain: Option, -} +type LSPS1PaymentInfo = lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo; #[cfg(feature = "uniffi")] -impl From for PaymentInfo { - fn from(value: lightning_liquidity::lsps1::msgs::PaymentInfo) -> Self { - PaymentInfo { - bolt11: value.bolt11.map(|b| b.into()), - onchain: value.onchain.map(|o| o.into()), - } - } -} - -/// An onchain payment. -#[cfg(feature = "uniffi")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OnchainPaymentInfo { - /// Indicates the current state of the payment. - pub state: lightning_liquidity::lsps1::msgs::PaymentState, - /// The datetime when the payment option expires. - pub expires_at: chrono::DateTime, - /// The total fee the LSP will charge to open this channel in satoshi. - pub fee_total_sat: u64, - /// The amount the client needs to pay to have the requested channel openend. - pub order_total_sat: u64, - /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel - /// opened. - pub address: bitcoin::Address, - /// The minimum number of block confirmations that are required for the on-chain payment to be - /// considered confirmed. - pub min_onchain_payment_confirmations: Option, - /// The minimum fee rate for the on-chain payment in case the client wants the payment to be - /// confirmed without a confirmation. - pub min_fee_for_0conf: Arc, - /// The address where the LSP will send the funds if the order fails. - pub refund_onchain_address: Option, -} - -#[cfg(feature = "uniffi")] -impl From for OnchainPaymentInfo { - fn from(value: lightning_liquidity::lsps1::msgs::OnchainPaymentInfo) -> Self { - Self { - state: value.state, - expires_at: value.expires_at, - fee_total_sat: value.fee_total_sat, - order_total_sat: value.order_total_sat, - address: value.address, - min_onchain_payment_confirmations: value.min_onchain_payment_confirmations, - min_fee_for_0conf: Arc::new(value.min_fee_for_0conf), - refund_onchain_address: value.refund_onchain_address, - } - } -} +type LSPS1PaymentInfo = crate::uniffi_types::LSPS1PaymentInfo; #[derive(Debug, Clone)] pub(crate) struct LSPS2FeeResponse { - opening_fee_params_menu: Vec, + opening_fee_params_menu: Vec, } #[derive(Debug, Clone)] @@ -1456,7 +1473,7 @@ impl LSPS1Liquidity { } /// 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 { + pub fn check_order_status(&self, order_id: LSPS1OrderId) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; diff --git a/src/message_handler.rs b/src/message_handler.rs index cebd1ea07..25995a481 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -10,6 +10,7 @@ use crate::liquidity::LiquiditySource; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; use lightning::util::logger::Logger; +use lightning::util::ser::LengthLimitedRead; use lightning_types::features::{InitFeatures, NodeFeatures}; @@ -47,7 +48,7 @@ where { type CustomMessage = RawLSPSMessage; - fn read( + fn read( &self, message_type: u16, buffer: &mut RD, ) -> Result, lightning::ln::msgs::DecodeError> { match self { diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 817a428bd..9eb6cef81 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -20,15 +20,10 @@ use crate::payment::store::{ LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; -use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; use crate::types::{ChannelManager, PaymentStore}; - -use lightning::ln::bolt11_payment; -use lightning::ln::channelmanager::{ - Bolt11InvoiceParameters, PaymentId, RecipientOnionFields, Retry, RetryableSendFailure, -}; -use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::ln::channelmanager::{Bolt11InvoiceParameters, PaymentId, Retry}; +use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -90,10 +85,10 @@ impl Bolt11Payment { /// Send a payment given an invoice. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send( - &self, invoice: &Bolt11Invoice, sending_parameters: Option, + &self, invoice: &Bolt11Invoice, route_parameters: Option, ) -> Result { let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); @@ -101,11 +96,6 @@ impl Bolt11Payment { return Err(Error::NotRunning); } - let (payment_hash, recipient_onion, mut route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead."); - Error::InvalidInvoice - })?; - let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); if let Some(payment) = self.payment_store.get(&payment_id) { if payment.status == PaymentStatus::Pending @@ -116,32 +106,31 @@ impl Bolt11Payment { } } - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); + if let Some(payment) = self.payment_store.get(&payment_id) { + if payment.status == PaymentStatus::Pending + || payment.status == PaymentStatus::Succeeded + { + log_error!(self.logger, "Payment error: an invoice must not be paid twice."); + return Err(Error::DuplicatePayment); + } + } - let payment_secret = Some(*invoice.payment_secret()); + let route_parameters = + route_parameters.or(self.config.route_parameters).unwrap_or_default(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let payment_secret = Some(*invoice.payment_secret()); - match self.channel_manager.send_payment( - payment_hash, - recipient_onion, + match self.channel_manager.pay_for_bolt11_invoice( + invoice, payment_id, - route_params, + None, + route_parameters, retry_strategy, ) { Ok(()) => { + println!("Payment sent successfully."); let payee_pubkey = invoice.recover_payee_pub_key(); let amt_msat = invoice.amount_milli_satoshis().unwrap(); log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); @@ -164,30 +153,49 @@ impl Bolt11Payment { Ok(payment_id) }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - match e { - RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), - _ => { - let kind = PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: payment_secret, - }; - let payment = PaymentDetails::new( - payment_id, - kind, - invoice.amount_milli_satoshis(), - None, - PaymentDirection::Outbound, - PaymentStatus::Failed, - ); - - self.payment_store.insert(payment)?; - Err(Error::PaymentSendingFailed) - }, - } + Err(_) => { + println!("Failed to send payment due to the given invoice being invalid or incompatible."); + log_error!(self.logger, + "Failed to send payment due to the given invoice being invalid or incompatible." + ); + return Err(Error::ChannelClosingFailed); }, + // Err(Bolt11PaymentError::InvalidAmount) => { + // log_error!(self.logger, + // "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead." + // ); + // return Err(Error::InvalidInvoice); + // }, + // Err(Bolt11PaymentError::InvalidInvoice) => { + // log_error!(self.logger, + // "Failed to send payment due to the given invoice being invalid or incompatible." + // ); + // return Err(Error::InvalidInvoice); + // }, + // Err(Bolt11PaymentError::SendingFailed(e)) => { + // log_error!(self.logger, "Failed to send payment: {:?}", e); + // match e { + // RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + // _ => { + // let kind = PaymentKind::Bolt11 { + // hash: payment_hash, + // preimage: None, + // secret: payment_secret, + // }; + // let payment = PaymentDetails::new( + // payment_id, + // kind, + // invoice.amount_milli_satoshis(), + // None, + // PaymentDirection::Outbound, + // PaymentStatus::Failed, + // ); + + // self.payment_store.insert(payment)?; + // Err(Error::PaymentSendingFailed) + // }, + // } + // }, } } @@ -198,11 +206,11 @@ impl Bolt11Payment { /// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the /// amount paid to be determined by the user. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, - sending_parameters: Option, + route_parameters: Option, ) -> Result { let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); @@ -230,46 +238,16 @@ impl Bolt11Payment { } } - let payment_secret = invoice.payment_secret(); - let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); - let mut payment_params = PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), - invoice.min_final_cltv_expiry_delta() as u32, - ) - .with_expiry_time(expiry_time.as_secs()) - .with_route_hints(invoice.route_hints()) - .map_err(|_| Error::InvalidInvoice)?; - if let Some(features) = invoice.features() { - payment_params = payment_params - .with_bolt11_features(features.clone()) - .map_err(|_| Error::InvalidInvoice)?; - } - let mut route_params = - RouteParameters::from_payment_params_and_value(payment_params, amount_msat); - - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; - + let route_parameters = + route_parameters.or(self.config.route_parameters).unwrap_or_default(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let recipient_fields = RecipientOnionFields::secret_only(*payment_secret); + let payment_secret = Some(*invoice.payment_secret()); - match self.channel_manager.send_payment( - payment_hash, - recipient_fields, + match self.channel_manager.pay_for_bolt11_invoice( + invoice, payment_id, - route_params, + Some(amount_msat), + route_parameters, retry_strategy, ) { Ok(()) => { @@ -284,7 +262,7 @@ impl Bolt11Payment { let kind = PaymentKind::Bolt11 { hash: payment_hash, preimage: None, - secret: Some(*payment_secret), + secret: payment_secret, }; let payment = PaymentDetails::new( @@ -299,31 +277,49 @@ impl Bolt11Payment { Ok(payment_id) }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - - match e { - RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), - _ => { - let kind = PaymentKind::Bolt11 { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), - }; - let payment = PaymentDetails::new( - payment_id, - kind, - Some(amount_msat), - None, - PaymentDirection::Outbound, - PaymentStatus::Failed, - ); - self.payment_store.insert(payment)?; - - Err(Error::PaymentSendingFailed) - }, - } + Err(_) => { + log_error!(self.logger, + "Failed to send payment due to the given invoice being invalid or incompatible." + ); + return Err(Error::ChannelClosingFailed); }, + // Err(Bolt11PaymentError::InvalidAmount) => { + // log_error!( + // self.logger, + // "Failed to send payment due to amount given being insufficient." + // ); + // return Err(Error::InvalidInvoice); + // }, + // Err(Bolt11PaymentError::InvalidInvoice) => { + // log_error!(self.logger, + // "Failed to send payment due to the given invoice being invalid or incompatible." + // ); + // return Err(Error::InvalidInvoice); + // }, + // Err(Bolt11PaymentError::SendingFailed(e)) => { + // log_error!(self.logger, "Failed to send payment: {:?}", e); + // match e { + // RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + // _ => { + // let kind = PaymentKind::Bolt11 { + // hash: payment_hash, + // preimage: None, + // secret: payment_secret, + // }; + // let payment = PaymentDetails::new( + // payment_id, + // kind, + // Some(amount_msat), + // None, + // PaymentDirection::Outbound, + // PaymentStatus::Failed, + // ); + + // self.payment_store.insert(payment)?; + // Err(Error::PaymentSendingFailed) + // }, + // } + // }, } } @@ -572,16 +568,37 @@ impl Bolt11Payment { max_total_lsp_fee_limit_msat: Option, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + let (invoice, _) = self.receive_via_jit_channel_inner( Some(amount_msat), &description, expiry_secs, max_total_lsp_fee_limit_msat, None, + true, )?; Ok(maybe_wrap(invoice)) } + /// Returns a payable invoice for manual claiming via a JIT channel. + /// + /// Similar to `receive_via_jit_channel` but requires manual claiming via `claim_for_hash`. + pub fn receive_via_jit_channel_manual_claim( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, + ) -> Result<(Bolt11Invoice, PaymentPreimage), Error> { + let description = maybe_try_convert_enum(description)?; + let (invoice, preimage) = self.receive_via_jit_channel_inner( + Some(amount_msat), + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + false, + )?; + let preimage = preimage.ok_or(Error::InvoiceCreationFailed)?; + Ok((maybe_wrap(invoice), preimage)) + } + /// Returns a payable invoice that can be used to request a variable amount payment (also known /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. /// @@ -598,12 +615,13 @@ impl Bolt11Payment { max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { let description = maybe_try_convert_enum(description)?; - let invoice = self.receive_via_jit_channel_inner( + let (invoice, _) = self.receive_via_jit_channel_inner( None, &description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, + true, )?; Ok(maybe_wrap(invoice)) } @@ -611,8 +629,8 @@ impl Bolt11Payment { fn receive_via_jit_channel_inner( &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, - max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { + max_proportional_lsp_fee_limit_ppm_msat: Option, auto_claim: bool, + ) -> Result<(LdkBolt11Invoice, Option), Error> { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -675,9 +693,12 @@ impl Bolt11Payment { let id = PaymentId(payment_hash.0); let preimage = self.channel_manager.get_payment_preimage(payment_hash, payment_secret.clone()).ok(); + + let stored_preimage = if auto_claim { preimage } else { None }; + let kind = PaymentKind::Bolt11Jit { hash: payment_hash, - preimage, + preimage: stored_preimage, secret: Some(payment_secret.clone()), counterparty_skimmed_fee_msat: None, lsp_fee_limits, @@ -695,7 +716,7 @@ impl Bolt11Payment { // Persist LSP peer to make sure we reconnect on restart. self.peer_store.add_peer(peer_info)?; - Ok(invoice) + Ok((invoice, preimage)) } /// Sends payment probes over all paths of a route that would be used to pay the given invoice. @@ -711,18 +732,42 @@ impl Bolt11Payment { /// payment. To mitigate this issue, channels with available liquidity less than the required /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send /// pre-flight probes. - pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { + /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + pub fn send_probes( + &self, invoice: &Bolt11Invoice, route_parameters: Option, + ) -> Result<(), Error> { let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } - let (_payment_hash, _recipient_onion, route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + let payment_params = PaymentParameters::from_bolt11_invoice(invoice); + + let amount_msat = invoice.amount_milli_satoshis().ok_or_else(|| { log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_probes_using_amount instead."); Error::InvalidInvoice })?; + let mut route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); self.channel_manager @@ -741,9 +786,13 @@ impl Bolt11Payment { /// This can be used to send pre-flight probes for a so-called "zero-amount" invoice, i.e., an /// invoice that leaves the amount paid to be determined by the user. /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + /// /// See [`Self::send_probes`] for more information. pub fn send_probes_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, + route_parameters: Option, ) -> Result<(), Error> { let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); @@ -751,26 +800,36 @@ impl Bolt11Payment { return Err(Error::NotRunning); } - let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = - invoice.amount_milli_satoshis() - { + let payment_params = PaymentParameters::from_bolt11_invoice(invoice); + + if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { if amount_msat < invoice_amount_msat { log_error!( self.logger, - "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); + "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", + invoice_amount_msat, + amount_msat + ); return Err(Error::InvalidAmount); } + } - bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being \"zero-amount\"."); - Error::InvalidInvoice - })? - } else { - bolt11_payment::payment_parameters_from_variable_amount_invoice(&invoice, amount_msat).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); - Error::InvalidInvoice - })? - }; + let mut route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index b9efa3241..0b37f5c65 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -19,6 +19,7 @@ use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; +use lightning::routing::router::RouteParametersConfig; use lightning::util::string::UntrustedString; use rand::RngCore; @@ -82,7 +83,7 @@ impl Bolt12Payment { rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let offer_amount_msat = match offer.amount() { Some(Amount::Bitcoin { amount_msats }) => amount_msats, @@ -103,7 +104,7 @@ impl Bolt12Payment { payer_note.clone(), payment_id, retry_strategy, - max_total_routing_fee_msat, + route_params_config, ) { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); @@ -185,7 +186,7 @@ impl Bolt12Payment { rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let offer_amount_msat = match offer.amount() { Some(Amount::Bitcoin { amount_msats }) => amount_msats, @@ -210,7 +211,7 @@ impl Bolt12Payment { payer_note.clone(), payment_id, retry_strategy, - max_total_routing_fee_msat, + route_params_config, ) { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); @@ -392,7 +393,7 @@ impl Bolt12Payment { .duration_since(UNIX_EPOCH) .unwrap(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let mut refund_builder = self .channel_manager @@ -401,7 +402,7 @@ impl Bolt12Payment { absolute_expiry, payment_id, retry_strategy, - max_total_routing_fee_msat, + route_params_config, ) .map_err(|e| { log_error!(self.logger, "Failed to create refund builder: {:?}", e); diff --git a/src/payment/mod.rs b/src/payment/mod.rs index b031e37fd..54f7894dc 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -22,87 +22,3 @@ pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; pub use unified_qr::{QrPaymentResult, UnifiedQrPayment}; - -/// Represents information used to send a payment. -#[derive(Clone, Debug, PartialEq)] -pub struct SendingParameters { - /// The maximum total fees, in millisatoshi, that may accrue during route finding. - /// - /// This limit also applies to the total fees that may arise while retrying failed payment - /// paths. - /// - /// Note that values below a few sats may result in some paths being spuriously ignored. - #[cfg(not(feature = "uniffi"))] - pub max_total_routing_fee_msat: Option>, - /// The maximum total fees, in millisatoshi, that may accrue during route finding. - /// - /// This limit also applies to the total fees that may arise while retrying failed payment - /// paths. - /// - /// Note that values below a few sats may result in some paths being spuriously ignored. - #[cfg(feature = "uniffi")] - pub max_total_routing_fee_msat: Option, - /// The maximum total CLTV delta we accept for the route. - /// - /// Defaults to [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]. - /// - /// [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]: lightning::routing::router::DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA - pub max_total_cltv_expiry_delta: Option, - /// The maximum number of paths that may be used by (MPP) payments. - /// - /// Defaults to [`DEFAULT_MAX_PATH_COUNT`]. - /// - /// [`DEFAULT_MAX_PATH_COUNT`]: lightning::routing::router::DEFAULT_MAX_PATH_COUNT - pub max_path_count: Option, - /// Selects the maximum share of a channel's total capacity which will be sent over a channel, - /// as a power of 1/2. - /// - /// A higher value prefers to send the payment using more MPP parts whereas - /// a lower value prefers to send larger MPP parts, potentially saturating channels and - /// increasing failure probability for those paths. - /// - /// Note that this restriction will be relaxed during pathfinding after paths which meet this - /// restriction have been found. While paths which meet this criteria will be searched for, it - /// is ultimately up to the scorer to select them over other paths. - /// - /// Examples: - /// - /// | Value | Max Proportion of Channel Capacity Used | - /// |-------|-----------------------------------------| - /// | 0 | Up to 100% of the channel’s capacity | - /// | 1 | Up to 50% of the channel’s capacity | - /// | 2 | Up to 25% of the channel’s capacity | - /// | 3 | Up to 12.5% of the channel’s capacity | - /// - /// Default value: 2 - pub max_channel_saturation_power_of_half: Option, -} - -/// Represents the possible states of [`SendingParameters::max_total_routing_fee_msat`]. -// -// Required only in bindings as UniFFI can't expose `Option>`. -#[cfg(feature = "uniffi")] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum MaxTotalRoutingFeeLimit { - None, - Some { amount_msat: u64 }, -} - -#[cfg(feature = "uniffi")] -impl From for Option { - fn from(value: MaxTotalRoutingFeeLimit) -> Self { - match value { - MaxTotalRoutingFeeLimit::Some { amount_msat } => Some(amount_msat), - MaxTotalRoutingFeeLimit::None => None, - } - } -} - -#[cfg(feature = "uniffi")] -impl From> for MaxTotalRoutingFeeLimit { - fn from(value: Option) -> Self { - value.map_or(MaxTotalRoutingFeeLimit::None, |amount_msat| MaxTotalRoutingFeeLimit::Some { - amount_msat, - }) - } -} diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 1508b6cd8..821092e9d 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -11,11 +11,10 @@ use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; -use crate::payment::SendingParameters; use crate::types::{ChannelManager, CustomTlvRecord, KeysManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; -use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use lightning::sign::EntropySource; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -52,25 +51,26 @@ impl SpontaneousPayment { /// Send a spontaneous aka. "keysend", payment. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, None) + self.send_inner(amount_msat, node_id, route_parameters, None) } /// Send a spontaneous payment including a list of custom TLVs. pub fn send_with_custom_tlvs( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, - custom_tlvs: Vec, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, custom_tlvs: Vec, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs)) + self.send_inner(amount_msat, node_id, route_parameters, Some(custom_tlvs)) } fn send_inner( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, - custom_tlvs: Option>, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, custom_tlvs: Option>, ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { @@ -95,20 +95,19 @@ impl SpontaneousPayment { amount_msat, ); - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } let recipient_fields = match custom_tlvs { Some(tlvs) => RecipientOnionFields::spontaneous_empty() diff --git a/src/types.rs b/src/types.rs index 3103ead3f..659a4369a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -76,11 +76,8 @@ pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< pub(crate) type Broadcaster = crate::tx_broadcaster::TransactionBroadcaster>; -pub(crate) type Wallet = - crate::wallet::Wallet, Arc, Arc>; - -pub(crate) type KeysManager = - crate::wallet::WalletKeysManager, Arc, Arc>; +pub(crate) type Wallet = crate::wallet::Wallet; +pub(crate) type KeysManager = crate::wallet::WalletKeysManager; pub(crate) type Router = DefaultRouter< Arc, diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs new file mode 100644 index 000000000..a8d1421bb --- /dev/null +++ b/src/uniffi_types.rs @@ -0,0 +1,837 @@ +// 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. + +// Importing these items ensures they are accessible in the uniffi bindings +// without introducing unused import warnings in lib.rs. +// +// Make sure to add any re-exported items that need to be used in uniffi below. + +pub use crate::config::{ + default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, + EsploraSyncConfig, MaxDustHTLCExposure, +}; +pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; +pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig}; +pub use crate::logger::{LogLevel, LogRecord, LogWriter}; +pub use crate::payment::store::{ + ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, +}; +pub use crate::payment::QrPaymentResult; + +pub use lightning::chain::channelmonitor::BalanceSource; +pub use lightning::events::{ClosureReason, PaymentFailureReason}; +pub use lightning::ln::types::ChannelId; +pub use lightning::offers::invoice::Bolt12Invoice; +pub use lightning::offers::offer::{Offer, OfferId}; +pub use lightning::offers::refund::Refund; +pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; +pub use lightning::routing::router::RouteParametersConfig; +pub use lightning::util::string::UntrustedString; + +pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; + +pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; + +pub use lightning_liquidity::lsps0::ser::LSPSDateTime; +pub use lightning_liquidity::lsps1::msgs::{ + LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState, +}; + +pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; + +pub use bip39::Mnemonic; + +pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; + +use crate::UniffiCustomTypeConverter; + +use crate::builder::sanitize_alias; +use crate::error::Error; +use crate::hex_utils; +use crate::{SocketAddress, UserChannelId}; + +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +use lightning::ln::channelmanager::PaymentId; +use lightning::util::ser::Writeable; +use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; + +use std::convert::TryInto; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +impl UniffiCustomTypeConverter for PublicKey { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(key) = PublicKey::from_str(&val) { + return Ok(key); + } + + Err(Error::InvalidPublicKey.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for NodeId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(key) = NodeId::from_str(&val) { + return Ok(key); + } + + Err(Error::InvalidNodeId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Address { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(addr) = Address::from_str(&val) { + return Ok(addr.assume_checked()); + } + + Err(Error::InvalidAddress.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Offer { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Offer::from_str(&val).map_err(|_| Error::InvalidOffer.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Refund { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Refund::from_str(&val).map_err(|_| Error::InvalidRefund.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Bolt12Invoice { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + if let Ok(invoice) = Bolt12Invoice::try_from(bytes_vec) { + return Ok(invoice); + } + } + Err(Error::InvalidInvoice.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.encode()) + } +} + +impl UniffiCustomTypeConverter for OfferId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(OfferId(bytes)); + } + } + Err(Error::InvalidOfferId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for PaymentId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentId(bytes)); + } + } + Err(Error::InvalidPaymentId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for PaymentHash { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(hash) = Sha256::from_str(&val) { + Ok(PaymentHash(hash.to_byte_array())) + } else { + Err(Error::InvalidPaymentHash.into()) + } + } + + fn from_custom(obj: Self) -> Self::Builtin { + Sha256::from_slice(&obj.0).unwrap().to_string() + } +} + +impl UniffiCustomTypeConverter for PaymentPreimage { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentPreimage(bytes)); + } + } + Err(Error::InvalidPaymentPreimage.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for PaymentSecret { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentSecret(bytes)); + } + } + Err(Error::InvalidPaymentSecret.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for ChannelId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(hex_vec) = hex_utils::to_vec(&val) { + if hex_vec.len() == 32 { + let mut channel_id = [0u8; 32]; + channel_id.copy_from_slice(&hex_vec[..]); + return Ok(Self(channel_id)); + } + } + Err(Error::InvalidChannelId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for UserChannelId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(UserChannelId(u128::from_str(&val).map_err(|_| Error::InvalidChannelId)?)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.0.to_string() + } +} + +impl UniffiCustomTypeConverter for Txid { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Txid::from_str(&val)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for BlockHash { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(BlockHash::from_str(&val)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Mnemonic { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Mnemonic::from_str(&val).map_err(|_| Error::InvalidSecretKey)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for SocketAddress { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(SocketAddress::from_str(&val).map_err(|_| Error::InvalidSocketAddress)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for UntrustedString { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(UntrustedString(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for NodeAlias { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(sanitize_alias(&val).map_err(|_| Error::InvalidNodeAlias)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +/// Represents the description of an invoice which has to be either a directly included string or +/// a hash of a description provided out of band. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Bolt11InvoiceDescription { + /// Contains a full description. + Direct { + /// Description of what the invoice is for + description: String, + }, + /// Contains a hash. + Hash { + /// Hash of the description of what the invoice is for + hash: String, + }, +} + +impl TryFrom<&Bolt11InvoiceDescription> for lightning_invoice::Bolt11InvoiceDescription { + type Error = Error; + + fn try_from(value: &Bolt11InvoiceDescription) -> Result { + match value { + Bolt11InvoiceDescription::Direct { description } => { + Description::new(description.clone()) + .map(lightning_invoice::Bolt11InvoiceDescription::Direct) + .map_err(|_| Error::InvoiceCreationFailed) + }, + Bolt11InvoiceDescription::Hash { hash } => Sha256::from_str(&hash) + .map(lightning_invoice::Sha256) + .map(lightning_invoice::Bolt11InvoiceDescription::Hash) + .map_err(|_| Error::InvoiceCreationFailed), + } + } +} + +impl From for Bolt11InvoiceDescription { + fn from(value: lightning_invoice::Bolt11InvoiceDescription) -> Self { + match value { + lightning_invoice::Bolt11InvoiceDescription::Direct(description) => { + Bolt11InvoiceDescription::Direct { description: description.to_string() } + }, + lightning_invoice::Bolt11InvoiceDescription::Hash(hash) => { + Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) } + }, + } + } +} + +impl<'a> From> for Bolt11InvoiceDescription { + fn from(value: Bolt11InvoiceDescriptionRef<'a>) -> Self { + match value { + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(description) => { + Bolt11InvoiceDescription::Direct { description: description.to_string() } + }, + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(hash) => { + Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) } + }, + } + } +} + +/// Enum representing the crypto currencies (or networks) supported by this library +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Currency { + /// Bitcoin mainnet + Bitcoin, + + /// Bitcoin testnet + BitcoinTestnet, + + /// Bitcoin regtest + Regtest, + + /// Bitcoin simnet + Simnet, + + /// Bitcoin signet + Signet, +} + +impl From for Currency { + fn from(currency: lightning_invoice::Currency) -> Self { + match currency { + lightning_invoice::Currency::Bitcoin => Currency::Bitcoin, + lightning_invoice::Currency::BitcoinTestnet => Currency::BitcoinTestnet, + lightning_invoice::Currency::Regtest => Currency::Regtest, + lightning_invoice::Currency::Simnet => Currency::Simnet, + lightning_invoice::Currency::Signet => Currency::Signet, + } + } +} + +/// A channel descriptor for a hop along a payment path. +/// +/// While this generally comes from BOLT 11's `r` field, this struct includes more fields than are +/// available in BOLT 11. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RouteHintHop { + /// The node_id of the non-target end of the route + pub src_node_id: PublicKey, + /// The short_channel_id of this channel + pub short_channel_id: u64, + /// The fees which must be paid to use this channel + pub fees: RoutingFees, + /// The difference in CLTV values between this node and the next node. + pub cltv_expiry_delta: u16, + /// The minimum value, in msat, which must be relayed to the next hop. + pub htlc_minimum_msat: Option, + /// The maximum value in msat available for routing with a single HTLC. + pub htlc_maximum_msat: Option, +} + +impl From for RouteHintHop { + fn from(hop: lightning::routing::router::RouteHintHop) -> Self { + Self { + src_node_id: hop.src_node_id, + short_channel_id: hop.short_channel_id, + cltv_expiry_delta: hop.cltv_expiry_delta, + htlc_minimum_msat: hop.htlc_minimum_msat, + htlc_maximum_msat: hop.htlc_maximum_msat, + fees: hop.fees, + } + } +} + +/// Represents a syntactically and semantically correct lightning BOLT11 invoice. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bolt11Invoice { + pub inner: LdkBolt11Invoice, +} + +impl Bolt11Invoice { + pub fn from_str(invoice_str: &str) -> Result { + invoice_str.parse() + } + + /// Returns the underlying invoice [`LdkBolt11Invoice`] + pub fn into_inner(self) -> LdkBolt11Invoice { + self.inner + } + + /// The hash of the [`RawBolt11Invoice`] that was signed. + /// + /// [`RawBolt11Invoice`]: lightning_invoice::RawBolt11Invoice + pub fn signable_hash(&self) -> Vec { + self.inner.signable_hash().to_vec() + } + + /// Returns the hash to which we will receive the preimage on completion of the payment + pub fn payment_hash(&self) -> PaymentHash { + PaymentHash(self.inner.payment_hash().to_byte_array()) + } + + /// Get the payment secret if one was included in the invoice + pub fn payment_secret(&self) -> PaymentSecret { + PaymentSecret(self.inner.payment_secret().0) + } + + /// Returns the amount if specified in the invoice as millisatoshis. + pub fn amount_milli_satoshis(&self) -> Option { + self.inner.amount_milli_satoshis() + } + + /// Returns the invoice's expiry time (in seconds), if present, otherwise [`DEFAULT_EXPIRY_TIME`]. + /// + /// [`DEFAULT_EXPIRY_TIME`]: lightning_invoice::DEFAULT_EXPIRY_TIME + pub fn expiry_time_seconds(&self) -> u64 { + self.inner.expiry_time().as_secs() + } + + /// Returns the `Bolt11Invoice`'s timestamp as seconds since the Unix epoch + pub fn seconds_since_epoch(&self) -> u64 { + self.inner.duration_since_epoch().as_secs() + } + + /// Returns the seconds remaining until the invoice expires. + pub fn seconds_until_expiry(&self) -> u64 { + self.inner.duration_until_expiry().as_secs() + } + + /// Returns whether the invoice has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// Returns whether the expiry time would pass at the given point in time. + /// `at_time_seconds` is the timestamp as seconds since the Unix epoch. + pub fn would_expire(&self, at_time_seconds: u64) -> bool { + self.inner.would_expire(Duration::from_secs(at_time_seconds)) + } + + /// Return the description or a hash of it for longer ones + pub fn description(&self) -> Bolt11InvoiceDescription { + self.inner.description().into() + } + + /// Returns the invoice's `min_final_cltv_expiry_delta` time, if present, otherwise + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]. + /// + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]: lightning_invoice::DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA + pub fn min_final_cltv_expiry_delta(&self) -> u64 { + self.inner.min_final_cltv_expiry_delta() + } + + /// Returns the network for which the invoice was issued + pub fn network(&self) -> Network { + self.inner.network() + } + + /// Returns the currency for which the invoice was issued + pub fn currency(&self) -> Currency { + self.inner.currency().into() + } + + /// Returns a list of all fallback addresses as [`Address`]es + pub fn fallback_addresses(&self) -> Vec
{ + self.inner.fallback_addresses() + } + + /// Returns a list of all routes included in the invoice as the underlying hints + pub fn route_hints(&self) -> Vec> { + self.inner + .route_hints() + .iter() + .map(|route| route.0.iter().map(|hop| RouteHintHop::from(hop.clone())).collect()) + .collect() + } + + /// Recover the payee's public key (only to be used if none was included in the invoice) + pub fn recover_payee_pub_key(&self) -> PublicKey { + self.inner.recover_payee_pub_key() + } +} + +impl std::str::FromStr for Bolt11Invoice { + type Err = Error; + + fn from_str(invoice_str: &str) -> Result { + match invoice_str.parse::() { + Ok(signed) => match LdkBolt11Invoice::from_signed(signed) { + Ok(invoice) => Ok(Bolt11Invoice { inner: invoice }), + Err(_) => Err(Error::InvalidInvoice), + }, + Err(_) => Err(Error::InvalidInvoice), + } + } +} + +impl From for Bolt11Invoice { + fn from(invoice: LdkBolt11Invoice) -> Self { + Bolt11Invoice { inner: invoice } + } +} + +impl From for LdkBolt11Invoice { + fn from(wrapper: Bolt11Invoice) -> Self { + wrapper.into_inner() + } +} + +impl std::fmt::Display for Bolt11Invoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +/// Details regarding how to pay for an order. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1PaymentInfo { + /// A Lightning payment using BOLT 11. + pub bolt11: Option, + /// An onchain payment. + pub onchain: Option, +} + +impl From for LSPS1PaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo) -> Self { + LSPS1PaymentInfo { + bolt11: value.bolt11.map(|b| b.into()), + onchain: value.onchain.map(|o| o.into()), + } + } +} + +/// A Lightning payment using BOLT 11. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1Bolt11PaymentInfo { + /// Indicates the current state of the payment. + pub state: LSPS1PaymentState, + /// The datetime when the payment option expires. + pub expires_at: LSPSDateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// A BOLT11 invoice the client can pay to have to channel opened. + pub invoice: Arc, +} + +impl From for LSPS1Bolt11PaymentInfo { + fn from(info: lightning_liquidity::lsps1::msgs::LSPS1Bolt11PaymentInfo) -> Self { + Self { + state: info.state, + expires_at: info.expires_at, + fee_total_sat: info.fee_total_sat, + order_total_sat: info.order_total_sat, + invoice: Arc::new(info.invoice.into()), + } + } +} + +/// An onchain payment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1OnchainPaymentInfo { + /// Indicates the current state of the payment. + pub state: lightning_liquidity::lsps1::msgs::LSPS1PaymentState, + /// The datetime when the payment option expires. + pub expires_at: LSPSDateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel + /// opened. + pub address: bitcoin::Address, + /// The minimum number of block confirmations that are required for the on-chain payment to be + /// considered confirmed. + pub min_onchain_payment_confirmations: Option, + /// The minimum fee rate for the on-chain payment in case the client wants the payment to be + /// confirmed without a confirmation. + pub min_fee_for_0conf: Arc, + /// The address where the LSP will send the funds if the order fails. + pub refund_onchain_address: Option, +} + +impl From for LSPS1OnchainPaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::LSPS1OnchainPaymentInfo) -> Self { + Self { + state: value.state, + expires_at: value.expires_at, + fee_total_sat: value.fee_total_sat, + order_total_sat: value.order_total_sat, + address: value.address, + min_onchain_payment_confirmations: value.min_onchain_payment_confirmations, + min_fee_for_0conf: Arc::new(value.min_fee_for_0conf), + refund_onchain_address: value.refund_onchain_address, + } + } +} + +impl UniffiCustomTypeConverter for LSPS1OrderId { + 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 LSPSDateTime { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(LSPSDateTime::from_str(&val).map_err(|_| Error::InvalidDateTime)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_rfc3339() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn create_test_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { + let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; + let ldk_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap(); + let wrapped_invoice = Bolt11Invoice::from(ldk_invoice.clone()); + (ldk_invoice, wrapped_invoice) + } + + #[test] + fn test_invoice_description_conversion() { + let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); + let description = Bolt11InvoiceDescription::Hash { hash }; + let converted_description = + lightning_invoice::Bolt11InvoiceDescription::try_from(&description).unwrap(); + let reconverted_description: Bolt11InvoiceDescription = converted_description.into(); + assert_eq!(description, reconverted_description); + } + + #[test] + fn test_bolt11_invoice_basic_properties() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + assert_eq!( + ldk_invoice.payment_hash().to_string(), + wrapped_invoice.payment_hash().to_string() + ); + assert_eq!(ldk_invoice.amount_milli_satoshis(), wrapped_invoice.amount_milli_satoshis()); + + assert_eq!( + ldk_invoice.min_final_cltv_expiry_delta(), + wrapped_invoice.min_final_cltv_expiry_delta() + ); + assert_eq!( + ldk_invoice.payment_secret().0.to_vec(), + wrapped_invoice.payment_secret().0.to_vec() + ); + + assert_eq!(ldk_invoice.network(), wrapped_invoice.network()); + assert_eq!( + format!("{:?}", ldk_invoice.currency()), + format!("{:?}", wrapped_invoice.currency()) + ); + } + + #[test] + fn test_bolt11_invoice_time_related_fields() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + assert_eq!(ldk_invoice.expiry_time().as_secs(), wrapped_invoice.expiry_time_seconds()); + assert_eq!( + ldk_invoice.duration_until_expiry().as_secs(), + wrapped_invoice.seconds_until_expiry() + ); + assert_eq!( + ldk_invoice.duration_since_epoch().as_secs(), + wrapped_invoice.seconds_since_epoch() + ); + + let future_time = Duration::from_secs(wrapped_invoice.seconds_since_epoch() + 10000); + assert!(!ldk_invoice.would_expire(future_time)); + assert!(!wrapped_invoice.would_expire(future_time.as_secs())); + } + + #[test] + fn test_bolt11_invoice_description() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let ldk_description = ldk_invoice.description(); + let wrapped_description = wrapped_invoice.description(); + + match (ldk_description, &wrapped_description) { + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(ldk_description), + Bolt11InvoiceDescription::Direct { description }, + ) => { + assert_eq!(ldk_description.to_string(), *description) + }, + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(ldk_hash), + Bolt11InvoiceDescription::Hash { hash }, + ) => { + assert_eq!(hex_utils::to_string(ldk_hash.0.as_ref()), *hash) + }, + _ => panic!("Description types don't match"), + } + } + + #[test] + fn test_bolt11_invoice_route_hints() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let wrapped_route_hints = wrapped_invoice.route_hints(); + let ldk_route_hints = ldk_invoice.route_hints(); + assert_eq!(ldk_route_hints.len(), wrapped_route_hints.len()); + + let ldk_hop = &ldk_route_hints[0].0[0]; + let wrapped_hop = &wrapped_route_hints[0][0]; + assert_eq!(ldk_hop.src_node_id, wrapped_hop.src_node_id); + assert_eq!(ldk_hop.short_channel_id, wrapped_hop.short_channel_id); + assert_eq!(ldk_hop.cltv_expiry_delta, wrapped_hop.cltv_expiry_delta); + assert_eq!(ldk_hop.htlc_minimum_msat, wrapped_hop.htlc_minimum_msat); + assert_eq!(ldk_hop.htlc_maximum_msat, wrapped_hop.htlc_maximum_msat); + assert_eq!(ldk_hop.fees.base_msat, wrapped_hop.fees.base_msat); + assert_eq!(ldk_hop.fees.proportional_millionths, wrapped_hop.fees.proportional_millionths); + } + + #[test] + fn test_bolt11_invoice_roundtrip() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let invoice_str = wrapped_invoice.to_string(); + let parsed_invoice: LdkBolt11Invoice = invoice_str.parse().unwrap(); + assert_eq!( + ldk_invoice.payment_hash().to_byte_array().to_vec(), + parsed_invoice.payment_hash().to_byte_array().to_vec() + ); + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index fbac1d1b6..8b53138a8 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -8,12 +8,12 @@ use persist::KVStoreWalletPersister; use crate::config::Config; -use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger}; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; -use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; +use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; use crate::payment::store::ConfirmationStatus; use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; -use crate::types::PaymentStore; +use crate::types::{Broadcaster, PaymentStore}; use crate::Error; use lightning::chain::chaininterface::BroadcasterInterface; @@ -23,11 +23,11 @@ use lightning::chain::{BestBlock, Listen}; use lightning::events::bump_transaction::{Utxo, WalletSource}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::inbound_payment::ExpandedKey; -use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; +use lightning::ln::msgs::UnsignedGossipMessage; use lightning::ln::script::ShutdownScript; use lightning::sign::{ - ChangeDestinationSource, EntropySource, InMemorySigner, KeysManager, NodeSigner, OutputSpender, - Recipient, SignerProvider, SpendableOutputDescriptor, + ChangeDestinationSource, ChangeDestinationSourceSync, EntropySource, InMemorySigner, + KeysManager, NodeSigner, OutputSpender, Recipient, SignerProvider, SpendableOutputDescriptor, }; use lightning::util::message_signing; @@ -44,13 +44,14 @@ use bitcoin::key::XOnlyPublicKey; use bitcoin::psbt::Psbt; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; +use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::{ Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, WitnessProgram, WitnessVersion, }; -use std::ops::Deref; +use std::future::Future; +use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -63,32 +64,23 @@ pub(crate) enum OnchainSendAmount { pub(crate) mod persist; pub(crate) mod ser; -pub(crate) struct Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +pub(crate) struct Wallet { // A BDK on-chain wallet. inner: Mutex>, persister: Mutex, - broadcaster: B, - fee_estimator: E, + broadcaster: Arc, + fee_estimator: Arc, payment_store: Arc, config: Arc, - logger: L, + logger: Arc, } -impl Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl Wallet { pub(crate) fn new( wallet: bdk_wallet::PersistedWallet, - wallet_persister: KVStoreWalletPersister, broadcaster: B, fee_estimator: E, - payment_store: Arc, config: Arc, logger: L, + wallet_persister: KVStoreWalletPersister, broadcaster: Arc, + fee_estimator: Arc, payment_store: Arc, + config: Arc, logger: Arc, ) -> Self { let inner = Mutex::new(wallet); let persister = Mutex::new(wallet_persister); @@ -570,12 +562,7 @@ where } } -impl Listen for Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl Listen for Wallet { fn filtered_block_connected( &self, _header: &bitcoin::block::Header, _txdata: &lightning::chain::transaction::TransactionData, _height: u32, @@ -635,12 +622,7 @@ where } } -impl WalletSource for Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl WalletSource for Wallet { fn list_confirmed_utxos(&self) -> Result, ()> { let locked_wallet = self.inner.lock().unwrap(); let mut utxos = Vec::new(); @@ -777,30 +759,20 @@ where /// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are /// directly spendable by the BDK wallet. -pub(crate) struct WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +pub(crate) struct WalletKeysManager { inner: KeysManager, - wallet: Arc>, - logger: L, + wallet: Arc, + logger: Arc, } -impl WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl WalletKeysManager { /// Constructs a `WalletKeysManager` that overrides the destination and shutdown scripts. /// /// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, and /// `starting_time_nanos`. pub fn new( - seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, - wallet: Arc>, logger: L, + seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, wallet: Arc, + logger: Arc, ) -> Self { let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos); Self { inner, wallet, logger } @@ -819,12 +791,7 @@ where } } -impl NodeSigner for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl NodeSigner for WalletKeysManager { fn get_node_id(&self, recipient: Recipient) -> Result { self.inner.get_node_id(recipient) } @@ -856,17 +823,12 @@ where } } -impl OutputSpender for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl OutputSpender for WalletKeysManager { /// See [`KeysManager::spend_spendable_outputs`] for documentation on this method. - fn spend_spendable_outputs( + fn spend_spendable_outputs( &self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: ScriptBuf, feerate_sat_per_1000_weight: u32, - locktime: Option, secp_ctx: &Secp256k1, + locktime: Option, secp_ctx: &Secp256k1, ) -> Result { self.inner.spend_spendable_outputs( descriptors, @@ -879,39 +841,21 @@ where } } -impl EntropySource for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl EntropySource for WalletKeysManager { fn get_secure_random_bytes(&self) -> [u8; 32] { self.inner.get_secure_random_bytes() } } -impl SignerProvider for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl SignerProvider for WalletKeysManager { type EcdsaSigner = InMemorySigner; - fn generate_channel_keys_id( - &self, inbound: bool, channel_value_satoshis: u64, user_channel_id: u128, - ) -> [u8; 32] { - self.inner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id) + fn generate_channel_keys_id(&self, inbound: bool, user_channel_id: u128) -> [u8; 32] { + self.inner.generate_channel_keys_id(inbound, user_channel_id) } - fn derive_channel_signer( - &self, channel_value_satoshis: u64, channel_keys_id: [u8; 32], - ) -> Self::EcdsaSigner { - self.inner.derive_channel_signer(channel_value_satoshis, channel_keys_id) - } - - fn read_chan_signer(&self, reader: &[u8]) -> Result { - self.inner.read_chan_signer(reader) + fn derive_channel_signer(&self, channel_keys_id: [u8; 32]) -> Self::EcdsaSigner { + self.inner.derive_channel_signer(channel_keys_id) } fn get_destination_script(&self, _channel_keys_id: [u8; 32]) -> Result { @@ -941,12 +885,7 @@ where } } -impl ChangeDestinationSource for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl ChangeDestinationSourceSync for WalletKeysManager { fn get_change_destination_script(&self) -> Result { let address = self.wallet.get_new_internal_address().map_err(|e| { log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); @@ -954,3 +893,21 @@ where Ok(address.script_pubkey()) } } + +impl ChangeDestinationSource for WalletKeysManager { + fn get_change_destination_script<'a>( + &self, + ) -> Pin> + Send + 'a>> { + let wallet = Arc::clone(&self.wallet); + let logger = Arc::clone(&self.logger); + Box::pin(async move { + wallet + .get_new_internal_address() + .map_err(|e| { + log_error!(logger, "Failed to retrieve new address from wallet: {}", e); + }) + .map(|addr| addr.script_pubkey()) + .map_err(|_| ()) + }) + } +} diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index db48eca23..0023e8e8a 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -20,25 +20,30 @@ use ldk_node::config::EsploraSyncConfig; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, - SendingParameters, }; use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; +use lightning::routing::router::RouteParametersConfig; use lightning::util::persist::KVStore; use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use bitcoin::address::NetworkUnchecked; +use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; + +use bitcoin::address::NetworkUnchecked; use bitcoin::Address; use bitcoin::Amount; +use lightning_types::payment::PaymentHash; use log::LevelFilter; use std::str::FromStr; use std::sync::Arc; +use crate::common::expect_payment_claimable_event; + #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -200,11 +205,11 @@ fn multi_hop_sending() { // Sleep a bit for gossip to propagate. std::thread::sleep(std::time::Duration::from_secs(1)); - let sending_params = SendingParameters { - max_total_routing_fee_msat: Some(Some(75_000).into()), - max_total_cltv_expiry_delta: Some(1000), - max_path_count: Some(10), - max_channel_saturation_power_of_half: Some(2), + let route_params = RouteParametersConfig { + max_total_routing_fee_msat: Some(75_000), + max_total_cltv_expiry_delta: 1000, + max_path_count: 10, + max_channel_saturation_power_of_half: 2, }; let invoice_description = @@ -213,7 +218,7 @@ fn multi_hop_sending() { .bolt11_payment() .receive(2_500_000, &invoice_description.clone().into(), 9217) .unwrap(); - nodes[0].bolt11_payment().send(&invoice, Some(sending_params)).unwrap(); + nodes[0].bolt11_payment().send(&invoice, Some(route_params)).unwrap(); expect_event!(nodes[1], PaymentForwarded); @@ -1258,6 +1263,7 @@ fn lsps2_client_service_integration() { min_channel_lifetime: 100, min_channel_opening_fee_msat: 0, max_client_to_self_delay: 1024, + client_trusts_lsp: false, }; let service_config = random_config(true); @@ -1381,3 +1387,254 @@ fn facade_logging() { validate_log_entry(entry); } } + +#[test] +fn lsps2_client_trusts_lsp() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let sync_config = EsploraSyncConfig { background_sync_config: None }; + + // 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, + client_trusts_lsp: true, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build().unwrap(); + service_node.start().unwrap(); + + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build().unwrap(); + payer_node.start().unwrap(); + + let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); + let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); + let payer_addr_onchain = 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_onchain, client_addr_onchain, payer_addr_onchain], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + println!("Premine complete!"); + // Open a channel payer -> service that will allow paying the JIT invoice + 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 initial_mempool_size = bitcoind.client.get_raw_mempool().unwrap().0.len(); + + 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, preimage) = client_node + .bolt11_payment() + .receive_via_jit_channel_manual_claim( + 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(); + println!("Payment ID: {:?}", payment_id); + 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()); + println!("Try to find funding tx... It won't be found yet, as the client has not claimed it."); + let mut funding_tx_found = false; + for _ in 0..50 { + std::thread::sleep(std::time::Duration::from_millis(100)); + let current_mempool = bitcoind.client.get_raw_mempool().unwrap(); + if current_mempool.0.len() > initial_mempool_size { + funding_tx_found = true; + break; + } + } + assert!(!funding_tx_found, "Funding transaction should NOT be broadcast yet"); + 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; + + let manual_payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); + let _ = expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + + client_node + .bolt11_payment() + .claim_for_hash(manual_payment_hash, jit_amount_msat, preimage) + .unwrap(); + + expect_payment_successful_event!(payer_node, Some(payment_id), None); + + let _ = expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + + println!("Waiting for funding transaction to be broadcast..."); + let mut funding_tx_found = false; + for _ in 0..500 { + std::thread::sleep(std::time::Duration::from_millis(100)); + let current_mempool = bitcoind.client.get_raw_mempool().unwrap(); + if current_mempool.0.len() > initial_mempool_size { + funding_tx_found = true; + break; + } + } + + assert!(funding_tx_found, "Funding transaction should be broadcast after the client claims it"); +} + +#[test] +fn lsps2_lsp_trusts_client_but_client_does_not_claim() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let sync_config = EsploraSyncConfig { background_sync_config: None }; + + // 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, + client_trusts_lsp: false, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build().unwrap(); + service_node.start().unwrap(); + + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr.clone(), None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build().unwrap(); + payer_node.start().unwrap(); + + let service_addr_onchain = service_node.onchain_payment().new_address().unwrap(); + let client_addr_onchain = client_node.onchain_payment().new_address().unwrap(); + let payer_addr_onchain = 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_onchain, client_addr_onchain, payer_addr_onchain], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + println!("Premine complete!"); + // Open a channel payer -> service that will allow paying the JIT invoice + 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 initial_mempool_size = bitcoind.client.get_raw_mempool().unwrap().0.len(); + + 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_manual_claim( + 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()); + println!("Waiting for funding transaction to be broadcast... It will be there because LSP trusts the client, even though the client has not claimed it yet."); + let mut funding_tx_found = false; + for _ in 0..500 { + std::thread::sleep(std::time::Duration::from_millis(100)); + let current_mempool = bitcoind.client.get_raw_mempool().unwrap(); + if current_mempool.0.len() > initial_mempool_size { + funding_tx_found = true; + break; + } + } + assert!(funding_tx_found, "Funding transaction should be broadcast"); +}