Skip to main content

fedimint_lightning/
lnd.rs

1use std::fmt::{self, Display};
2use std::str::FromStr;
3use std::sync::Arc;
4use std::time::{Duration, UNIX_EPOCH};
5
6use anyhow::ensure;
7use async_trait::async_trait;
8use bitcoin::OutPoint;
9use bitcoin::hashes::{Hash, sha256};
10use fedimint_core::encoding::Encodable;
11use fedimint_core::task::{TaskGroup, sleep};
12use fedimint_core::util::FmtCompact;
13use fedimint_core::{Amount, BitcoinAmountOrAll, crit, secp256k1};
14use fedimint_gateway_common::{
15    ListTransactionsResponse, PaymentDetails, PaymentDirection, PaymentKind,
16};
17use fedimint_ln_common::PrunedInvoice;
18use fedimint_ln_common::contracts::Preimage;
19use fedimint_ln_common::route_hints::{RouteHint, RouteHintHop};
20use fedimint_logging::LOG_LIGHTNING;
21use hex::ToHex;
22use secp256k1::PublicKey;
23use tokio::sync::mpsc;
24use tokio_stream::wrappers::ReceiverStream;
25use tonic_lnd::invoicesrpc::lookup_invoice_msg::InvoiceRef;
26use tonic_lnd::invoicesrpc::{
27    AddHoldInvoiceRequest, CancelInvoiceMsg, LookupInvoiceMsg, SettleInvoiceMsg,
28    SubscribeSingleInvoiceRequest,
29};
30use tonic_lnd::lnrpc::channel_point::FundingTxid;
31use tonic_lnd::lnrpc::failure::FailureCode;
32use tonic_lnd::lnrpc::invoice::InvoiceState;
33use tonic_lnd::lnrpc::payment::PaymentStatus;
34use tonic_lnd::lnrpc::{
35    ChanInfoRequest, ChannelBalanceRequest, ChannelPoint, CloseChannelRequest, ConnectPeerRequest,
36    GetInfoRequest, Invoice, InvoiceSubscription, LightningAddress, ListChannelsRequest,
37    ListInvoiceRequest, ListPaymentsRequest, ListPeersRequest, OpenChannelRequest,
38    SendCoinsRequest, WalletBalanceRequest,
39};
40use tonic_lnd::routerrpc::{
41    CircuitKey, ForwardHtlcInterceptResponse, ResolveHoldForwardAction, SendPaymentRequest,
42    TrackPaymentRequest,
43};
44use tonic_lnd::tonic::Code;
45use tonic_lnd::walletrpc::AddrRequest;
46use tonic_lnd::{Client as LndClient, connect};
47use tracing::{debug, info, trace, warn};
48
49use super::{
50    ChannelInfo, ILnRpcClient, LightningRpcError, ListChannelsResponse, Lnv2HoldInvoiceFilter,
51    MAX_LIGHTNING_RETRIES, RouteHtlcStream,
52};
53use crate::{
54    CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
55    CreateInvoiceResponse, GetBalancesResponse, GetInvoiceRequest, GetInvoiceResponse,
56    GetLnOnchainAddressResponse, GetNodeInfoResponse, GetRouteHintsResponse,
57    InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription, NO_INCOMING_CIRCUIT,
58    OpenChannelResponse, PayInvoiceResponse, PaymentAction, SendOnchainRequest,
59    SendOnchainResponse,
60};
61
62type HtlcSubscriptionSender = mpsc::Sender<InterceptPaymentRequest>;
63
64const LND_PAYMENT_TIMEOUT_SECONDS: i32 = 180;
65
66#[derive(Debug, Clone, Copy, Eq, PartialEq)]
67enum HoldInvoiceAction {
68    Complete,
69    AlreadyComplete,
70}
71
72#[derive(Debug, Clone, Copy, Eq, PartialEq)]
73struct HoldInvoiceStateError {
74    failure_reason: &'static str,
75    permanent: bool,
76}
77
78fn hold_invoice_action(
79    requested_action: PaymentActionKind,
80    invoice_state: Option<InvoiceState>,
81) -> Result<HoldInvoiceAction, HoldInvoiceStateError> {
82    match (requested_action, invoice_state) {
83        (PaymentActionKind::Settle, Some(InvoiceState::Accepted))
84        | (PaymentActionKind::Cancel, Some(InvoiceState::Open | InvoiceState::Accepted)) => {
85            Ok(HoldInvoiceAction::Complete)
86        }
87        (PaymentActionKind::Settle, Some(InvoiceState::Settled))
88        | (PaymentActionKind::Cancel, Some(InvoiceState::Canceled)) => {
89            Ok(HoldInvoiceAction::AlreadyComplete)
90        }
91        (PaymentActionKind::Settle, Some(InvoiceState::Canceled)) => Err(HoldInvoiceStateError {
92            failure_reason: "HOLD invoice was canceled instead of settled",
93            permanent: true,
94        }),
95        (PaymentActionKind::Cancel, Some(InvoiceState::Settled)) => Err(HoldInvoiceStateError {
96            failure_reason: "HOLD invoice was settled instead of canceled",
97            permanent: true,
98        }),
99        (PaymentActionKind::Settle, Some(InvoiceState::Open)) => Err(HoldInvoiceStateError {
100            failure_reason: "HOLD invoice is open and has no accepted HTLC to settle",
101            permanent: false,
102        }),
103        (_, None) => Err(HoldInvoiceStateError {
104            failure_reason: "HOLD invoice does not exist",
105            permanent: true,
106        }),
107    }
108}
109
110#[derive(Debug, Clone, Copy, Eq, PartialEq)]
111enum PaymentActionKind {
112    Settle,
113    Cancel,
114}
115
116#[derive(Clone)]
117pub struct GatewayLndClient {
118    /// LND client
119    address: String,
120    tls_cert: String,
121    macaroon: String,
122    lnd_sender: Option<mpsc::Sender<ForwardHtlcInterceptResponse>>,
123    /// Predicate used to distinguish HOLD invoices the gateway created
124    /// (federation-bound) from unrelated HOLD invoices on the same LND node.
125    /// Without this, every HOLD invoice on a shared LND would be intercepted
126    /// as if it were federation-bound, producing invalid LNv1 responses that
127    /// crash LND's htlc_interceptor stream.
128    lnv2_filter: Lnv2HoldInvoiceFilter,
129}
130
131impl GatewayLndClient {
132    pub fn new(
133        address: String,
134        tls_cert: String,
135        macaroon: String,
136        lnd_sender: Option<mpsc::Sender<ForwardHtlcInterceptResponse>>,
137        lnv2_filter: Lnv2HoldInvoiceFilter,
138    ) -> Self {
139        info!(
140            target: LOG_LIGHTNING,
141            address = %address,
142            tls_cert_path = %tls_cert,
143            macaroon = %macaroon,
144            "Gateway configured to connect to LND LnRpcClient",
145        );
146        GatewayLndClient {
147            address,
148            tls_cert,
149            macaroon,
150            lnd_sender,
151            lnv2_filter,
152        }
153    }
154
155    async fn connect(&self) -> Result<LndClient, LightningRpcError> {
156        let mut retries = 0;
157        let client = loop {
158            if retries >= MAX_LIGHTNING_RETRIES {
159                return Err(LightningRpcError::FailedToConnect);
160            }
161
162            retries += 1;
163
164            match connect(
165                self.address.clone(),
166                self.tls_cert.clone(),
167                self.macaroon.clone(),
168            )
169            .await
170            {
171                Ok(client) => break client,
172                Err(err) => {
173                    debug!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Couldn't connect to LND, retrying in 1 second...");
174                    sleep(Duration::from_secs(1)).await;
175                }
176            }
177        };
178
179        Ok(client)
180    }
181
182    /// Spawns a new background task that subscribes to updates of a specific
183    /// HOLD invoice. When the HOLD invoice is ACCEPTED, we can request the
184    /// preimage from the Gateway. A new task is necessary because LND's
185    /// global `subscribe_invoices` does not currently emit updates for HOLD invoices: <https://github.com/lightningnetwork/lnd/issues/3120>
186    async fn spawn_lnv2_hold_invoice_subscription(
187        &self,
188        task_group: &TaskGroup,
189        payment_stream_group: TaskGroup,
190        gateway_sender: HtlcSubscriptionSender,
191        payment_hash: Vec<u8>,
192    ) -> Result<(), LightningRpcError> {
193        let mut client = self.connect().await?;
194
195        let self_copy = self.clone();
196        let r_hash = payment_hash.clone();
197        task_group.spawn("LND HOLD Invoice Subscription", |handle| async move {
198            let future_stream =
199                client
200                    .invoices()
201                    .subscribe_single_invoice(SubscribeSingleInvoiceRequest {
202                        r_hash: r_hash.clone(),
203                    });
204
205            let mut hold_stream = tokio::select! {
206                stream = future_stream => {
207                    match stream {
208                        Ok(stream) => stream.into_inner(),
209                        Err(err) => {
210                            crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to subscribe to hold invoice updates, shutting down payment-stream subgroup to trigger gateway reconnect");
211                            payment_stream_group.shutdown();
212                            return;
213                        }
214                    }
215                },
216                () = handle.make_shutdown_rx() => {
217                    info!(target: LOG_LIGHTNING, "LND HOLD Invoice Subscription received shutdown signal");
218                    return;
219                }
220            };
221
222            loop {
223                let hold = tokio::select! {
224                    () = handle.make_shutdown_rx() => {
225                        info!(target: LOG_LIGHTNING, "LND HOLD Invoice Subscription received shutdown signal");
226                        break;
227                    }
228                    hold_update = hold_stream.message() => {
229                        match hold_update {
230                            Ok(Some(hold)) => hold,
231                            Ok(None) => {
232                                // LND closed the stream because the invoice
233                                // reached a terminal state (settled, canceled,
234                                // or expired).
235                                break;
236                            }
237                            Err(err) => {
238                                crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over hold invoice update stream, shutting down payment-stream subgroup to trigger gateway reconnect");
239                                payment_stream_group.shutdown();
240                                break;
241                            }
242                        }
243                    }
244                };
245
246                debug!(
247                    target: LOG_LIGHTNING,
248                    payment_hash = %PrettyPaymentHash(&r_hash),
249                    state = %hold.state,
250                    "LND HOLD Invoice Update",
251                );
252
253                if hold.state() == InvoiceState::Accepted {
254                    // Only forward HOLD invoices that the gateway created on
255                    // behalf of a federation. We check here (rather than at
256                    // the new-invoice add-event) because the contract is
257                    // saved to gateway_db *after* the HOLD invoice is created
258                    // on LND, so the add-event races registration. By the
259                    // time `Accepted` fires the HTLC has arrived, which means
260                    // the BOLT11 invoice was published and the contract is
261                    // committed.
262                    let hash = sha256::Hash::from_slice(&hold.r_hash)
263                        .expect("LND payment hashes are 32 bytes");
264                    if !(self_copy.lnv2_filter)(hash).await {
265                        trace!(
266                            target: LOG_LIGHTNING,
267                            payment_hash = %PrettyPaymentHash(&hold.r_hash),
268                            "Ignoring HOLD invoice not created by this gateway",
269                        );
270                        continue;
271                    }
272
273                    let (incoming_chan_id, htlc_id) = NO_INCOMING_CIRCUIT;
274                    let intercept = InterceptPaymentRequest {
275                        payment_hash: Hash::from_slice(&hold.r_hash.clone())
276                            .expect("Failed to convert to Hash"),
277                        amount_msat: hold.amt_paid_msat as u64,
278                        // The rest of the fields are not used in LNv2 and can be removed once LNv1
279                        // support is over
280                        expiry: hold.expiry as u32,
281                        short_channel_id: Some(0),
282                        // The payment is held by a HOLD invoice on our own
283                        // node rather than by an intercepted forward, which is
284                        // how `complete_htlc` knows to resolve it by settling
285                        // or canceling that invoice.
286                        incoming_chan_id,
287                        htlc_id,
288                    };
289
290                    match gateway_sender.send(intercept).await {
291                        Ok(()) => {}
292                        Err(err) => {
293                            warn!(
294                                target: LOG_LIGHTNING,
295                                err = %err.fmt_compact(),
296                                "Hold Invoice Subscription failed to send Intercept to gateway"
297                            );
298                            let _ = self_copy.cancel_hold_invoice(hold.r_hash).await;
299                        }
300                    }
301                }
302            }
303        });
304
305        Ok(())
306    }
307
308    /// Spawns a new background task that subscribes to "add" updates for all
309    /// invoices. This is used to detect when a new invoice has been
310    /// created. If this invoice is a HOLD invoice, it is potentially destined
311    /// for a federation. At this point, we spawn a separate task to monitor the
312    /// status of the HOLD invoice.
313    async fn spawn_lnv2_invoice_subscription(
314        &self,
315        task_group: &TaskGroup,
316        gateway_sender: HtlcSubscriptionSender,
317    ) -> Result<(), LightningRpcError> {
318        let mut client = self.connect().await?;
319
320        // Compute the minimum `add_index` that we need to subscribe to updates for.
321        let first_index_offset = client
322            .lightning()
323            .list_invoices(ListInvoiceRequest {
324                pending_only: true,
325                index_offset: 0,
326                num_max_invoices: u64::MAX,
327                reversed: false,
328                ..Default::default()
329            })
330            .await
331            .map_err(|status| {
332                warn!(target: LOG_LIGHTNING, status = %status, "Failed to list all invoices");
333                LightningRpcError::FailedToRouteHtlcs {
334                    failure_reason: "Failed to list all invoices".to_string(),
335                }
336            })?
337            .into_inner()
338            .first_index_offset;
339
340        // `SubscribeInvoices` only replays invoices with an `add_index` strictly
341        // greater than `add_index`, so subscribing from `first_index_offset`
342        // directly would skip the add event of the oldest pending invoice and we
343        // would never spawn a monitor for it. Subtract one so that oldest pending
344        // invoice is replayed. `saturating_sub` keeps this correct in the
345        // empty-list case where `first_index_offset` is 0.
346        let add_index = first_index_offset.saturating_sub(1);
347
348        let self_copy = self.clone();
349        let hold_group = task_group.make_subgroup();
350        // See the matching comment in `spawn_lnv1_htlc_interceptor`: if this
351        // task exits unexpectedly we shut down the payment-stream subgroup so
352        // the gateway transitions to `Disconnected` and reconnects.
353        let subgroup = task_group.clone();
354        task_group.spawn("LND Invoice Subscription", move |handle| async move {
355            let future_stream = client.lightning().subscribe_invoices(InvoiceSubscription {
356                add_index,
357                settle_index: u64::MAX, // we do not need settle invoice events
358            });
359            let mut invoice_stream = tokio::select! {
360                stream = future_stream => {
361                    match stream {
362                        Ok(stream) => stream.into_inner(),
363                        Err(err) => {
364                            warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to subscribe to all invoice updates");
365                            subgroup.shutdown();
366                            return;
367                        }
368                    }
369                },
370                () = handle.make_shutdown_rx() => {
371                    info!(target: LOG_LIGHTNING, "LND Invoice Subscription received shutdown signal");
372                    return;
373                }
374            };
375
376            info!(target: LOG_LIGHTNING, "LND Invoice Subscription: starting to process invoice updates");
377            while let Some(invoice) = tokio::select! {
378                () = handle.make_shutdown_rx() => {
379                    info!(target: LOG_LIGHTNING, "LND Invoice Subscription task received shutdown signal");
380                    None
381                }
382                invoice_update = invoice_stream.message() => {
383                    match invoice_update {
384                        Ok(invoice) => invoice,
385                        Err(err) => {
386                            warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over invoice update stream");
387                            None
388                        }
389                    }
390                }
391            } {
392                // If the `r_preimage` is empty and the invoice is OPEN, this means a new HOLD
393                // invoice has been created, which is potentially an invoice destined for a
394                // federation. We will spawn a new task to monitor the status of
395                // the HOLD invoice.
396                let payment_hash = invoice.r_hash.clone();
397
398                debug!(
399                    target: LOG_LIGHTNING,
400                    payment_hash = %PrettyPaymentHash(&payment_hash),
401                    state = %invoice.state,
402                    "LND HOLD Invoice Update",
403                );
404
405                if invoice.r_preimage.is_empty() && invoice.state() == InvoiceState::Open {
406                    info!(
407                        target: LOG_LIGHTNING,
408                        payment_hash = %PrettyPaymentHash(&payment_hash),
409                        "Monitoring new LNv2 invoice",
410                    );
411                    if let Err(err) = self_copy
412                        .spawn_lnv2_hold_invoice_subscription(
413                            &hold_group,
414                            subgroup.clone(),
415                            gateway_sender.clone(),
416                            payment_hash.clone(),
417                        )
418                        .await
419                    {
420                        // Spawning failed because `connect()` exhausted its
421                        // retries, a strong signal that LND is unreachable. We
422                        // can no longer observe this invoice's `Accepted`
423                        // update, so shut down the payment-stream subgroup to
424                        // force a gateway reconnect.
425                        warn!(
426                            target: LOG_LIGHTNING,
427                            err = %err.fmt_compact(),
428                            payment_hash = %PrettyPaymentHash(&payment_hash),
429                            "Failed to spawn HOLD invoice subscription task, shutting down payment-stream subgroup to trigger gateway reconnect",
430                        );
431                        subgroup.shutdown();
432                    }
433                }
434            }
435
436            if !handle.is_shutting_down() {
437                warn!(target: LOG_LIGHTNING, "LND Invoice Subscription exited unexpectedly, shutting down payment-stream subgroup to trigger gateway reconnect");
438                subgroup.shutdown();
439            }
440        });
441
442        Ok(())
443    }
444
445    /// Spawns a new background task that intercepts HTLCs from the LND node. In
446    /// the LNv1 protocol, this is used as a trigger mechanism for
447    /// requesting the Gateway to retrieve the preimage for a payment.
448    async fn spawn_lnv1_htlc_interceptor(
449        &self,
450        task_group: &TaskGroup,
451        lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
452        lnd_rx: mpsc::Receiver<ForwardHtlcInterceptResponse>,
453        gateway_sender: HtlcSubscriptionSender,
454    ) -> Result<(), LightningRpcError> {
455        let mut client = self.connect().await?;
456
457        // Verify that LND is reachable via RPC before attempting to spawn a new thread
458        // that will intercept HTLCs.
459        client
460            .lightning()
461            .get_info(GetInfoRequest {})
462            .await
463            .map_err(|status| LightningRpcError::FailedToGetNodeInfo {
464                failure_reason: format!("Failed to get node info {status:?}"),
465            })?;
466
467        // If the HTLC interceptor exits unexpectedly we shut down the
468        // payment-stream subgroup. That cascades to the lnv2 invoice
469        // subscription (and its hold-invoice subtasks), which drop their
470        // `gateway_sender` clones, closing the gateway's HTLC stream and
471        // driving the gateway back to `Disconnected` so it reconnects.
472        let subgroup = task_group.clone();
473        task_group.spawn("LND HTLC Subscription", |handle| async move {
474                let future_stream = client
475                    .router()
476                    .htlc_interceptor(ReceiverStream::new(lnd_rx));
477                let mut htlc_stream = tokio::select! {
478                    stream = future_stream => {
479                        match stream {
480                            Ok(stream) => stream.into_inner(),
481                            Err(e) => {
482                                crit!(target: LOG_LIGHTNING, err = %e.fmt_compact(), "Failed to establish htlc stream");
483                                subgroup.shutdown();
484                                return;
485                            }
486                        }
487                    },
488                    () = handle.make_shutdown_rx() => {
489                        info!(target: LOG_LIGHTNING, "LND HTLC Subscription received shutdown signal while trying to intercept HTLC stream, exiting...");
490                        return;
491                    }
492                };
493
494                debug!(target: LOG_LIGHTNING, "LND HTLC Subscription: starting to process stream");
495                // To gracefully handle shutdown signals, we need to be able to receive signals
496                // while waiting for the next message from the HTLC stream.
497                //
498                // If we're in the middle of processing a message from the stream, we need to
499                // finish before stopping the spawned task. Checking if the task group is
500                // shutting down at the start of each iteration will cause shutdown signals to
501                // not process until another message arrives from the HTLC stream, which may
502                // take a long time, or never.
503                while let Some(htlc) = tokio::select! {
504                    () = handle.make_shutdown_rx() => {
505                        info!(target: LOG_LIGHTNING, "LND HTLC Subscription task received shutdown signal");
506                        None
507                    }
508                    htlc_message = htlc_stream.message() => {
509                        match htlc_message {
510                            Ok(htlc) => htlc,
511                            Err(err) => {
512                                warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over HTLC stream");
513                                None
514                            }
515                    }}
516                } {
517                    trace!(target: LOG_LIGHTNING, ?htlc, "LND Handling HTLC");
518
519                    let Some(incoming_circuit_key) = htlc.incoming_circuit_key else {
520                        // We have no circuit key, so the HTLC cannot be cancelled
521                        // either; it will time out at LND. Log enough context to
522                        // correlate with the sender's invoice and the target
523                        // federation.
524                        warn!(
525                            target: LOG_LIGHTNING,
526                            payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
527                            scid = htlc.outgoing_requested_chan_id,
528                            amount_msat = htlc.outgoing_amount_msat,
529                            "Cannot route HTLC: incoming_circuit_key is None"
530                        );
531                        continue;
532                    };
533
534                    let chan_id = incoming_circuit_key.chan_id;
535                    let htlc_id = incoming_circuit_key.htlc_id;
536
537                    // Forward all HTLCs to gatewayd, gatewayd will filter them based on scid
538                    let intercept = InterceptPaymentRequest {
539                        payment_hash: Hash::from_slice(&htlc.payment_hash).expect("Failed to convert payment Hash"),
540                        amount_msat: htlc.outgoing_amount_msat,
541                        expiry: htlc.incoming_expiry,
542                        short_channel_id: Some(htlc.outgoing_requested_chan_id),
543                        incoming_chan_id: chan_id,
544                        htlc_id,
545                    };
546
547                    match gateway_sender.send(intercept).await {
548                        Ok(()) => {}
549                        Err(err) => {
550                            warn!(
551                                target: LOG_LIGHTNING,
552                                err = %err.fmt_compact(),
553                                payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
554                                scid = htlc.outgoing_requested_chan_id,
555                                amount_msat = htlc.outgoing_amount_msat,
556                                "Failed to send HTLC to gatewayd for processing"
557                            );
558                            let _ = Self::cancel_htlc(incoming_circuit_key, lnd_sender.clone())
559                                .await
560                                .map_err(|err| {
561                                    warn!(
562                                        target: LOG_LIGHTNING,
563                                        err = %err.fmt_compact(),
564                                        payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
565                                        chan_id,
566                                        htlc_id,
567                                        "Failed to cancel HTLC"
568                                    );
569                                });
570                        }
571                    }
572                }
573
574                // Loop exited because of an HTLC stream error or end-of-stream
575                // (the expected-shutdown case is handled above).
576                if !handle.is_shutting_down() {
577                    warn!(target: LOG_LIGHTNING, "LND HTLC Subscription exited unexpectedly, shutting down payment-stream subgroup to trigger gateway reconnect");
578                    subgroup.shutdown();
579                }
580            });
581
582        Ok(())
583    }
584
585    /// Spawns background tasks for monitoring the status of incoming payments.
586    async fn spawn_interceptor(
587        &self,
588        task_group: &TaskGroup,
589        lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
590        lnd_rx: mpsc::Receiver<ForwardHtlcInterceptResponse>,
591        gateway_sender: HtlcSubscriptionSender,
592    ) -> Result<(), LightningRpcError> {
593        self.spawn_lnv1_htlc_interceptor(task_group, lnd_sender, lnd_rx, gateway_sender.clone())
594            .await?;
595
596        self.spawn_lnv2_invoice_subscription(task_group, gateway_sender)
597            .await?;
598
599        Ok(())
600    }
601
602    async fn cancel_htlc(
603        key: CircuitKey,
604        lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
605    ) -> Result<(), LightningRpcError> {
606        // TODO: Specify a failure code and message
607        let response = ForwardHtlcInterceptResponse {
608            incoming_circuit_key: Some(key),
609            action: ResolveHoldForwardAction::Fail.into(),
610            preimage: vec![],
611            failure_message: vec![],
612            failure_code: FailureCode::TemporaryChannelFailure.into(),
613            ..Default::default()
614        };
615        Self::send_lnd_response(lnd_sender, response).await
616    }
617
618    async fn send_lnd_response(
619        lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
620        response: ForwardHtlcInterceptResponse,
621    ) -> Result<(), LightningRpcError> {
622        // TODO: Consider retrying this if the send fails
623        lnd_sender.send(response).await.map_err(|send_error| {
624            LightningRpcError::FailedToCompleteHtlc {
625                failure_reason: format!(
626                    "Failed to send ForwardHtlcInterceptResponse to LND {send_error:?}"
627                ),
628            }
629        })
630    }
631
632    async fn lookup_payment(
633        &self,
634        payment_hash: Vec<u8>,
635        client: &mut LndClient,
636    ) -> Result<Option<String>, LightningRpcError> {
637        // Loop until we successfully get the status of the payment, or determine that
638        // the payment has not been made yet.
639        loop {
640            let payments = client
641                .router()
642                .track_payment_v2(TrackPaymentRequest {
643                    payment_hash: payment_hash.clone(),
644                    no_inflight_updates: true,
645                })
646                .await;
647
648            match payments {
649                Ok(payments) => {
650                    // Block until LND returns the completed payment
651                    if let Some(payment) =
652                        payments.into_inner().message().await.map_err(|status| {
653                            LightningRpcError::FailedPayment {
654                                failure_reason: status.message().to_string(),
655                            }
656                        })?
657                    {
658                        if payment.status() == PaymentStatus::Succeeded {
659                            return Ok(Some(payment.payment_preimage));
660                        }
661
662                        let failure_reason = payment.failure_reason();
663                        return Err(LightningRpcError::FailedPayment {
664                            failure_reason: format!("{failure_reason:?}"),
665                        });
666                    }
667                }
668                Err(err) => {
669                    // Break if we got a response back from the LND node that indicates the payment
670                    // hash was not found.
671                    if err.code() == Code::NotFound {
672                        return Ok(None);
673                    }
674
675                    warn!(
676                        target: LOG_LIGHTNING,
677                        payment_hash = %PrettyPaymentHash(&payment_hash),
678                        err = %err.fmt_compact(),
679                        "Could not get the status of payment. Trying again in 5 seconds"
680                    );
681                    sleep(Duration::from_secs(5)).await;
682                }
683            }
684        }
685    }
686
687    /// Looks up the invoice carrying `payment_hash`, returning `None` if the
688    /// node has no such invoice.
689    ///
690    /// Any other lookup failure is reported as an error, since callers retry on
691    /// error and an unreachable node is a condition a retry can clear.
692    async fn lookup_invoice(
693        client: &mut LndClient,
694        payment_hash: &[u8],
695    ) -> Result<Option<Invoice>, LightningRpcError> {
696        match client
697            .invoices()
698            .lookup_invoice_v2(LookupInvoiceMsg {
699                invoice_ref: Some(InvoiceRef::PaymentHash(payment_hash.to_vec())),
700                lookup_modifier: 0,
701            })
702            .await
703        {
704            Ok(invoice) => Ok(Some(invoice.into_inner())),
705            Err(err) if err.code() == Code::NotFound => Ok(None),
706            Err(err) => Err(LightningRpcError::FailedToCompleteHtlc {
707                failure_reason: format!("Failed to look up invoice: {}", err.fmt_compact()),
708            }),
709        }
710    }
711
712    /// Settles the LNv2 HOLD invoice carrying `payment_hash` with `preimage`.
713    ///
714    /// Only an already-settled invoice is an idempotent success. Missing,
715    /// nonterminal, and canceled invoices fail so callers cannot record a
716    /// settle outcome that Lightning did not produce.
717    async fn settle_hold_invoice(
718        &self,
719        payment_hash: Vec<u8>,
720        preimage: Preimage,
721    ) -> Result<(), LightningRpcError> {
722        let mut client = self.connect().await?;
723        let invoice = Self::lookup_invoice(&mut client, &payment_hash).await?;
724        match hold_invoice_action(
725            PaymentActionKind::Settle,
726            invoice.as_ref().map(Invoice::state),
727        ) {
728            Ok(HoldInvoiceAction::Complete) => {}
729            Ok(HoldInvoiceAction::AlreadyComplete) => {
730                info!(
731                    target: LOG_LIGHTNING,
732                    payment_hash = %PrettyPaymentHash(&payment_hash),
733                    "HOLD invoice was already settled",
734                );
735                return Ok(());
736            }
737            Err(error) => {
738                warn!(
739                    target: LOG_LIGHTNING,
740                    state = ?invoice.as_ref().map(Invoice::state),
741                    payment_hash = %PrettyPaymentHash(&payment_hash),
742                    failure_reason = error.failure_reason,
743                    "Cannot settle HOLD invoice",
744                );
745                return Err(if error.permanent {
746                    LightningRpcError::HtlcCompletionRejected {
747                        failure_reason: error.failure_reason.to_owned(),
748                    }
749                } else {
750                    LightningRpcError::FailedToCompleteHtlc {
751                        failure_reason: error.failure_reason.to_owned(),
752                    }
753                });
754            }
755        }
756
757        client
758            .invoices()
759            .settle_invoice(SettleInvoiceMsg {
760                preimage: preimage.0.to_vec(),
761            })
762            .await
763            .map_err(|err| {
764                warn!(
765                    target: LOG_LIGHTNING,
766                    err = %err.fmt_compact(),
767                    payment_hash = %PrettyPaymentHash(&payment_hash),
768                    "Failed to settle HOLD invoice",
769                );
770                LightningRpcError::FailedToCompleteHtlc {
771                    failure_reason: "Failed to settle HOLD invoice".to_string(),
772                }
773            })?;
774
775        info!(
776            target: LOG_LIGHTNING,
777            payment_hash = %PrettyPaymentHash(&payment_hash),
778            "Successfully settled HOLD invoice",
779        );
780
781        Ok(())
782    }
783
784    /// Cancels the LNv2 HOLD invoice carrying `payment_hash`, failing back any
785    /// HTLC it holds.
786    ///
787    /// Only an already-canceled invoice is an idempotent success. A settled
788    /// invoice fails so callers cannot record a cancel outcome after a racing
789    /// settle won.
790    async fn cancel_hold_invoice(&self, payment_hash: Vec<u8>) -> Result<(), LightningRpcError> {
791        let mut client = self.connect().await?;
792        let invoice = Self::lookup_invoice(&mut client, &payment_hash).await?;
793        match hold_invoice_action(
794            PaymentActionKind::Cancel,
795            invoice.as_ref().map(Invoice::state),
796        ) {
797            Ok(HoldInvoiceAction::Complete) => {}
798            Ok(HoldInvoiceAction::AlreadyComplete) => {
799                info!(
800                    target: LOG_LIGHTNING,
801                    payment_hash = %PrettyPaymentHash(&payment_hash),
802                    "HOLD invoice was already canceled",
803                );
804                return Ok(());
805            }
806            Err(error) => {
807                warn!(
808                    target: LOG_LIGHTNING,
809                    state = ?invoice.as_ref().map(Invoice::state),
810                    payment_hash = %PrettyPaymentHash(&payment_hash),
811                    failure_reason = error.failure_reason,
812                    "Cannot cancel HOLD invoice",
813                );
814                return Err(LightningRpcError::HtlcCompletionRejected {
815                    failure_reason: error.failure_reason.to_owned(),
816                });
817            }
818        }
819
820        client
821            .invoices()
822            .cancel_invoice(CancelInvoiceMsg {
823                payment_hash: payment_hash.clone(),
824            })
825            .await
826            .map_err(|err| {
827                warn!(
828                    target: LOG_LIGHTNING,
829                    err = %err.fmt_compact(),
830                    payment_hash = %PrettyPaymentHash(&payment_hash),
831                    "Failed to cancel HOLD invoice",
832                );
833                LightningRpcError::FailedToCompleteHtlc {
834                    failure_reason: "Failed to cancel HOLD invoice".to_string(),
835                }
836            })?;
837
838        info!(
839            target: LOG_LIGHTNING,
840            payment_hash = %PrettyPaymentHash(&payment_hash),
841            "Successfully canceled HOLD invoice",
842        );
843
844        Ok(())
845    }
846}
847
848impl fmt::Debug for GatewayLndClient {
849    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
850        write!(f, "LndClient")
851    }
852}
853
854#[async_trait]
855impl ILnRpcClient for GatewayLndClient {
856    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
857        let mut client = self.connect().await?;
858        let info = client
859            .lightning()
860            .get_info(GetInfoRequest {})
861            .await
862            .map_err(|status| LightningRpcError::FailedToGetNodeInfo {
863                failure_reason: format!("Failed to get node info {status:?}"),
864            })?
865            .into_inner();
866
867        let pub_key: PublicKey =
868            info.identity_pubkey
869                .parse()
870                .map_err(|e| LightningRpcError::FailedToGetNodeInfo {
871                    failure_reason: format!("Failed to parse public key {e:?}"),
872                })?;
873
874        let network = match info
875            .chains
876            .first()
877            .ok_or_else(|| LightningRpcError::FailedToGetNodeInfo {
878                failure_reason: "Failed to parse node network".to_string(),
879            })?
880            .network
881            .as_str()
882        {
883            // LND uses "mainnet", but rust-bitcoin uses "bitcoin".
884            // TODO: create a fedimint `Network` type that understands "mainnet"
885            "mainnet" => "bitcoin",
886            other => other,
887        }
888        .to_string();
889
890        return Ok(GetNodeInfoResponse {
891            pub_key,
892            alias: info.alias,
893            network,
894            block_height: info.block_height,
895            synced_to_chain: info.synced_to_chain,
896        });
897    }
898
899    async fn routehints(
900        &self,
901        num_route_hints: usize,
902    ) -> Result<GetRouteHintsResponse, LightningRpcError> {
903        let mut client = self.connect().await?;
904        let mut channels = client
905            .lightning()
906            .list_channels(ListChannelsRequest {
907                active_only: true,
908                inactive_only: false,
909                public_only: false,
910                private_only: false,
911                peer: vec![],
912                peer_alias_lookup: false,
913            })
914            .await
915            .map_err(|status| LightningRpcError::FailedToGetRouteHints {
916                failure_reason: format!("Failed to list channels {status:?}"),
917            })?
918            .into_inner()
919            .channels;
920
921        // Take the channels with the largest incoming capacity
922        channels.sort_by_key(|b| std::cmp::Reverse(b.remote_balance));
923        channels.truncate(num_route_hints);
924
925        let mut route_hints: Vec<RouteHint> = vec![];
926        for chan in &channels {
927            let info = client
928                .lightning()
929                .get_chan_info(ChanInfoRequest {
930                    chan_id: chan.chan_id,
931                    ..Default::default()
932                })
933                .await
934                .map_err(|status| LightningRpcError::FailedToGetRouteHints {
935                    failure_reason: format!("Failed to get channel info {status:?}"),
936                })?
937                .into_inner();
938
939            let Some(policy) = info.node1_policy else {
940                continue;
941            };
942            let src_node_id =
943                PublicKey::from_str(&chan.remote_pubkey).expect("Failed to parse pubkey");
944            let short_channel_id = chan.chan_id;
945            let base_msat = policy.fee_base_msat as u32;
946            let proportional_millionths = policy.fee_rate_milli_msat as u32;
947            let cltv_expiry_delta = policy.time_lock_delta;
948            let htlc_maximum_msat = Some(policy.max_htlc_msat);
949            let htlc_minimum_msat = Some(policy.min_htlc as u64);
950
951            let route_hint_hop = RouteHintHop {
952                src_node_id,
953                short_channel_id,
954                base_msat,
955                proportional_millionths,
956                cltv_expiry_delta: cltv_expiry_delta as u16,
957                htlc_minimum_msat,
958                htlc_maximum_msat,
959            };
960            route_hints.push(RouteHint(vec![route_hint_hop]));
961        }
962
963        Ok(GetRouteHintsResponse { route_hints })
964    }
965
966    async fn pay_private(
967        &self,
968        invoice: PrunedInvoice,
969        max_delay: u64,
970        max_fee: Amount,
971    ) -> Result<PayInvoiceResponse, LightningRpcError> {
972        let payment_hash = invoice.payment_hash.to_byte_array().to_vec();
973        info!(
974            target: LOG_LIGHTNING,
975            payment_hash = %PrettyPaymentHash(&payment_hash),
976            "LND Paying invoice",
977        );
978        let mut client = self.connect().await?;
979
980        debug!(
981            target: LOG_LIGHTNING,
982            payment_hash = %PrettyPaymentHash(&payment_hash),
983            "pay_private checking if payment for invoice exists"
984        );
985
986        // If the payment exists, that means we've already tried to pay the invoice
987        let preimage: Vec<u8> = match self
988            .lookup_payment(invoice.payment_hash.to_byte_array().to_vec(), &mut client)
989            .await?
990        {
991            Some(preimage) => {
992                info!(
993                    target: LOG_LIGHTNING,
994                    payment_hash = %PrettyPaymentHash(&payment_hash),
995                    "LND payment already exists for invoice",
996                );
997                hex::FromHex::from_hex(preimage.as_str()).map_err(|error| {
998                    LightningRpcError::FailedPayment {
999                        failure_reason: format!("Failed to convert preimage {error:?}"),
1000                    }
1001                })?
1002            }
1003            _ => {
1004                // LND API allows fee limits in the `i64` range, but we use `u64` for
1005                // max_fee_msat. This means we can only set an enforceable fee limit
1006                // between 0 and i64::MAX
1007                let fee_limit_msat: i64 =
1008                    max_fee
1009                        .msats
1010                        .try_into()
1011                        .map_err(|error| LightningRpcError::FailedPayment {
1012                            failure_reason: format!(
1013                                "max_fee_msat exceeds valid LND fee limit ranges {error:?}"
1014                            ),
1015                        })?;
1016
1017                let amt_msat = invoice.amount.msats.try_into().map_err(|error| {
1018                    LightningRpcError::FailedPayment {
1019                        failure_reason: format!("amount exceeds valid LND amount ranges {error:?}"),
1020                    }
1021                })?;
1022                let final_cltv_delta =
1023                    invoice.min_final_cltv_delta.try_into().map_err(|error| {
1024                        LightningRpcError::FailedPayment {
1025                            failure_reason: format!(
1026                                "final cltv delta exceeds valid LND range {error:?}"
1027                            ),
1028                        }
1029                    })?;
1030                let cltv_limit =
1031                    max_delay
1032                        .try_into()
1033                        .map_err(|error| LightningRpcError::FailedPayment {
1034                            failure_reason: format!("max delay exceeds valid LND range {error:?}"),
1035                        })?;
1036
1037                let dest_features = wire_features_to_lnd_feature_vec(&invoice.destination_features)
1038                    .map_err(|e| LightningRpcError::FailedPayment {
1039                        failure_reason: e.to_string(),
1040                    })?;
1041
1042                debug!(
1043                    target: LOG_LIGHTNING,
1044                    payment_hash = %PrettyPaymentHash(&payment_hash),
1045                    "LND payment does not exist, will attempt to pay",
1046                );
1047                let payments = client
1048                    .router()
1049                    .send_payment_v2(SendPaymentRequest {
1050                        amt_msat,
1051                        dest: invoice.destination.serialize().to_vec(),
1052                        dest_features,
1053                        payment_hash: invoice.payment_hash.to_byte_array().to_vec(),
1054                        payment_addr: invoice.payment_secret.to_vec(),
1055                        route_hints: route_hints_to_lnd(&invoice.route_hints),
1056                        final_cltv_delta,
1057                        cltv_limit,
1058                        no_inflight_updates: false,
1059                        timeout_seconds: LND_PAYMENT_TIMEOUT_SECONDS,
1060                        fee_limit_msat,
1061                        ..Default::default()
1062                    })
1063                    .await
1064                    .map_err(|status| {
1065                        warn!(
1066                            target: LOG_LIGHTNING,
1067                            status = %status,
1068                            payment_hash = %PrettyPaymentHash(&payment_hash),
1069                            "LND payment request failed",
1070                        );
1071                        LightningRpcError::FailedPayment {
1072                            failure_reason: format!("Failed to make outgoing payment {status:?}"),
1073                        }
1074                    })?;
1075
1076                debug!(
1077                    target: LOG_LIGHTNING,
1078                    payment_hash = %PrettyPaymentHash(&payment_hash),
1079                    "LND payment request sent, waiting for payment status...",
1080                );
1081                let mut messages = payments.into_inner();
1082                loop {
1083                    match messages.message().await.map_err(|error| {
1084                        LightningRpcError::FailedPayment {
1085                            failure_reason: format!("Failed to get payment status {error:?}"),
1086                        }
1087                    }) {
1088                        Ok(Some(payment)) if payment.status() == PaymentStatus::Succeeded => {
1089                            info!(
1090                                target: LOG_LIGHTNING,
1091                                payment_hash = %PrettyPaymentHash(&payment_hash),
1092                                "LND payment succeeded for invoice",
1093                            );
1094                            break hex::FromHex::from_hex(payment.payment_preimage.as_str())
1095                                .map_err(|error| LightningRpcError::FailedPayment {
1096                                    failure_reason: format!("Failed to convert preimage {error:?}"),
1097                                })?;
1098                        }
1099                        Ok(Some(payment)) if payment.status() == PaymentStatus::InFlight => {
1100                            debug!(
1101                                target: LOG_LIGHTNING,
1102                                payment_hash = %PrettyPaymentHash(&payment_hash),
1103                                "LND payment is inflight",
1104                            );
1105                            continue;
1106                        }
1107                        Ok(Some(payment)) => {
1108                            warn!(
1109                                target: LOG_LIGHTNING,
1110                                payment_hash = %PrettyPaymentHash(&payment_hash),
1111                                status = %payment.status,
1112                                "LND payment failed",
1113                            );
1114                            let failure_reason = payment.failure_reason();
1115                            return Err(LightningRpcError::FailedPayment {
1116                                failure_reason: format!("{failure_reason:?}"),
1117                            });
1118                        }
1119                        Ok(None) => {
1120                            warn!(
1121                                target: LOG_LIGHTNING,
1122                                payment_hash = %PrettyPaymentHash(&payment_hash),
1123                                "LND payment failed with no payment status",
1124                            );
1125                            return Err(LightningRpcError::FailedPayment {
1126                                failure_reason: format!(
1127                                    "Failed to get payment status for payment hash {:?}",
1128                                    invoice.payment_hash
1129                                ),
1130                            });
1131                        }
1132                        Err(err) => {
1133                            warn!(
1134                                target: LOG_LIGHTNING,
1135                                payment_hash = %PrettyPaymentHash(&payment_hash),
1136                                err = %err.fmt_compact(),
1137                                "LND payment failed",
1138                            );
1139                            return Err(err);
1140                        }
1141                    }
1142                }
1143            }
1144        };
1145        Ok(PayInvoiceResponse {
1146            preimage: Preimage(preimage.try_into().expect("Failed to create preimage")),
1147        })
1148    }
1149
1150    /// Returns true if the lightning backend supports payments without full
1151    /// invoices
1152    fn supports_private_payments(&self) -> bool {
1153        true
1154    }
1155
1156    async fn route_htlcs<'a>(
1157        self: Box<Self>,
1158        task_group: &TaskGroup,
1159    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
1160        const CHANNEL_SIZE: usize = 100;
1161
1162        // Channel to send intercepted htlc to the gateway for processing
1163        let (gateway_sender, gateway_receiver) =
1164            mpsc::channel::<InterceptPaymentRequest>(CHANNEL_SIZE);
1165
1166        let (lnd_sender, lnd_rx) = mpsc::channel::<ForwardHtlcInterceptResponse>(CHANNEL_SIZE);
1167
1168        self.spawn_interceptor(
1169            task_group,
1170            lnd_sender.clone(),
1171            lnd_rx,
1172            gateway_sender.clone(),
1173        )
1174        .await?;
1175        let new_client = Arc::new(Self {
1176            address: self.address.clone(),
1177            tls_cert: self.tls_cert.clone(),
1178            macaroon: self.macaroon.clone(),
1179            lnd_sender: Some(lnd_sender.clone()),
1180            lnv2_filter: self.lnv2_filter.clone(),
1181        });
1182        Ok((Box::pin(ReceiverStream::new(gateway_receiver)), new_client))
1183    }
1184
1185    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
1186        let incoming_circuit = htlc.incoming_circuit();
1187        let InterceptPaymentResponse {
1188            action,
1189            payment_hash,
1190            incoming_chan_id: _,
1191            htlc_id: _,
1192        } = htlc;
1193
1194        let (action, preimage) = match action {
1195            PaymentAction::Settle(preimage) => (ResolveHoldForwardAction::Settle, preimage),
1196            PaymentAction::Cancel => (ResolveHoldForwardAction::Fail, Preimage([0; 32])),
1197            PaymentAction::Forward => (ResolveHoldForwardAction::Resume, Preimage([0; 32])),
1198        };
1199
1200        // Resolve the payment the way it arrived. Deciding instead by probing
1201        // LND for a HOLD invoice carrying the payment hash would conflate the
1202        // two ways, because the hash is chosen by whoever is being paid: an
1203        // attacker can register an LNv2 receive and an LNv1 offer for the same
1204        // hash, and the completion for the intercepted LNv1 HTLC would then
1205        // settle or cancel the unrelated LNv2 HOLD invoice.
1206        let Some((chan_id, htlc_id)) = incoming_circuit else {
1207            // LNv2: the payment is held by a HOLD invoice on our own node, so
1208            // there is no forward to resolve.
1209            return match action {
1210                ResolveHoldForwardAction::Settle => {
1211                    self.settle_hold_invoice(payment_hash.to_byte_array().to_vec(), preimage)
1212                        .await
1213                }
1214                // Neither `Fail` nor `Resume` has a meaning for a HOLD invoice
1215                // beyond "the gateway could not claim this payment": there is
1216                // no next hop to resume towards, so fail it back to the payer.
1217                _ => {
1218                    self.cancel_hold_invoice(payment_hash.to_byte_array().to_vec())
1219                        .await
1220                }
1221            };
1222        };
1223
1224        // LNv1: hand the interceptor its response for this exact circuit.
1225        let Some(lnd_sender) = self.lnd_sender.clone() else {
1226            crit!("Gatewayd has not started to route HTLCs");
1227            return Err(LightningRpcError::FailedToCompleteHtlc {
1228                failure_reason: "Gatewayd has not started to route HTLCs".to_string(),
1229            });
1230        };
1231
1232        let response = ForwardHtlcInterceptResponse {
1233            incoming_circuit_key: Some(CircuitKey { chan_id, htlc_id }),
1234            action: action.into(),
1235            preimage: preimage.0.to_vec(),
1236            failure_message: vec![],
1237            failure_code: FailureCode::TemporaryChannelFailure.into(),
1238            ..Default::default()
1239        };
1240
1241        Self::send_lnd_response(lnd_sender, response).await
1242    }
1243
1244    async fn create_invoice(
1245        &self,
1246        create_invoice_request: CreateInvoiceRequest,
1247    ) -> Result<CreateInvoiceResponse, LightningRpcError> {
1248        let mut client = self.connect().await?;
1249        let description = create_invoice_request
1250            .description
1251            .unwrap_or(InvoiceDescription::Direct(String::new()));
1252
1253        if let Some(payment_hash_value) = create_invoice_request.payment_hash {
1254            let payment_hash = payment_hash_value.to_byte_array().to_vec();
1255            let hold_invoice_request = match description {
1256                InvoiceDescription::Direct(description) => AddHoldInvoiceRequest {
1257                    memo: description,
1258                    hash: payment_hash.clone(),
1259                    value_msat: create_invoice_request.amount_msat as i64,
1260                    expiry: i64::from(create_invoice_request.expiry_secs),
1261                    ..Default::default()
1262                },
1263                InvoiceDescription::Hash(desc_hash) => AddHoldInvoiceRequest {
1264                    description_hash: desc_hash.to_byte_array().to_vec(),
1265                    hash: payment_hash.clone(),
1266                    value_msat: create_invoice_request.amount_msat as i64,
1267                    expiry: i64::from(create_invoice_request.expiry_secs),
1268                    ..Default::default()
1269                },
1270            };
1271
1272            let hold_invoice_response = client
1273                .invoices()
1274                .add_hold_invoice(hold_invoice_request)
1275                .await
1276                .map_err(|e| LightningRpcError::FailedToGetInvoice {
1277                    failure_reason: e.to_string(),
1278                })?;
1279
1280            let invoice = hold_invoice_response.into_inner().payment_request;
1281            Ok(CreateInvoiceResponse { invoice })
1282        } else {
1283            let invoice = match description {
1284                InvoiceDescription::Direct(description) => Invoice {
1285                    memo: description,
1286                    value_msat: create_invoice_request.amount_msat as i64,
1287                    expiry: i64::from(create_invoice_request.expiry_secs),
1288                    ..Default::default()
1289                },
1290                InvoiceDescription::Hash(desc_hash) => Invoice {
1291                    description_hash: desc_hash.to_byte_array().to_vec(),
1292                    value_msat: create_invoice_request.amount_msat as i64,
1293                    expiry: i64::from(create_invoice_request.expiry_secs),
1294                    ..Default::default()
1295                },
1296            };
1297
1298            let add_invoice_response =
1299                client.lightning().add_invoice(invoice).await.map_err(|e| {
1300                    LightningRpcError::FailedToGetInvoice {
1301                        failure_reason: e.to_string(),
1302                    }
1303                })?;
1304
1305            let invoice = add_invoice_response.into_inner().payment_request;
1306            Ok(CreateInvoiceResponse { invoice })
1307        }
1308    }
1309
1310    async fn get_ln_onchain_address(
1311        &self,
1312    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
1313        let mut client = self.connect().await?;
1314
1315        match client
1316            .wallet()
1317            .next_addr(AddrRequest {
1318                account: String::new(), // Default wallet account.
1319                r#type: 4,              // Taproot address.
1320                change: false,
1321            })
1322            .await
1323        {
1324            Ok(response) => Ok(GetLnOnchainAddressResponse {
1325                address: response.into_inner().addr,
1326            }),
1327            Err(e) => Err(LightningRpcError::FailedToGetLnOnchainAddress {
1328                failure_reason: format!("Failed to get funding address {e:?}"),
1329            }),
1330        }
1331    }
1332
1333    async fn send_onchain(
1334        &self,
1335        SendOnchainRequest {
1336            address,
1337            amount,
1338            fee_rate_sats_per_vbyte,
1339        }: SendOnchainRequest,
1340    ) -> Result<SendOnchainResponse, LightningRpcError> {
1341        #[allow(deprecated)]
1342        let request = match amount {
1343            BitcoinAmountOrAll::All => SendCoinsRequest {
1344                addr: address.assume_checked().to_string(),
1345                amount: 0,
1346                target_conf: 0,
1347                sat_per_vbyte: fee_rate_sats_per_vbyte,
1348                sat_per_byte: 0,
1349                send_all: true,
1350                label: String::new(),
1351                min_confs: 0,
1352                spend_unconfirmed: true,
1353                ..Default::default()
1354            },
1355            BitcoinAmountOrAll::Amount(amount) => SendCoinsRequest {
1356                addr: address.assume_checked().to_string(),
1357                amount: amount.to_sat() as i64,
1358                target_conf: 0,
1359                sat_per_vbyte: fee_rate_sats_per_vbyte,
1360                sat_per_byte: 0,
1361                send_all: false,
1362                label: String::new(),
1363                min_confs: 0,
1364                spend_unconfirmed: true,
1365                ..Default::default()
1366            },
1367        };
1368
1369        match self.connect().await?.lightning().send_coins(request).await {
1370            Ok(res) => Ok(SendOnchainResponse {
1371                txid: res.into_inner().txid,
1372            }),
1373            Err(e) => Err(LightningRpcError::FailedToWithdrawOnchain {
1374                failure_reason: format!("Failed to withdraw funds on-chain {e:?}"),
1375            }),
1376        }
1377    }
1378
1379    async fn open_channel(
1380        &self,
1381        crate::OpenChannelRequest {
1382            pubkey,
1383            host,
1384            channel_size_sats,
1385            push_amount_sats,
1386        }: crate::OpenChannelRequest,
1387    ) -> Result<OpenChannelResponse, LightningRpcError> {
1388        let mut client = self.connect().await?;
1389
1390        let peers = client
1391            .lightning()
1392            .list_peers(ListPeersRequest { latest_error: true })
1393            .await
1394            .map_err(|e| LightningRpcError::FailedToConnectToPeer {
1395                failure_reason: format!("Could not list peers: {e:?}"),
1396            })?
1397            .into_inner();
1398
1399        // Connect to the peer first if we are not connected already
1400        if !peers.peers.into_iter().any(|peer| {
1401            PublicKey::from_str(&peer.pub_key).expect("could not parse public key") == pubkey
1402        }) {
1403            client
1404                .lightning()
1405                .connect_peer(ConnectPeerRequest {
1406                    addr: Some(LightningAddress {
1407                        pubkey: pubkey.to_string(),
1408                        host,
1409                    }),
1410                    perm: false,
1411                    timeout: 10,
1412                })
1413                .await
1414                .map_err(|e| LightningRpcError::FailedToConnectToPeer {
1415                    failure_reason: format!("Failed to connect to peer {e:?}"),
1416                })?;
1417        }
1418
1419        // Open the channel
1420        match client
1421            .lightning()
1422            .open_channel_sync(OpenChannelRequest {
1423                node_pubkey: pubkey.serialize().to_vec(),
1424                local_funding_amount: channel_size_sats.try_into().expect("u64 -> i64"),
1425                push_sat: push_amount_sats.try_into().expect("u64 -> i64"),
1426                ..Default::default()
1427            })
1428            .await
1429        {
1430            Ok(res) => Ok(OpenChannelResponse {
1431                funding_txid: match res.into_inner().funding_txid {
1432                    Some(txid) => match txid {
1433                        FundingTxid::FundingTxidBytes(mut bytes) => {
1434                            bytes.reverse();
1435                            hex::encode(bytes)
1436                        }
1437                        FundingTxid::FundingTxidStr(str) => str,
1438                    },
1439                    None => String::new(),
1440                },
1441            }),
1442            Err(e) => Err(LightningRpcError::FailedToOpenChannel {
1443                failure_reason: format!("Failed to open channel {e:?}"),
1444            }),
1445        }
1446    }
1447
1448    async fn close_channels_with_peer(
1449        &self,
1450        CloseChannelsWithPeerRequest {
1451            pubkey,
1452            force,
1453            sats_per_vbyte,
1454        }: CloseChannelsWithPeerRequest,
1455    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
1456        let mut client = self.connect().await?;
1457
1458        let channels_with_peer = client
1459            .lightning()
1460            .list_channels(ListChannelsRequest {
1461                active_only: false,
1462                inactive_only: false,
1463                public_only: false,
1464                private_only: false,
1465                peer: pubkey.serialize().to_vec(),
1466                peer_alias_lookup: false,
1467            })
1468            .await
1469            .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1470                failure_reason: format!("Failed to list channels {e:?}"),
1471            })?
1472            .into_inner()
1473            .channels;
1474
1475        for channel in &channels_with_peer {
1476            let channel_point =
1477                bitcoin::OutPoint::from_str(&channel.channel_point).map_err(|e| {
1478                    LightningRpcError::FailedToCloseChannelsWithPeer {
1479                        failure_reason: format!("Failed to parse channel point {e:?}"),
1480                    }
1481                })?;
1482
1483            if force {
1484                client
1485                    .lightning()
1486                    .close_channel(CloseChannelRequest {
1487                        channel_point: Some(ChannelPoint {
1488                            funding_txid: Some(
1489                                tonic_lnd::lnrpc::channel_point::FundingTxid::FundingTxidBytes(
1490                                    <bitcoin::Txid as AsRef<[u8]>>::as_ref(&channel_point.txid)
1491                                        .to_vec(),
1492                                ),
1493                            ),
1494                            output_index: channel_point.vout,
1495                        }),
1496                        force,
1497                        ..Default::default()
1498                    })
1499                    .await
1500                    .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1501                        failure_reason: format!("Failed to close channel {e:?}"),
1502                    })?;
1503            } else {
1504                client
1505                    .lightning()
1506                    .close_channel(CloseChannelRequest {
1507                        channel_point: Some(ChannelPoint {
1508                            funding_txid: Some(
1509                                tonic_lnd::lnrpc::channel_point::FundingTxid::FundingTxidBytes(
1510                                    <bitcoin::Txid as AsRef<[u8]>>::as_ref(&channel_point.txid)
1511                                        .to_vec(),
1512                                ),
1513                            ),
1514                            output_index: channel_point.vout,
1515                        }),
1516                        force,
1517                        sat_per_vbyte: sats_per_vbyte.unwrap_or_default(),
1518                        ..Default::default()
1519                    })
1520                    .await
1521                    .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1522                        failure_reason: format!("Failed to close channel {e:?}"),
1523                    })?;
1524            }
1525        }
1526
1527        Ok(CloseChannelsWithPeerResponse {
1528            num_channels_closed: channels_with_peer.len() as u32,
1529        })
1530    }
1531
1532    async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
1533        let mut client = self.connect().await?;
1534
1535        // Fetch peer addresses so we can populate remote_address on each channel
1536        let peer_addresses: std::collections::HashMap<String, String> = client
1537            .lightning()
1538            .list_peers(ListPeersRequest {
1539                latest_error: false,
1540            })
1541            .await
1542            .map(|resp| {
1543                resp.into_inner()
1544                    .peers
1545                    .into_iter()
1546                    .filter_map(|peer| {
1547                        if peer.address.is_empty() {
1548                            None
1549                        } else {
1550                            Some((peer.pub_key, peer.address))
1551                        }
1552                    })
1553                    .collect()
1554            })
1555            .unwrap_or_default();
1556
1557        match client
1558            .lightning()
1559            .list_channels(ListChannelsRequest {
1560                active_only: false,
1561                inactive_only: false,
1562                public_only: false,
1563                private_only: false,
1564                peer: vec![],
1565                peer_alias_lookup: true,
1566            })
1567            .await
1568        {
1569            Ok(response) => Ok(ListChannelsResponse {
1570                channels: response
1571                    .into_inner()
1572                    .channels
1573                    .into_iter()
1574                    .map(|channel| {
1575                        let channel_size_sats = channel.capacity.try_into().expect("i64 -> u64");
1576
1577                        let local_balance_sats: u64 =
1578                            channel.local_balance.try_into().expect("i64 -> u64");
1579                        let local_channel_reserve_sats: u64 = match channel.local_constraints {
1580                            Some(constraints) => constraints.chan_reserve_sat,
1581                            None => 0,
1582                        };
1583
1584                        let outbound_liquidity_sats =
1585                            local_balance_sats.saturating_sub(local_channel_reserve_sats);
1586
1587                        let remote_balance_sats: u64 =
1588                            channel.remote_balance.try_into().expect("i64 -> u64");
1589                        let remote_channel_reserve_sats: u64 = match channel.remote_constraints {
1590                            Some(constraints) => constraints.chan_reserve_sat,
1591                            None => 0,
1592                        };
1593
1594                        let inbound_liquidity_sats =
1595                            remote_balance_sats.saturating_sub(remote_channel_reserve_sats);
1596
1597                        let funding_outpoint = OutPoint::from_str(&channel.channel_point).ok();
1598
1599                        let remote_address = peer_addresses.get(&channel.remote_pubkey).cloned();
1600
1601                        ChannelInfo {
1602                            remote_pubkey: PublicKey::from_str(&channel.remote_pubkey)
1603                                .expect("Lightning node returned invalid remote channel pubkey"),
1604                            channel_size_sats,
1605                            outbound_liquidity_sats,
1606                            inbound_liquidity_sats,
1607                            is_active: channel.active,
1608                            funding_outpoint,
1609                            remote_node_alias: if channel.peer_alias.is_empty() {
1610                                None
1611                            } else {
1612                                Some(channel.peer_alias.clone())
1613                            },
1614                            remote_address,
1615                        }
1616                    })
1617                    .collect(),
1618            }),
1619            Err(e) => Err(LightningRpcError::FailedToListChannels {
1620                failure_reason: format!("Failed to list active channels {e:?}"),
1621            }),
1622        }
1623    }
1624
1625    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
1626        let mut client = self.connect().await?;
1627
1628        let wallet_balance_response = client
1629            .lightning()
1630            .wallet_balance(WalletBalanceRequest {
1631                ..Default::default()
1632            })
1633            .await
1634            .map_err(|e| LightningRpcError::FailedToGetBalances {
1635                failure_reason: format!("Failed to get on-chain balance {e:?}"),
1636            })?
1637            .into_inner();
1638
1639        let channel_balance_response = client
1640            .lightning()
1641            .channel_balance(ChannelBalanceRequest {})
1642            .await
1643            .map_err(|e| LightningRpcError::FailedToGetBalances {
1644                failure_reason: format!("Failed to get lightning balance {e:?}"),
1645            })?
1646            .into_inner();
1647        let total_outbound = channel_balance_response.local_balance.unwrap_or_default();
1648        let unsettled_outbound = channel_balance_response
1649            .unsettled_local_balance
1650            .unwrap_or_default();
1651        let pending_outbound = channel_balance_response
1652            .pending_open_local_balance
1653            .unwrap_or_default();
1654        let lightning_balance_msats = total_outbound
1655            .msat
1656            .saturating_sub(unsettled_outbound.msat)
1657            .saturating_sub(pending_outbound.msat);
1658
1659        let total_inbound = channel_balance_response.remote_balance.unwrap_or_default();
1660        let unsettled_inbound = channel_balance_response
1661            .unsettled_remote_balance
1662            .unwrap_or_default();
1663        let pending_inbound = channel_balance_response
1664            .pending_open_remote_balance
1665            .unwrap_or_default();
1666        let inbound_lightning_liquidity_msats = total_inbound
1667            .msat
1668            .saturating_sub(unsettled_inbound.msat)
1669            .saturating_sub(pending_inbound.msat);
1670
1671        Ok(GetBalancesResponse {
1672            onchain_balance_sats: (wallet_balance_response.total_balance
1673                + wallet_balance_response.reserved_balance_anchor_chan)
1674                as u64,
1675            lightning_balance_msats,
1676            inbound_lightning_liquidity_msats,
1677        })
1678    }
1679
1680    async fn get_invoice(
1681        &self,
1682        get_invoice_request: GetInvoiceRequest,
1683    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
1684        let mut client = self.connect().await?;
1685        let invoice = client
1686            .invoices()
1687            .lookup_invoice_v2(LookupInvoiceMsg {
1688                invoice_ref: Some(InvoiceRef::PaymentHash(
1689                    get_invoice_request.payment_hash.consensus_encode_to_vec(),
1690                )),
1691                ..Default::default()
1692            })
1693            .await;
1694        let invoice = match invoice {
1695            Ok(invoice) => invoice.into_inner(),
1696            Err(_) => return Ok(None),
1697        };
1698        let preimage: [u8; 32] = invoice
1699            .clone()
1700            .r_preimage
1701            .try_into()
1702            .expect("Could not convert preimage");
1703        let status = match &invoice.state() {
1704            InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
1705            InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
1706            _ => fedimint_gateway_common::PaymentStatus::Pending,
1707        };
1708
1709        Ok(Some(GetInvoiceResponse {
1710            preimage: Some(preimage.consensus_encode_to_hex()),
1711            payment_hash: Some(
1712                sha256::Hash::from_slice(&invoice.r_hash).expect("Could not convert payment hash"),
1713            ),
1714            amount: Amount::from_msats(invoice.value_msat as u64),
1715            created_at: UNIX_EPOCH + Duration::from_secs(invoice.creation_date as u64),
1716            status,
1717        }))
1718    }
1719
1720    async fn list_transactions(
1721        &self,
1722        start_secs: u64,
1723        end_secs: u64,
1724    ) -> Result<ListTransactionsResponse, LightningRpcError> {
1725        let mut client = self.connect().await?;
1726        let payments = client
1727            .lightning()
1728            .list_payments(ListPaymentsRequest {
1729                // On higher versions on LND, we can filter on the time range directly in the query
1730                ..Default::default()
1731            })
1732            .await
1733            .map_err(|err| LightningRpcError::FailedToListTransactions {
1734                failure_reason: err.to_string(),
1735            })?
1736            .into_inner();
1737
1738        let mut payments = payments
1739            .payments
1740            .iter()
1741            .filter_map(|payment| {
1742                let timestamp_secs = (payment.creation_time_ns / 1_000_000_000) as u64;
1743                if timestamp_secs < start_secs || timestamp_secs >= end_secs {
1744                    return None;
1745                }
1746                let payment_hash = sha256::Hash::from_str(&payment.payment_hash).ok();
1747                let preimage = (!payment.payment_preimage.is_empty())
1748                    .then_some(payment.payment_preimage.clone());
1749                let status = match &payment.status() {
1750                    PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1751                    PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1752                    _ => fedimint_gateway_common::PaymentStatus::Pending,
1753                };
1754                Some(PaymentDetails {
1755                    payment_hash,
1756                    preimage,
1757                    payment_kind: PaymentKind::Bolt11,
1758                    amount: Amount::from_msats(payment.value_msat as u64),
1759                    direction: PaymentDirection::Outbound,
1760                    status,
1761                    timestamp_secs,
1762                })
1763            })
1764            .collect::<Vec<_>>();
1765
1766        let invoices = client
1767            .lightning()
1768            .list_invoices(ListInvoiceRequest {
1769                pending_only: false,
1770                // On higher versions on LND, we can filter on the time range directly in the query
1771                ..Default::default()
1772            })
1773            .await
1774            .map_err(|err| LightningRpcError::FailedToListTransactions {
1775                failure_reason: err.to_string(),
1776            })?
1777            .into_inner();
1778
1779        let mut incoming_payments = invoices
1780            .invoices
1781            .iter()
1782            .filter_map(|invoice| {
1783                let timestamp_secs = invoice.settle_date as u64;
1784                if timestamp_secs < start_secs || timestamp_secs >= end_secs {
1785                    return None;
1786                }
1787                let status = match &invoice.state() {
1788                    InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
1789                    InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
1790                    _ => return None,
1791                };
1792                let preimage = (!invoice.r_preimage.is_empty())
1793                    .then_some(invoice.r_preimage.encode_hex::<String>());
1794                Some(PaymentDetails {
1795                    payment_hash: Some(
1796                        sha256::Hash::from_slice(&invoice.r_hash)
1797                            .expect("Could not convert payment hash"),
1798                    ),
1799                    preimage,
1800                    payment_kind: PaymentKind::Bolt11,
1801                    amount: Amount::from_msats(invoice.value_msat as u64),
1802                    direction: PaymentDirection::Inbound,
1803                    status,
1804                    timestamp_secs,
1805                })
1806            })
1807            .collect::<Vec<_>>();
1808
1809        payments.append(&mut incoming_payments);
1810        payments.sort_by_key(|p| p.timestamp_secs);
1811
1812        Ok(ListTransactionsResponse {
1813            transactions: payments,
1814        })
1815    }
1816
1817    fn create_offer(
1818        &self,
1819        _amount_msat: Option<Amount>,
1820        _description: Option<String>,
1821        _expiry_secs: Option<u32>,
1822        _quantity: Option<u64>,
1823    ) -> Result<String, LightningRpcError> {
1824        Err(LightningRpcError::Bolt12Error {
1825            failure_reason: "LND Does not support Bolt12".to_string(),
1826        })
1827    }
1828
1829    async fn pay_offer(
1830        &self,
1831        _offer: String,
1832        _quantity: Option<u64>,
1833        _amount: Option<Amount>,
1834        _payer_note: Option<String>,
1835    ) -> Result<Preimage, LightningRpcError> {
1836        Err(LightningRpcError::Bolt12Error {
1837            failure_reason: "LND Does not support Bolt12".to_string(),
1838        })
1839    }
1840
1841    fn sync_wallet(&self) -> Result<(), LightningRpcError> {
1842        // There is nothing explicit needed to do for syncing an LND node
1843        Ok(())
1844    }
1845}
1846
1847fn route_hints_to_lnd(
1848    route_hints: &[fedimint_ln_common::route_hints::RouteHint],
1849) -> Vec<tonic_lnd::lnrpc::RouteHint> {
1850    route_hints
1851        .iter()
1852        .map(|hint| tonic_lnd::lnrpc::RouteHint {
1853            hop_hints: hint
1854                .0
1855                .iter()
1856                .map(|hop| tonic_lnd::lnrpc::HopHint {
1857                    node_id: hop.src_node_id.serialize().encode_hex(),
1858                    chan_id: hop.short_channel_id,
1859                    fee_base_msat: hop.base_msat,
1860                    fee_proportional_millionths: hop.proportional_millionths,
1861                    cltv_expiry_delta: u32::from(hop.cltv_expiry_delta),
1862                })
1863                .collect(),
1864        })
1865        .collect()
1866}
1867
1868fn wire_features_to_lnd_feature_vec(features_wire_encoded: &[u8]) -> anyhow::Result<Vec<i32>> {
1869    ensure!(
1870        features_wire_encoded.len() <= 1_000,
1871        "Will not process feature bit vectors larger than 1000 byte"
1872    );
1873
1874    let lnd_features = features_wire_encoded
1875        .iter()
1876        .rev()
1877        .enumerate()
1878        .flat_map(|(byte_idx, &feature_byte)| {
1879            (0..8).filter_map(move |bit_idx| {
1880                if (feature_byte & (1u8 << bit_idx)) != 0 {
1881                    Some(
1882                        i32::try_from(byte_idx * 8 + bit_idx)
1883                            .expect("Index will never exceed i32::MAX for feature vectors <8MB"),
1884                    )
1885                } else {
1886                    None
1887                }
1888            })
1889        })
1890        .collect::<Vec<_>>();
1891
1892    Ok(lnd_features)
1893}
1894
1895/// Utility struct for logging payment hashes. Useful for debugging.
1896struct PrettyPaymentHash<'a>(&'a Vec<u8>);
1897
1898impl Display for PrettyPaymentHash<'_> {
1899    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1900        write!(f, "payment_hash={}", self.0.encode_hex::<String>())
1901    }
1902}
1903
1904#[cfg(test)]
1905mod tests;