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