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