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