Skip to main content

fedimint_lightning/
lnd.rs

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