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