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