Skip to main content

fedimint_lightning/
ldk.rs

1use std::collections::{BTreeMap, HashMap};
2use std::path::Path;
3use std::str::FromStr;
4use std::sync::Arc;
5use std::time::{Duration, UNIX_EPOCH};
6
7use async_trait::async_trait;
8use bitcoin::hashes::{Hash, sha256};
9use bitcoin::{FeeRate, Network, OutPoint};
10use fedimint_bip39::Mnemonic;
11use fedimint_core::envs::is_running_in_test_env;
12use fedimint_core::task::{TaskGroup, TaskHandle, block_in_place};
13use fedimint_core::util::{FmtCompact, SafeUrl};
14use fedimint_core::{Amount, BitcoinAmountOrAll, crit};
15use fedimint_gateway_common::{
16    ChainSource, ConnectPeerRequest, GetInvoiceRequest, GetInvoiceResponse,
17    ListTransactionsResponse, NodeAddress, SetChannelFeesRequest,
18};
19use fedimint_ln_common::contracts::Preimage;
20use fedimint_logging::{LOG_LIGHTNING, LOG_LIGHTNING_LDK};
21use ldk_node::config::ChannelConfig;
22use ldk_node::lightning::ln::msgs::SocketAddress;
23use ldk_node::lightning::routing::gossip::{NodeAlias, NodeId};
24use ldk_node::logger::{LogLevel, LogRecord, LogWriter};
25use ldk_node::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
26use lightning::ln::channelmanager::PaymentId;
27use lightning::offers::offer::{Offer, OfferId};
28use lightning::routing::router::RouteParametersConfig;
29use lightning::types::payment::{PaymentHash, PaymentPreimage};
30use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description};
31use thiserror::Error;
32use tokio::sync::mpsc::Sender;
33use tokio::sync::{RwLock, oneshot};
34use tokio_stream::wrappers::ReceiverStream;
35use tracing::{debug, error, info, trace, warn};
36
37use super::{ChannelInfo, ILnRpcClient, LightningRpcError, ListChannelsResponse, RouteHtlcStream};
38use crate::{
39    CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
40    CreateInvoiceResponse, GetBalancesResponse, GetLnOnchainAddressResponse, GetNodeInfoResponse,
41    GetRouteHintsResponse, InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription,
42    NO_INCOMING_CIRCUIT, OpenChannelRequest, OpenChannelResponse, PayInvoiceResponse,
43    PaymentAction, SendOnchainRequest, SendOnchainResponse,
44};
45
46/// Forwards `ldk-node`'s log records into the gateway's `tracing` subscriber.
47///
48/// By default `ldk-node` writes to its own append-only `ldk_node/ldk_node.log`
49/// file, which is invisible to stdout/stderr log collectors and grows without
50/// bound. Routing the records through `tracing` (under the
51/// [`LOG_LIGHTNING_LDK`] target) puts them alongside the rest of gatewayd's
52/// logs and makes them filterable via `RUST_LOG`.
53struct LdkTracingLogger {
54    /// Whether we're running under devimint/tests. When set, some benign LDK
55    /// error logs that are expected in regtest are downgraded to avoid spamming
56    /// the test output. See [`Self::downgraded_level`].
57    in_test_env: bool,
58}
59
60impl LdkTracingLogger {
61    /// Returns the level to emit `record` at, downgrading benign-but-noisy LDK
62    /// errors when running under devimint/tests.
63    ///
64    /// In regtest there is no fee-rate history, so `ldk-node` logs "Failed to
65    /// retrieve fee rate estimates ... Falling back to default" at `Error` on
66    /// essentially every sync. This is harmless (LDK falls back to a default
67    /// feerate), so in test environments we emit it at `Debug` instead. In
68    /// production the original `Error` level is preserved, since a persistent
69    /// failure there can indicate a real problem.
70    fn downgraded_level(&self, record: &LogRecord<'_>) -> LogLevel {
71        if self.in_test_env
72            && record.level == LogLevel::Error
73            && record.module_path == "ldk_node::chain"
74            && format!("{}", record.args).contains("Failed to retrieve fee rate estimates")
75        {
76            LogLevel::Debug
77        } else {
78            record.level
79        }
80    }
81}
82
83impl LogWriter for LdkTracingLogger {
84    fn log(&self, record: LogRecord<'_>) {
85        // `tracing` requires a static level per call-site, so match each LDK level.
86        match self.downgraded_level(&record) {
87            LogLevel::Gossip | LogLevel::Trace => trace!(
88                target: LOG_LIGHTNING_LDK,
89                ldk_module = record.module_path, line = record.line, "{}", record.args,
90            ),
91            LogLevel::Debug => debug!(
92                target: LOG_LIGHTNING_LDK,
93                ldk_module = record.module_path, line = record.line, "{}", record.args,
94            ),
95            LogLevel::Info => info!(
96                target: LOG_LIGHTNING_LDK,
97                ldk_module = record.module_path, line = record.line, "{}", record.args,
98            ),
99            LogLevel::Warn => warn!(
100                target: LOG_LIGHTNING_LDK,
101                ldk_module = record.module_path, line = record.line, "{}", record.args,
102            ),
103            LogLevel::Error => error!(
104                target: LOG_LIGHTNING_LDK,
105                ldk_module = record.module_path, line = record.line, "{}", record.args,
106            ),
107        }
108    }
109}
110
111pub struct GatewayLdkClient {
112    /// The underlying lightning node.
113    node: Arc<ldk_node::Node>,
114
115    task_group: TaskGroup,
116
117    /// The HTLC stream, until it is taken by calling
118    /// `ILnRpcClient::route_htlcs`.
119    htlc_stream_receiver_or: Option<tokio::sync::mpsc::Receiver<InterceptPaymentRequest>>,
120
121    /// Lock pool used to ensure that our implementation of `ILnRpcClient::pay`
122    /// doesn't allow for multiple simultaneous calls with the same invoice to
123    /// execute in parallel. This helps ensure that the function is idempotent.
124    outbound_lightning_payment_lock_pool: lockable::LockPool<PaymentId>,
125
126    /// Lock pool used to ensure that our implementation of
127    /// `ILnRpcClient::pay_offer` doesn't allow for multiple simultaneous
128    /// calls with the same offer to execute in parallel. This helps ensure
129    /// that the function is idempotent.
130    outbound_offer_lock_pool: lockable::LockPool<LdkOfferId>,
131
132    /// A map keyed by the `UserChannelId` of a channel that is currently
133    /// opening. The `Sender` is used to communicate the `OutPoint` back to
134    /// the API handler from the event handler when the channel has been
135    /// opened and is now pending, or the reason it closed before that.
136    pending_channels: Arc<RwLock<BTreeMap<UserChannelId, PendingChannelSender>>>,
137
138    /// Waiters for outgoing LDK payments that are woken by terminal payment
139    /// events (`PaymentSuccessful` / `PaymentFailed`). This lets `pay()` block
140    /// until the payment resolves instead of polling `node.payment()`. The
141    /// actual result is still read from `node.payment()` after the wakeup; this
142    /// map only signals that a terminal event has arrived.
143    pending_payments: Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
144}
145
146/// Sends a channel's funding outpoint once the channel is pending, or the
147/// reason the channel closed before that.
148type PendingChannelSender = oneshot::Sender<Result<OutPoint, String>>;
149
150impl std::fmt::Debug for GatewayLdkClient {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("GatewayLdkClient").finish_non_exhaustive()
153    }
154}
155
156/// How the gateway obtained the mnemonic the LDK node derives its keys from.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum MnemonicSource {
159    /// Freshly generated by this process, so it controls no historical onchain
160    /// funds and the node can start its onchain wallet at the current tip.
161    Generated,
162    /// Imported or loaded from a previous run, so it may control onchain funds
163    /// the node has no record of, e.g. after its data directory was lost. The
164    /// node then scans the chain from genesis when it first creates its
165    /// onchain wallet, since starting at the tip would leave those funds
166    /// undiscovered. This has no effect once the wallet exists.
167    Existing,
168}
169
170impl GatewayLdkClient {
171    /// Creates a new `GatewayLdkClient` instance and starts the underlying
172    /// lightning node. All resources, including the lightning node, will be
173    /// cleaned up when the returned `GatewayLdkClient` instance is dropped.
174    /// There's no need to manually stop the node.
175    ///
176    /// # Errors
177    ///
178    /// Fails with [`LdkClientInitError::MissingEsploraHost`] or
179    /// [`LdkClientInitError::InvalidDataDir`] if the configuration cannot be
180    /// used, with [`LdkClientInitError::Build`] if LDK cannot build the node,
181    /// and with [`LdkClientInitError::Start`] if it cannot start it.
182    #[allow(clippy::too_many_arguments)]
183    pub fn new(
184        data_dir: &Path,
185        chain_source: ChainSource,
186        network: Network,
187        lightning_port: u16,
188        alias: String,
189        mnemonic: Mnemonic,
190        mnemonic_source: MnemonicSource,
191        runtime: Arc<tokio::runtime::Runtime>,
192    ) -> Result<Self, LdkClientInitError> {
193        let mut bytes = [0u8; 32];
194        let alias = if alias.is_empty() {
195            "LDK Gateway".to_string()
196        } else {
197            alias
198        };
199        let alias_bytes = alias.as_bytes();
200        let truncated = &alias_bytes[..alias_bytes.len().min(32)];
201        bytes[..truncated.len()].copy_from_slice(truncated);
202        let node_alias = Some(NodeAlias(bytes));
203
204        let mut node_builder = ldk_node::Builder::from_config(ldk_node::config::Config {
205            network,
206            listening_addresses: Some(vec![SocketAddress::TcpIpV4 {
207                addr: [0, 0, 0, 0],
208                port: lightning_port,
209            }]),
210            node_alias,
211            ..Default::default()
212        });
213
214        // Route LDK's logs into the gateway's `tracing` subscriber so they land in
215        // the same place (stderr / log file) and honor `RUST_LOG`, instead of LDK's
216        // default append-only `ldk_node/ldk_node.log` file.
217        node_builder.set_custom_logger(Arc::new(LdkTracingLogger {
218            in_test_env: is_running_in_test_env(),
219        }));
220
221        node_builder.set_entropy_bip39_mnemonic(mnemonic, None);
222
223        match chain_source.clone() {
224            ChainSource::Bitcoind {
225                username,
226                password,
227                server_url,
228            } => {
229                node_builder.set_chain_source_bitcoind_rpc(
230                    server_url
231                        .host_str()
232                        .expect("Could not retrieve host from bitcoind RPC url")
233                        .to_string(),
234                    server_url
235                        .port()
236                        .expect("Could not retrieve port from bitcoind RPC url"),
237                    username,
238                    password,
239                );
240            }
241            ChainSource::Esplora { server_url } => {
242                node_builder.set_chain_source_esplora(get_esplora_url(server_url)?, None);
243            }
244        };
245        let Some(data_dir_str) = data_dir.to_str() else {
246            return Err(LdkClientInitError::InvalidDataDir);
247        };
248        node_builder.set_storage_dir_path(data_dir_str.to_string());
249        node_builder.set_runtime(runtime.handle().clone());
250        if mnemonic_source == MnemonicSource::Existing {
251            info!(
252                target: LOG_LIGHTNING,
253                "Mnemonic was not generated by this process, recovering onchain wallet from genesis if it does not exist yet"
254            );
255            node_builder.set_wallet_recovery_mode();
256        }
257
258        info!(chain_source = %chain_source, data_dir = %data_dir_str, alias = %alias, "Starting LDK Node...");
259        let node = Arc::new(node_builder.build().map_err(LdkClientInitError::Build)?);
260        node.start().map_err(|err| {
261            crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to start LDK Node");
262            LdkClientInitError::Start(err)
263        })?;
264
265        let (htlc_stream_sender, htlc_stream_receiver) = tokio::sync::mpsc::channel(1024);
266        let task_group = TaskGroup::new();
267
268        let node_clone = node.clone();
269        let pending_channels = Arc::new(RwLock::new(BTreeMap::new()));
270        let pending_channels_clone = pending_channels.clone();
271        let pending_payments = Arc::new(RwLock::new(HashMap::new()));
272        let pending_payments_clone = pending_payments.clone();
273        task_group.spawn("ldk lightning node event handler", |handle| async move {
274            loop {
275                Self::handle_next_event(
276                    &node_clone,
277                    &htlc_stream_sender,
278                    &handle,
279                    pending_channels_clone.clone(),
280                    pending_payments_clone.clone(),
281                )
282                .await;
283            }
284        });
285
286        info!("Successfully started LDK Gateway");
287        Ok(GatewayLdkClient {
288            node,
289            task_group,
290            htlc_stream_receiver_or: Some(htlc_stream_receiver),
291            outbound_lightning_payment_lock_pool: lockable::LockPool::new(),
292            outbound_offer_lock_pool: lockable::LockPool::new(),
293            pending_channels,
294            pending_payments,
295        })
296    }
297
298    async fn handle_next_event(
299        node: &ldk_node::Node,
300        htlc_stream_sender: &Sender<InterceptPaymentRequest>,
301        handle: &TaskHandle,
302        pending_channels: Arc<RwLock<BTreeMap<UserChannelId, PendingChannelSender>>>,
303        pending_payments: Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
304    ) {
305        // We manually check for task termination in case we receive a payment while the
306        // task is shutting down. In that case, we want to finish the payment
307        // before shutting this task down.
308        let event = tokio::select! {
309            event = node.next_event_async() => {
310                event
311            }
312            () = handle.make_shutdown_rx() => {
313                return;
314            }
315        };
316
317        match event {
318            ldk_node::Event::PaymentClaimable {
319                payment_id: _,
320                payment_hash,
321                claimable_amount_msat,
322                claim_deadline,
323                custom_records: _,
324            } => {
325                if let Err(err) = htlc_stream_sender
326                    .send(InterceptPaymentRequest {
327                        payment_hash: Hash::from_slice(&payment_hash.0)
328                            .expect("Failed to create Hash"),
329                        // LDK reports the real claimable amount, so the two
330                        // amounts coincide here.
331                        amount_msat: claimable_amount_msat,
332                        incoming_amount_msat: claimable_amount_msat,
333                        expiry: claim_deadline.unwrap_or_default(),
334                        short_channel_id: None,
335                        // LDK claims payments through its own payment store,
336                        // so it never intercepts forwards for the gateway.
337                        incoming_chan_id: NO_INCOMING_CIRCUIT.0,
338                        htlc_id: NO_INCOMING_CIRCUIT.1,
339                    })
340                    .await
341                {
342                    warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed send InterceptHtlcRequest to stream");
343                }
344            }
345            ldk_node::Event::ChannelPending {
346                channel_id,
347                user_channel_id,
348                former_temporary_channel_id: _,
349                counterparty_node_id: _,
350                funding_txo,
351            } => {
352                info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is pending");
353                let mut channels = pending_channels.write().await;
354                if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
355                    let _ = sender.send(Ok(funding_txo));
356                } else {
357                    debug!(
358                        ?user_channel_id,
359                        "No channel pending channel open for user channel id"
360                    );
361                }
362            }
363            ldk_node::Event::ChannelClosed {
364                channel_id,
365                user_channel_id,
366                counterparty_node_id: _,
367                reason,
368            } => {
369                info!(target: LOG_LIGHTNING, %channel_id, "LDK Channel is closed");
370                let mut channels = pending_channels.write().await;
371                if let Some(sender) = channels.remove(&UserChannelId(user_channel_id)) {
372                    let reason = if let Some(reason) = reason {
373                        reason.to_string()
374                    } else {
375                        "Channel has been closed".to_string()
376                    };
377                    let _ = sender.send(Err(reason));
378                } else {
379                    debug!(
380                        ?user_channel_id,
381                        "No channel pending channel open for user channel id"
382                    );
383                }
384            }
385            ldk_node::Event::PaymentSuccessful {
386                payment_id: Some(payment_id),
387                ..
388            }
389            | ldk_node::Event::PaymentFailed {
390                payment_id: Some(payment_id),
391                ..
392            } => {
393                Self::wake_pending_payment(&pending_payments, payment_id).await;
394            }
395            _ => {}
396        }
397
398        // `PaymentClaimable`, `ChannelPending`/`ChannelClosed`, and terminal
399        // outgoing payment events (`PaymentSuccessful` / `PaymentFailed`) are the
400        // only event types that we are interested in. We can safely ignore all
401        // other events.
402        if let Err(err) = node.event_handled() {
403            warn!(err = %err.fmt_compact(), "LDK could not mark event handled");
404        }
405    }
406
407    /// Wakes the `pay()` waiter (if any) for `payment_id` once a terminal
408    /// payment event has been observed. The actual payment result is read from
409    /// `node.payment()` by the woken waiter.
410    async fn wake_pending_payment(
411        pending_payments: &Arc<RwLock<HashMap<PaymentId, oneshot::Sender<()>>>>,
412        payment_id: PaymentId,
413    ) -> PendingPaymentWakeup {
414        let Some(sender) = pending_payments.write().await.remove(&payment_id) else {
415            return PendingPaymentWakeup::NoWaiter;
416        };
417
418        if sender.send(()).is_ok() {
419            PendingPaymentWakeup::Woken
420        } else {
421            PendingPaymentWakeup::ReceiverDropped
422        }
423    }
424
425    /// Returns the node's payment record for `payment_id`, but only when it is
426    /// one of our own outbound attempts.
427    ///
428    /// LDK keys BOLT11 payments by `PaymentId(payment_hash)` in both
429    /// directions, so a registered invoice or a claimed inbound payment for the
430    /// same hash shares a slot with our outbound send. Without this direction
431    /// check such an inbound record could be mistaken for the result of our
432    /// `pay()`: reported as a spurious success (a preimage we never sent for),
433    /// a spurious failure, or -- while still pending -- block `pay()` forever.
434    fn outbound_payment(&self, payment_id: PaymentId) -> Option<PaymentDetails> {
435        self.node
436            .payment(&payment_id)
437            .filter(|details| details.direction == PaymentDirection::Outbound)
438    }
439
440    /// Reads the result of an outgoing payment from `node.payment()`.
441    ///
442    /// Returns `None` while the payment is still pending (or not yet known to
443    /// the node), and `Some` once it has reached a terminal status.
444    fn ldk_payment_result(
445        &self,
446        payment_id: PaymentId,
447    ) -> Option<Result<PayInvoiceResponse, LightningRpcError>> {
448        let payment_details = self.outbound_payment(payment_id)?;
449        match payment_details.status {
450            PaymentStatus::Pending => None,
451            PaymentStatus::Succeeded => {
452                if let PaymentKind::Bolt11 {
453                    preimage: Some(preimage),
454                    ..
455                } = payment_details.kind
456                {
457                    Some(Ok(PayInvoiceResponse {
458                        preimage: Preimage(preimage.0),
459                    }))
460                } else {
461                    Some(Err(LightningRpcError::FailedPayment {
462                        failure_reason: "LDK payment succeeded without preimage".to_string(),
463                    }))
464                }
465            }
466            PaymentStatus::Failed => Some(Err(LightningRpcError::FailedPayment {
467                failure_reason: "LDK payment failed".to_string(),
468            })),
469        }
470    }
471}
472
473/// A failure to create the LDK Lightning node behind a [`GatewayLdkClient`].
474///
475/// Creating the client configures an LDK node from the gateway's settings,
476/// builds it on top of the data directory and starts it.
477#[derive(Debug, Error)]
478#[non_exhaustive]
479pub enum LdkClientInitError {
480    /// The Esplora chain source's URL names no host to connect to.
481    #[error("Missing esplora host")]
482    MissingEsploraHost,
483
484    /// The data directory's path is not valid UTF-8, which LDK needs for its
485    /// storage path.
486    #[error("Invalid data dir path")]
487    InvalidDataDir,
488
489    /// LDK could not build the node, for example because its storage could
490    /// not be read or was created for another network.
491    #[error("The LDK node could not be built")]
492    Build(#[source] ldk_node::BuildError),
493
494    /// LDK built the node but could not start it.
495    #[error("The LDK node could not be started")]
496    Start(#[source] ldk_node::NodeError),
497}
498
499/// Why an invoice must not be registered for a payment hash on the node.
500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
501enum InboundRegistrationRefusal {
502    /// `pay()` holds the per-hash lock, so an outbound payment for the hash
503    /// is being dispatched or awaited right now.
504    OutboundInFlight,
505    /// The node holds an outbound record for the hash, pending or terminal.
506    OutboundRecorded,
507}
508
509impl std::fmt::Display for InboundRegistrationRefusal {
510    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511        match self {
512            Self::OutboundInFlight => {
513                write!(f, "an outbound payment for this hash is in flight")
514            }
515            Self::OutboundRecorded => {
516                write!(f, "the node holds an outbound payment for this hash")
517            }
518        }
519    }
520}
521
522/// Decides whether an invoice may be registered for a payment hash the node
523/// may already know as one of our outbound payments.
524///
525/// `ldk-node` keys BOLT11 payments by `PaymentId(payment_hash)` in both
526/// directions, and registering an invoice overwrites whatever record shares
527/// the key. Registering one for a hash we pay would erase the outbound record
528/// `pay()` reads its result from, so a settled payment would be reported as
529/// failed and the outgoing contract forfeited while the payee keeps the funds.
530///
531/// `outbound_lock_acquired` is whether the caller took the per-hash lock
532/// `pay()` holds for the whole life of a payment, and `existing_direction` is
533/// the direction of the node's record for the hash, if any. A terminal
534/// outbound record is refused as well: a restarted state machine re-runs
535/// `pay()` and needs that record to recover the payment's result instead of
536/// re-dispatching. An existing inbound record is left to the caller, whose
537/// own reservation already rejects duplicate registrations.
538fn check_inbound_registration(
539    outbound_lock_acquired: bool,
540    existing_direction: Option<PaymentDirection>,
541) -> Result<(), InboundRegistrationRefusal> {
542    if !outbound_lock_acquired {
543        return Err(InboundRegistrationRefusal::OutboundInFlight);
544    }
545
546    match existing_direction {
547        Some(PaymentDirection::Outbound) => Err(InboundRegistrationRefusal::OutboundRecorded),
548        Some(PaymentDirection::Inbound) | None => Ok(()),
549    }
550}
551
552/// Classifies an `ldk-node` claim or fail error for the gateway's completion
553/// retry loop.
554///
555/// `claim_for_hash` and `fail_for_hash` fail deterministically for an unknown
556/// payment hash, a preimage that does not hash to it, or an amount below the
557/// registered one. Retrying cannot change any of those, so they are reported
558/// as [`LightningRpcError::HtlcCompletionRejected`] and the completion state
559/// machine records the outcome instead of retrying forever. Everything else,
560/// today only a failed store write, is treated as transient: the incoming
561/// contract is already funded by the time this runs, so an error this list
562/// does not know must keep retrying rather than be recorded as final.
563fn htlc_completion_error(err: &ldk_node::NodeError, payment_hash: &str) -> LightningRpcError {
564    match err {
565        ldk_node::NodeError::InvalidPaymentHash
566        | ldk_node::NodeError::InvalidPaymentPreimage
567        | ldk_node::NodeError::InvalidAmount => LightningRpcError::HtlcCompletionRejected {
568            failure_reason: format!(
569                "LDK rejected completion of payment with hash {payment_hash}: {err}"
570            ),
571        },
572        _ => LightningRpcError::FailedToCompleteHtlc {
573            failure_reason: format!(
574                "Failed to complete LDK payment with hash {payment_hash}: {err}"
575            ),
576        },
577    }
578}
579
580impl Drop for GatewayLdkClient {
581    fn drop(&mut self) {
582        self.task_group.shutdown();
583
584        info!(target: LOG_LIGHTNING, "Stopping LDK Node...");
585        match self.node.stop() {
586            Err(err) => {
587                warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to stop LDK Node");
588            }
589            _ => {
590                info!(target: LOG_LIGHTNING, "LDK Node stopped.");
591            }
592        }
593    }
594}
595
596#[async_trait]
597impl ILnRpcClient for GatewayLdkClient {
598    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
599        let node_status = self.node.status();
600        let ldk_block_height = node_status.current_best_block.height;
601        let onchain_sync = node_status.latest_onchain_wallet_sync_timestamp;
602        let lightning_sync = node_status.latest_lightning_wallet_sync_timestamp;
603        let is_running = node_status.is_running;
604        debug!(target: LOG_LIGHTNING, ?onchain_sync, ?lightning_sync, ?is_running, "LDK Sync Status");
605
606        Ok(GetNodeInfoResponse {
607            pub_key: self.node.node_id(),
608            alias: match self.node.node_alias() {
609                Some(alias) => alias.to_string(),
610                None => format!("LDK Fedimint Gateway Node {}", self.node.node_id()),
611            },
612            network: self.node.config().network.to_string(),
613            block_height: ldk_block_height,
614            // `synced_to_chain` is used for determining if the Lightning node is ready, so we care
615            // about the `lightning_sync` status.
616            synced_to_chain: lightning_sync.is_some(),
617        })
618    }
619
620    async fn routehints(
621        &self,
622        _num_route_hints: usize,
623    ) -> Result<GetRouteHintsResponse, LightningRpcError> {
624        // `ILnRpcClient::routehints()` is currently only ever used for LNv1 payment
625        // receives and will be removed when we switch to LNv2. The LDK gateway will
626        // never support LNv1 payment receives, only LNv2 payment receives, which
627        // require that the gateway's lightning node generates invoices rather than the
628        // fedimint client, so it is able to insert the proper route hints on its own.
629        Ok(GetRouteHintsResponse {
630            route_hints: vec![],
631        })
632    }
633
634    async fn pay(
635        &self,
636        invoice: Bolt11Invoice,
637        max_delay: u64,
638        max_fee: Amount,
639    ) -> Result<PayInvoiceResponse, LightningRpcError> {
640        let payment_id = PaymentId(*invoice.payment_hash().as_byte_array());
641
642        // Lock by the payment hash to prevent multiple simultaneous calls with the same
643        // invoice from executing. This prevents `ldk-node::Bolt11Payment::send()` from
644        // being called multiple times with the same invoice. This is important because
645        // `ldk-node::Bolt11Payment::send()` is not idempotent, but this function must
646        // be idempotent.
647        let _payment_lock_guard = self
648            .outbound_lightning_payment_lock_pool
649            .async_lock(payment_id)
650            .await;
651
652        // Register a waiter before initiating the payment so that a terminal
653        // payment event firing immediately after `send()` returns still wakes
654        // us, rather than racing ahead of the registration.
655        let (payment_sender, payment_receiver) = oneshot::channel();
656        self.pending_payments
657            .write()
658            .await
659            .insert(payment_id, payment_sender);
660
661        // If no outbound attempt of ours is known to the node we can initiate
662        // it, and if one is known we can skip calling
663        // `ldk-node::Bolt11Payment::send()` and wait for the payment to
664        // complete. Checking specifically for an outbound record matters because
665        // an inbound payment shares `PaymentId(payment_hash)` with our send: a
666        // registered invoice for the same hash must not make us skip `send()`.
667        // The lock guard above guarantees that this block is only executed once
668        // at a time for a given payment hash, ensuring that there is no race
669        // condition between checking if a payment is known and initiating a new
670        // payment if it isn't.
671        if self.outbound_payment(payment_id).is_none() {
672            let sent_payment_id = match self.node.bolt11_payment().send(
673                &invoice,
674                Some(
675                    RouteParametersConfig::default()
676                        .with_max_total_routing_fee_msat(max_fee.msats)
677                        .with_max_total_cltv_expiry_delta(max_delay as u32),
678                ),
679            ) {
680                Ok(sent_payment_id) => sent_payment_id,
681                Err(err) => {
682                    self.pending_payments.write().await.remove(&payment_id);
683                    // TODO: Investigate whether all error types returned by
684                    // `Bolt11Payment::send()` result in idempotency.
685                    return Err(LightningRpcError::FailedPayment {
686                        failure_reason: format!("LDK payment failed to initialize: {err:?}"),
687                    });
688                }
689            };
690            assert_eq!(sent_payment_id, payment_id);
691        }
692
693        // The payment may already be in a terminal state (a known/resumed
694        // payment, or an event that fired before we registered the waiter), so
695        // check once up front before waiting.
696        if let Some(result) = self.ldk_payment_result(payment_id) {
697            self.pending_payments.write().await.remove(&payment_id);
698            return result;
699        }
700
701        // Otherwise wait for the event handler to wake us when a terminal
702        // `PaymentSuccessful` / `PaymentFailed` event arrives, instead of
703        // polling. A wakeup is delivered exactly once; the payment status is
704        // terminal by the time it fires.
705        let _ = payment_receiver.await;
706
707        self.pending_payments.write().await.remove(&payment_id);
708        self.ldk_payment_result(payment_id).unwrap_or_else(|| {
709            Err(LightningRpcError::FailedPayment {
710                failure_reason: "LDK payment event fired without terminal payment status"
711                    .to_string(),
712            })
713        })
714    }
715
716    async fn outbound_payment_exists(
717        &self,
718        payment_hash: sha256::Hash,
719    ) -> Result<bool, LightningRpcError> {
720        Ok(self
721            .outbound_payment(PaymentId(payment_hash.to_byte_array()))
722            .is_some())
723    }
724
725    async fn route_htlcs<'a>(
726        mut self: Box<Self>,
727        _task_group: &TaskGroup,
728    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
729        let route_htlc_stream = match self.htlc_stream_receiver_or.take() {
730            Some(stream) => Ok(Box::pin(ReceiverStream::new(stream))),
731            None => Err(LightningRpcError::FailedToRouteHtlcs {
732                failure_reason:
733                    "Stream does not exist. Likely was already taken by calling `route_htlcs()`."
734                        .to_string(),
735            }),
736        }?;
737
738        Ok((route_htlc_stream, Arc::new(*self)))
739    }
740
741    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
742        let InterceptPaymentResponse {
743            action,
744            payment_hash,
745            incoming_chan_id: _,
746            htlc_id: _,
747        } = htlc;
748
749        let ph = PaymentHash(*payment_hash.clone().as_byte_array());
750
751        // TODO: Get the actual amount from the LDK node. Probably makes the
752        // most sense to pipe it through the `InterceptHtlcResponse` struct.
753        // This value is only used by `ldk-node` to ensure that the amount
754        // claimed isn't less than the amount expected, but we've already
755        // verified that the amount is correct when we intercepted the payment.
756        let claimable_amount_msat = 999_999_999_999_999;
757
758        let ph_hex_str = hex::encode(payment_hash);
759
760        if let PaymentAction::Settle(preimage) = action {
761            self.node
762                .bolt11_payment()
763                .claim_for_hash(ph, claimable_amount_msat, PaymentPreimage(preimage.0))
764                .map_err(|err| htlc_completion_error(&err, &ph_hex_str))?;
765        } else {
766            warn!(target: LOG_LIGHTNING, payment_hash = %ph_hex_str, "Unwinding payment because the action was not `Settle`");
767            self.node
768                .bolt11_payment()
769                .fail_for_hash(ph)
770                .map_err(|err| htlc_completion_error(&err, &ph_hex_str))?;
771        }
772
773        return Ok(());
774    }
775
776    async fn create_invoice(
777        &self,
778        create_invoice_request: CreateInvoiceRequest,
779    ) -> Result<CreateInvoiceResponse, LightningRpcError> {
780        let payment_hash_or = if let Some(payment_hash) = create_invoice_request.payment_hash {
781            let ph = PaymentHash(*payment_hash.as_byte_array());
782            Some(ph)
783        } else {
784            None
785        };
786
787        let description = match create_invoice_request.description {
788            Some(InvoiceDescription::Direct(desc)) => {
789                Bolt11InvoiceDescription::Direct(Description::new(desc).map_err(|_| {
790                    LightningRpcError::FailedToGetInvoice {
791                        failure_reason: "Invalid description".to_string(),
792                    }
793                })?)
794            }
795            Some(InvoiceDescription::Hash(hash)) => {
796                Bolt11InvoiceDescription::Hash(lightning_invoice::Sha256(hash))
797            }
798            None => Bolt11InvoiceDescription::Direct(Description::empty()),
799        };
800
801        let invoice = match payment_hash_or {
802            Some(payment_hash) => {
803                // `pay()` holds this lock for the whole life of an outbound
804                // payment, so failing to take it means a payment for this hash
805                // is in flight right now. Keeping it until `receive_for_hash()`
806                // has run means `pay()` cannot write its outbound record between
807                // our check and the registration's insert, which would
808                // overwrite it.
809                let payment_id = PaymentId(payment_hash.0);
810                let outbound_lock_guard = self
811                    .outbound_lightning_payment_lock_pool
812                    .try_lock(payment_id);
813                let existing_direction = self
814                    .node
815                    .payment(&payment_id)
816                    .map(|details| details.direction);
817                if let Err(refusal) =
818                    check_inbound_registration(outbound_lock_guard.is_some(), existing_direction)
819                {
820                    warn!(
821                        target: LOG_LIGHTNING,
822                        payment_hash = %hex::encode(payment_hash.0),
823                        %refusal,
824                        "Refusing to register an invoice for a payment hash we pay outbound"
825                    );
826                    return Err(LightningRpcError::FailedToGetInvoice {
827                        failure_reason: format!(
828                            "Payment hash cannot be registered for an invoice: {refusal}"
829                        ),
830                    });
831                }
832
833                self.node.bolt11_payment().receive_for_hash(
834                    create_invoice_request.amount_msat,
835                    &description,
836                    create_invoice_request.expiry_secs,
837                    payment_hash,
838                )
839            }
840            None => self.node.bolt11_payment().receive(
841                create_invoice_request.amount_msat,
842                &description,
843                create_invoice_request.expiry_secs,
844            ),
845        }
846        .map_err(|e| LightningRpcError::FailedToGetInvoice {
847            failure_reason: e.to_string(),
848        })?;
849
850        Ok(CreateInvoiceResponse {
851            invoice: invoice.to_string(),
852        })
853    }
854
855    async fn get_ln_onchain_address(
856        &self,
857    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
858        self.node
859            .onchain_payment()
860            .new_address()
861            .map(|address| GetLnOnchainAddressResponse {
862                address: address.to_string(),
863            })
864            .map_err(|e| LightningRpcError::FailedToGetLnOnchainAddress {
865                failure_reason: e.to_string(),
866            })
867    }
868
869    async fn send_onchain(
870        &self,
871        SendOnchainRequest {
872            address,
873            amount,
874            fee_rate_sats_per_vbyte,
875        }: SendOnchainRequest,
876    ) -> Result<SendOnchainResponse, LightningRpcError> {
877        let onchain = self.node.onchain_payment();
878
879        let retain_reserves = false;
880        let txid = match amount {
881            BitcoinAmountOrAll::All => onchain.send_all_to_address(
882                &address.assume_checked(),
883                retain_reserves,
884                FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
885            ),
886            BitcoinAmountOrAll::Amount(amount_sats) => onchain.send_to_address(
887                &address.assume_checked(),
888                amount_sats.to_sat(),
889                FeeRate::from_sat_per_vb(fee_rate_sats_per_vbyte),
890            ),
891        }
892        .map_err(|e| LightningRpcError::FailedToWithdrawOnchain {
893            failure_reason: e.to_string(),
894        })?;
895
896        Ok(SendOnchainResponse {
897            txid: txid.to_string(),
898        })
899    }
900
901    async fn open_channel(
902        &self,
903        OpenChannelRequest {
904            pubkey,
905            host,
906            channel_size_sats,
907            push_amount_sats,
908            fee_rate_sats_per_vbyte,
909            base_fee_msat,
910            parts_per_million,
911        }: OpenChannelRequest,
912    ) -> Result<OpenChannelResponse, LightningRpcError> {
913        let push_amount_msats_or = if push_amount_sats == 0 {
914            None
915        } else {
916            Some(push_amount_sats * 1000)
917        };
918
919        if fee_rate_sats_per_vbyte.is_some() {
920            // LDK manages its own fee estimation for funding transactions; the
921            // user-supplied rate cannot be applied here.
922            warn!(
923                target: LOG_LIGHTNING,
924                "Ignoring fee_rate_sats_per_vbyte on LDK channel open; LDK uses its built-in fee estimator"
925            );
926        }
927
928        let channel_config = match (base_fee_msat, parts_per_million) {
929            (None, None) => None,
930            (base, ppm) => {
931                let mut config = ChannelConfig::default();
932                if let Some(base) = base {
933                    config.forwarding_fee_base_msat = u32::try_from(base).map_err(|_| {
934                        LightningRpcError::FailedToOpenChannel {
935                            failure_reason: format!(
936                                "base_fee_msat {base} does not fit in u32 (LDK limit)"
937                            ),
938                        }
939                    })?;
940                }
941                if let Some(ppm) = ppm {
942                    config.forwarding_fee_proportional_millionths =
943                        u32::try_from(ppm).map_err(|_| LightningRpcError::FailedToOpenChannel {
944                            failure_reason: format!(
945                                "parts_per_million {ppm} does not fit in u32 (LDK limit)"
946                            ),
947                        })?;
948                }
949                Some(config)
950            }
951        };
952
953        let (tx, rx) = oneshot::channel::<Result<OutPoint, String>>();
954
955        {
956            let mut channels = self.pending_channels.write().await;
957            let user_channel_id = self
958                .node
959                .open_announced_channel(
960                    pubkey,
961                    SocketAddress::from_str(&host).map_err(|e| {
962                        LightningRpcError::FailedToConnectToPeer {
963                            failure_reason: e.to_string(),
964                        }
965                    })?,
966                    channel_size_sats,
967                    push_amount_msats_or,
968                    channel_config,
969                )
970                .map_err(|e| LightningRpcError::FailedToOpenChannel {
971                    failure_reason: e.to_string(),
972                })?;
973
974            channels.insert(UserChannelId(user_channel_id), tx);
975        }
976
977        match rx
978            .await
979            .map_err(|err| LightningRpcError::FailedToOpenChannel {
980                failure_reason: err.to_string(),
981            })? {
982            Ok(outpoint) => {
983                let funding_txid = outpoint.txid;
984
985                Ok(OpenChannelResponse {
986                    funding_txid: funding_txid.to_string(),
987                })
988            }
989            Err(failure_reason) => Err(LightningRpcError::FailedToOpenChannel { failure_reason }),
990        }
991    }
992
993    async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
994        let NodeAddress { pubkey, address } = payload.node_address;
995        // Persist the peer so ldk-node automatically reconnects after restarts
996        // and connection drops. Without this a gateway whose only channel was
997        // opened inbound (e.g. bought from an LSP) has no stored peer address
998        // and the channel stays inactive after any disconnect until an
999        // operator manually reconnects.
1000        self.node.connect(pubkey, address, true).map_err(|e| {
1001            LightningRpcError::FailedToConnectToPeer {
1002                failure_reason: e.to_string(),
1003            }
1004        })
1005    }
1006
1007    async fn close_channels_with_peer(
1008        &self,
1009        CloseChannelsWithPeerRequest {
1010            pubkey,
1011            force,
1012            sats_per_vbyte: _,
1013        }: CloseChannelsWithPeerRequest,
1014    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
1015        let mut num_channels_closed = 0;
1016
1017        info!(%pubkey, "Closing all channels with peer");
1018        for channel_with_peer in self
1019            .node
1020            .list_channels()
1021            .iter()
1022            .filter(|channel| channel.counterparty_node_id == pubkey)
1023        {
1024            if force {
1025                match self.node.force_close_channel(
1026                    &channel_with_peer.user_channel_id,
1027                    pubkey,
1028                    Some("User initiated force close".to_string()),
1029                ) {
1030                    Ok(()) => num_channels_closed += 1,
1031                    Err(err) => {
1032                        error!(%pubkey, err = %err.fmt_compact(), "Could not force close channel");
1033                    }
1034                }
1035            } else {
1036                match self
1037                    .node
1038                    .close_channel(&channel_with_peer.user_channel_id, pubkey)
1039                {
1040                    Ok(()) => {
1041                        num_channels_closed += 1;
1042                    }
1043                    Err(err) => {
1044                        error!(%pubkey, err = %err.fmt_compact(), "Could not close channel");
1045                    }
1046                }
1047            }
1048        }
1049
1050        Ok(CloseChannelsWithPeerResponse {
1051            num_channels_closed,
1052        })
1053    }
1054
1055    async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
1056        let mut channels = Vec::new();
1057        let network_graph = self.node.network_graph();
1058
1059        // Build a map of peer pubkey -> address from connected/known peers
1060        let peer_addresses: std::collections::HashMap<_, _> = self
1061            .node
1062            .list_peers()
1063            .into_iter()
1064            .map(|peer| (peer.node_id, peer.address.to_string()))
1065            .collect();
1066
1067        for channel_details in self.node.list_channels().iter() {
1068            let node_id = NodeId::from_pubkey(&channel_details.counterparty_node_id);
1069            let node_info = network_graph.node(&node_id);
1070
1071            // Look up peer alias from network graph
1072            let remote_node_alias = node_info.as_ref().and_then(|info| {
1073                info.announcement_info.as_ref().and_then(|announcement| {
1074                    let alias = announcement.alias().to_string();
1075                    if alias.is_empty() { None } else { Some(alias) }
1076                })
1077            });
1078
1079            let remote_address = peer_addresses
1080                .get(&channel_details.counterparty_node_id)
1081                .cloned();
1082
1083            channels.push(ChannelInfo {
1084                remote_pubkey: channel_details.counterparty_node_id,
1085                channel_size_sats: channel_details.channel_value_sats,
1086                outbound_liquidity_sats: channel_details.outbound_capacity_msat / 1000,
1087                inbound_liquidity_sats: channel_details.inbound_capacity_msat / 1000,
1088                is_active: channel_details.is_usable,
1089                funding_outpoint: channel_details.funding_txo,
1090                remote_node_alias,
1091                remote_address,
1092                base_fee_msat: Some(u64::from(channel_details.config.forwarding_fee_base_msat)),
1093                parts_per_million: Some(u64::from(
1094                    channel_details
1095                        .config
1096                        .forwarding_fee_proportional_millionths,
1097                )),
1098            });
1099        }
1100
1101        Ok(ListChannelsResponse { channels })
1102    }
1103
1104    async fn set_channel_fees(
1105        &self,
1106        payload: SetChannelFeesRequest,
1107    ) -> Result<(), LightningRpcError> {
1108        // ldk-node's `update_channel_config` is keyed by `UserChannelId` +
1109        // counterparty pubkey, so resolve the funding outpoint to those by
1110        // scanning the live channel list.
1111        let channel = self
1112            .node
1113            .list_channels()
1114            .into_iter()
1115            .find(|c| c.funding_txo == Some(payload.funding_outpoint))
1116            .ok_or_else(|| LightningRpcError::FailedToSetChannelFees {
1117                failure_reason: format!(
1118                    "No channel found with funding outpoint {}",
1119                    payload.funding_outpoint,
1120                ),
1121            })?;
1122
1123        let forwarding_fee_base_msat = u32::try_from(payload.base_fee_msat).map_err(|_| {
1124            LightningRpcError::FailedToSetChannelFees {
1125                failure_reason: format!(
1126                    "base_fee_msat {} does not fit in u32 (LDK limit)",
1127                    payload.base_fee_msat,
1128                ),
1129            }
1130        })?;
1131        let forwarding_fee_proportional_millionths = u32::try_from(payload.parts_per_million)
1132            .map_err(|_| LightningRpcError::FailedToSetChannelFees {
1133                failure_reason: format!(
1134                    "parts_per_million {} does not fit in u32 (LDK limit)",
1135                    payload.parts_per_million,
1136                ),
1137            })?;
1138
1139        // Copy the channel's current config so we only change the two fee
1140        // fields and preserve cltv_expiry_delta, dust limits, etc.
1141        let new_config = ChannelConfig {
1142            forwarding_fee_base_msat,
1143            forwarding_fee_proportional_millionths,
1144            ..channel.config
1145        };
1146
1147        self.node
1148            .update_channel_config(
1149                &channel.user_channel_id,
1150                channel.counterparty_node_id,
1151                new_config,
1152            )
1153            .map_err(|e| LightningRpcError::FailedToSetChannelFees {
1154                failure_reason: e.to_string(),
1155            })?;
1156
1157        Ok(())
1158    }
1159
1160    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
1161        let balances = self.node.list_balances();
1162        let channel_lists = self
1163            .node
1164            .list_channels()
1165            .into_iter()
1166            .filter(|chan| chan.is_usable)
1167            .collect::<Vec<_>>();
1168        // map and get the total inbound_capacity_msat in the channels
1169        let total_inbound_liquidity_balance_msat: u64 = channel_lists
1170            .iter()
1171            .map(|channel| channel.inbound_capacity_msat)
1172            .sum();
1173
1174        Ok(GetBalancesResponse {
1175            onchain_balance_sats: balances.total_onchain_balance_sats,
1176            lightning_balance_msats: balances.total_lightning_balance_sats * 1000,
1177            inbound_lightning_liquidity_msats: total_inbound_liquidity_balance_msat,
1178        })
1179    }
1180
1181    async fn get_invoice(
1182        &self,
1183        get_invoice_request: GetInvoiceRequest,
1184    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
1185        let invoices = self
1186            .node
1187            .list_payments_with_filter(|details| {
1188                details.direction == PaymentDirection::Inbound
1189                    && details.id == PaymentId(get_invoice_request.payment_hash.to_byte_array())
1190                    && !matches!(details.kind, PaymentKind::Onchain { .. })
1191            })
1192            .iter()
1193            .map(|details| {
1194                let (preimage, payment_hash, _) = get_preimage_and_payment_hash(&details.kind);
1195                let status = match details.status {
1196                    PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1197                    PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1198                    PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
1199                };
1200                GetInvoiceResponse {
1201                    preimage: preimage.map(|p| p.to_string()),
1202                    payment_hash,
1203                    amount: Amount::from_msats(
1204                        details
1205                            .amount_msat
1206                            .expect("amountless invoices are not supported"),
1207                    ),
1208                    created_at: UNIX_EPOCH + Duration::from_secs(details.latest_update_timestamp),
1209                    status,
1210                }
1211            })
1212            .collect::<Vec<_>>();
1213
1214        Ok(invoices.first().cloned())
1215    }
1216
1217    async fn list_transactions(
1218        &self,
1219        start_secs: u64,
1220        end_secs: u64,
1221    ) -> Result<ListTransactionsResponse, LightningRpcError> {
1222        let transactions = self
1223            .node
1224            .list_payments_with_filter(|details| {
1225                !matches!(details.kind, PaymentKind::Onchain { .. })
1226                    && details.latest_update_timestamp >= start_secs
1227                    && details.latest_update_timestamp < end_secs
1228            })
1229            .iter()
1230            .map(|details| {
1231                let (preimage, payment_hash, payment_kind) =
1232                    get_preimage_and_payment_hash(&details.kind);
1233                let direction = match details.direction {
1234                    PaymentDirection::Outbound => {
1235                        fedimint_gateway_common::PaymentDirection::Outbound
1236                    }
1237                    PaymentDirection::Inbound => fedimint_gateway_common::PaymentDirection::Inbound,
1238                };
1239                let status = match details.status {
1240                    PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1241                    PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1242                    PaymentStatus::Pending => fedimint_gateway_common::PaymentStatus::Pending,
1243                };
1244                fedimint_gateway_common::PaymentDetails {
1245                    payment_hash,
1246                    preimage: preimage.map(|p| p.to_string()),
1247                    payment_kind,
1248                    amount: Amount::from_msats(
1249                        details
1250                            .amount_msat
1251                            .expect("amountless invoices are not supported"),
1252                    ),
1253                    direction,
1254                    status,
1255                    timestamp_secs: details.latest_update_timestamp,
1256                }
1257            })
1258            .collect::<Vec<_>>();
1259        Ok(ListTransactionsResponse { transactions })
1260    }
1261
1262    fn create_offer(
1263        &self,
1264        amount: Option<Amount>,
1265        description: Option<String>,
1266        expiry_secs: Option<u32>,
1267        quantity: Option<u64>,
1268    ) -> Result<String, LightningRpcError> {
1269        let description = description.unwrap_or_default();
1270        let offer = if let Some(amount) = amount {
1271            self.node
1272                .bolt12_payment()
1273                .receive(amount.msats, &description, expiry_secs, quantity)
1274                .map_err(|err| LightningRpcError::Bolt12Error {
1275                    failure_reason: err.to_string(),
1276                })?
1277        } else {
1278            self.node
1279                .bolt12_payment()
1280                .receive_variable_amount(&description, expiry_secs)
1281                .map_err(|err| LightningRpcError::Bolt12Error {
1282                    failure_reason: err.to_string(),
1283                })?
1284        };
1285
1286        Ok(offer.to_string())
1287    }
1288
1289    async fn pay_offer(
1290        &self,
1291        offer: String,
1292        quantity: Option<u64>,
1293        amount: Option<Amount>,
1294        payer_note: Option<String>,
1295    ) -> Result<Preimage, LightningRpcError> {
1296        let offer = Offer::from_str(&offer).map_err(|_| LightningRpcError::Bolt12Error {
1297            failure_reason: "Failed to parse Bolt12 Offer".to_string(),
1298        })?;
1299
1300        let _offer_lock_guard = self
1301            .outbound_offer_lock_pool
1302            .blocking_lock(LdkOfferId(offer.id()));
1303
1304        let payment_id = if let Some(amount) = amount {
1305            self.node
1306                .bolt12_payment()
1307                .send_using_amount(&offer, amount.msats, quantity, payer_note, None)
1308                .map_err(|err| LightningRpcError::Bolt12Error {
1309                    failure_reason: err.to_string(),
1310                })?
1311        } else {
1312            self.node
1313                .bolt12_payment()
1314                .send(&offer, quantity, payer_note, None)
1315                .map_err(|err| LightningRpcError::Bolt12Error {
1316                    failure_reason: err.to_string(),
1317                })?
1318        };
1319
1320        loop {
1321            if let Some(payment_details) = self.node.payment(&payment_id) {
1322                match payment_details.status {
1323                    PaymentStatus::Pending => {}
1324                    PaymentStatus::Succeeded => match payment_details.kind {
1325                        PaymentKind::Bolt12Offer {
1326                            preimage: Some(preimage),
1327                            ..
1328                        } => {
1329                            info!(target: LOG_LIGHTNING, offer = %offer, payment_id = %payment_id, preimage = %preimage, "Successfully paid offer");
1330                            return Ok(Preimage(preimage.0));
1331                        }
1332                        _ => {
1333                            return Err(LightningRpcError::FailedPayment {
1334                                failure_reason: "Unexpected payment kind".to_string(),
1335                            });
1336                        }
1337                    },
1338                    PaymentStatus::Failed => {
1339                        return Err(LightningRpcError::FailedPayment {
1340                            failure_reason: "Bolt12 payment failed".to_string(),
1341                        });
1342                    }
1343                }
1344            }
1345            fedimint_core::runtime::sleep(Duration::from_millis(100)).await;
1346        }
1347    }
1348
1349    fn sync_wallet(&self) -> Result<(), LightningRpcError> {
1350        block_in_place(|| {
1351            let _ = self.node.sync_wallets();
1352        });
1353        Ok(())
1354    }
1355}
1356
1357/// Maps LDK's `PaymentKind` to an optional preimage and an optional payment
1358/// hash depending on the type of payment.
1359fn get_preimage_and_payment_hash(
1360    kind: &PaymentKind,
1361) -> (
1362    Option<Preimage>,
1363    Option<sha256::Hash>,
1364    fedimint_gateway_common::PaymentKind,
1365) {
1366    match kind {
1367        PaymentKind::Bolt11 {
1368            hash,
1369            preimage,
1370            secret: _,
1371        } => (
1372            preimage.map(|p| Preimage(p.0)),
1373            Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1374            fedimint_gateway_common::PaymentKind::Bolt11,
1375        ),
1376        PaymentKind::Bolt11Jit {
1377            hash,
1378            preimage,
1379            secret: _,
1380            lsp_fee_limits: _,
1381            ..
1382        } => (
1383            preimage.map(|p| Preimage(p.0)),
1384            Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1385            fedimint_gateway_common::PaymentKind::Bolt11,
1386        ),
1387        PaymentKind::Bolt12Offer {
1388            hash,
1389            preimage,
1390            secret: _,
1391            offer_id: _,
1392            payer_note: _,
1393            quantity: _,
1394        } => (
1395            preimage.map(|p| Preimage(p.0)),
1396            hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
1397            fedimint_gateway_common::PaymentKind::Bolt12Offer,
1398        ),
1399        PaymentKind::Bolt12Refund {
1400            hash,
1401            preimage,
1402            secret: _,
1403            payer_note: _,
1404            quantity: _,
1405        } => (
1406            preimage.map(|p| Preimage(p.0)),
1407            hash.map(|h| sha256::Hash::from_slice(&h.0).expect("Failed to convert payment hash")),
1408            fedimint_gateway_common::PaymentKind::Bolt12Refund,
1409        ),
1410        PaymentKind::Spontaneous { hash, preimage } => (
1411            preimage.map(|p| Preimage(p.0)),
1412            Some(sha256::Hash::from_slice(&hash.0).expect("Failed to convert payment hash")),
1413            fedimint_gateway_common::PaymentKind::Bolt11,
1414        ),
1415        PaymentKind::Onchain { .. } => (None, None, fedimint_gateway_common::PaymentKind::Onchain),
1416    }
1417}
1418
1419/// When a port is specified in the Esplora URL, the esplora client inside LDK
1420/// node cannot connect to the lightning node when there is a trailing slash.
1421/// The `SafeUrl::Display` function will always serialize the `SafeUrl` with a
1422/// trailing slash, which causes the connection to fail.
1423///
1424/// To handle this, we explicitly construct the esplora URL when a port is
1425/// specified.
1426fn get_esplora_url(server_url: SafeUrl) -> Result<String, LdkClientInitError> {
1427    // Esplora client cannot handle trailing slashes
1428    let host = server_url
1429        .host_str()
1430        .ok_or(LdkClientInitError::MissingEsploraHost)?;
1431    let server_url = if let Some(port) = server_url.port() {
1432        format!("{}://{}:{}", server_url.scheme(), host, port)
1433    } else {
1434        server_url.to_string()
1435    };
1436    Ok(server_url)
1437}
1438
1439/// Outcome of attempting to wake a `pay()` waiter for a terminal payment event.
1440#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1441enum PendingPaymentWakeup {
1442    /// No waiter was registered for the payment id.
1443    NoWaiter,
1444    /// A waiter was registered and successfully woken.
1445    Woken,
1446    /// A waiter was registered but its receiver had already been dropped.
1447    ReceiverDropped,
1448}
1449
1450#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1451struct LdkOfferId(OfferId);
1452
1453impl std::hash::Hash for LdkOfferId {
1454    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
1455        state.write(&self.0.0);
1456    }
1457}
1458
1459#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1460pub struct UserChannelId(pub ldk_node::UserChannelId);
1461
1462impl PartialOrd for UserChannelId {
1463    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
1464        Some(self.cmp(other))
1465    }
1466}
1467
1468impl Ord for UserChannelId {
1469    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
1470        self.0.0.cmp(&other.0.0)
1471    }
1472}
1473
1474#[cfg(test)]
1475mod tests;