fedimint_lightning/
lnd.rs

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