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