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, ListActiveChannelsResponse,
50    MAX_LIGHTNING_RETRIES, 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.clone() 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 }: 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                    ..Default::default()
1271                })
1272                .await
1273                .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1274                    failure_reason: format!("Failed to close channel {e:?}"),
1275                })?;
1276        }
1277
1278        Ok(CloseChannelsWithPeerResponse {
1279            num_channels_closed: channels_with_peer.len() as u32,
1280        })
1281    }
1282
1283    async fn list_active_channels(&self) -> Result<ListActiveChannelsResponse, LightningRpcError> {
1284        let mut client = self.connect().await?;
1285
1286        match client
1287            .lightning()
1288            .list_channels(ListChannelsRequest {
1289                active_only: true,
1290                inactive_only: false,
1291                public_only: false,
1292                private_only: false,
1293                peer: vec![],
1294            })
1295            .await
1296        {
1297            Ok(response) => Ok(ListActiveChannelsResponse {
1298                channels: response
1299                    .into_inner()
1300                    .channels
1301                    .into_iter()
1302                    .map(|channel| {
1303                        let channel_size_sats = channel.capacity.try_into().expect("i64 -> u64");
1304
1305                        let local_balance_sats: u64 =
1306                            channel.local_balance.try_into().expect("i64 -> u64");
1307                        let local_channel_reserve_sats: u64 = match channel.local_constraints {
1308                            Some(constraints) => constraints.chan_reserve_sat,
1309                            None => 0,
1310                        };
1311
1312                        let outbound_liquidity_sats =
1313                            local_balance_sats.saturating_sub(local_channel_reserve_sats);
1314
1315                        let remote_balance_sats: u64 =
1316                            channel.remote_balance.try_into().expect("i64 -> u64");
1317                        let remote_channel_reserve_sats: u64 = match channel.remote_constraints {
1318                            Some(constraints) => constraints.chan_reserve_sat,
1319                            None => 0,
1320                        };
1321
1322                        let inbound_liquidity_sats =
1323                            remote_balance_sats.saturating_sub(remote_channel_reserve_sats);
1324
1325                        ChannelInfo {
1326                            remote_pubkey: PublicKey::from_str(&channel.remote_pubkey)
1327                                .expect("Lightning node returned invalid remote channel pubkey"),
1328                            channel_size_sats,
1329                            outbound_liquidity_sats,
1330                            inbound_liquidity_sats,
1331                            short_channel_id: channel.chan_id,
1332                        }
1333                    })
1334                    .collect(),
1335            }),
1336            Err(e) => Err(LightningRpcError::FailedToListActiveChannels {
1337                failure_reason: format!("Failed to list active channels {e:?}"),
1338            }),
1339        }
1340    }
1341
1342    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
1343        let mut client = self.connect().await?;
1344
1345        let wallet_balance_response = client
1346            .lightning()
1347            .wallet_balance(WalletBalanceRequest {})
1348            .await
1349            .map_err(|e| LightningRpcError::FailedToGetBalances {
1350                failure_reason: format!("Failed to get on-chain balance {e:?}"),
1351            })?
1352            .into_inner();
1353
1354        let channel_balance_response = client
1355            .lightning()
1356            .channel_balance(ChannelBalanceRequest {})
1357            .await
1358            .map_err(|e| LightningRpcError::FailedToGetBalances {
1359                failure_reason: format!("Failed to get lightning balance {e:?}"),
1360            })?
1361            .into_inner();
1362        let total_outbound = channel_balance_response.local_balance.unwrap_or_default();
1363        let unsettled_outbound = channel_balance_response
1364            .unsettled_local_balance
1365            .unwrap_or_default();
1366        let pending_outbound = channel_balance_response
1367            .pending_open_local_balance
1368            .unwrap_or_default();
1369        let lightning_balance_msats =
1370            total_outbound.msat - unsettled_outbound.msat - pending_outbound.msat;
1371
1372        let total_inbound = channel_balance_response.remote_balance.unwrap_or_default();
1373        let unsettled_inbound = channel_balance_response
1374            .unsettled_remote_balance
1375            .unwrap_or_default();
1376        let pending_inbound = channel_balance_response
1377            .pending_open_remote_balance
1378            .unwrap_or_default();
1379        let inbound_lightning_liquidity_msats =
1380            total_inbound.msat - unsettled_inbound.msat - pending_inbound.msat;
1381
1382        Ok(GetBalancesResponse {
1383            onchain_balance_sats: (wallet_balance_response.total_balance
1384                + wallet_balance_response.reserved_balance_anchor_chan)
1385                as u64,
1386            lightning_balance_msats,
1387            inbound_lightning_liquidity_msats,
1388        })
1389    }
1390
1391    async fn get_invoice(
1392        &self,
1393        get_invoice_request: GetInvoiceRequest,
1394    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
1395        let mut client = self.connect().await?;
1396        let invoice = client
1397            .invoices()
1398            .lookup_invoice_v2(LookupInvoiceMsg {
1399                invoice_ref: Some(InvoiceRef::PaymentHash(
1400                    get_invoice_request.payment_hash.consensus_encode_to_vec(),
1401                )),
1402                ..Default::default()
1403            })
1404            .await;
1405        let invoice = match invoice {
1406            Ok(invoice) => invoice.into_inner(),
1407            Err(_) => return Ok(None),
1408        };
1409        let preimage: [u8; 32] = invoice
1410            .clone()
1411            .r_preimage
1412            .try_into()
1413            .expect("Could not convert preimage");
1414        let status = match &invoice.state() {
1415            InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
1416            InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
1417            _ => fedimint_gateway_common::PaymentStatus::Pending,
1418        };
1419
1420        Ok(Some(GetInvoiceResponse {
1421            preimage: Some(preimage.consensus_encode_to_hex()),
1422            payment_hash: Some(
1423                sha256::Hash::from_slice(&invoice.r_hash).expect("Could not convert payment hash"),
1424            ),
1425            amount: Amount::from_msats(invoice.value_msat as u64),
1426            created_at: UNIX_EPOCH + Duration::from_secs(invoice.creation_date as u64),
1427            status,
1428        }))
1429    }
1430
1431    async fn list_transactions(
1432        &self,
1433        start_secs: u64,
1434        end_secs: u64,
1435    ) -> Result<ListTransactionsResponse, LightningRpcError> {
1436        let mut client = self.connect().await?;
1437        let payments = client
1438            .lightning()
1439            .list_payments(ListPaymentsRequest {
1440                // On higher versions on LND, we can filter on the time range directly in the query
1441                ..Default::default()
1442            })
1443            .await
1444            .map_err(|err| LightningRpcError::FailedToListTransactions {
1445                failure_reason: err.to_string(),
1446            })?
1447            .into_inner();
1448
1449        let mut payments = payments
1450            .payments
1451            .iter()
1452            .filter_map(|payment| {
1453                let timestamp_secs = (payment.creation_time_ns / 1_000_000_000) as u64;
1454                if timestamp_secs < start_secs || timestamp_secs >= end_secs {
1455                    return None;
1456                }
1457                let payment_hash = sha256::Hash::from_str(&payment.payment_hash).ok();
1458                let preimage = (!payment.payment_preimage.is_empty())
1459                    .then_some(payment.payment_preimage.clone());
1460                let status = match &payment.status() {
1461                    PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1462                    PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1463                    _ => fedimint_gateway_common::PaymentStatus::Pending,
1464                };
1465                Some(PaymentDetails {
1466                    payment_hash,
1467                    preimage,
1468                    payment_kind: PaymentKind::Bolt11,
1469                    amount: Amount::from_msats(payment.value_msat as u64),
1470                    direction: PaymentDirection::Outbound,
1471                    status,
1472                    timestamp_secs,
1473                })
1474            })
1475            .collect::<Vec<_>>();
1476
1477        let invoices = client
1478            .lightning()
1479            .list_invoices(ListInvoiceRequest {
1480                pending_only: false,
1481                // On higher versions on LND, we can filter on the time range directly in the query
1482                ..Default::default()
1483            })
1484            .await
1485            .map_err(|err| LightningRpcError::FailedToListTransactions {
1486                failure_reason: err.to_string(),
1487            })?
1488            .into_inner();
1489
1490        let mut incoming_payments = invoices
1491            .invoices
1492            .iter()
1493            .filter_map(|invoice| {
1494                let timestamp_secs = invoice.settle_date as u64;
1495                if timestamp_secs < start_secs || timestamp_secs >= end_secs {
1496                    return None;
1497                }
1498                let status = match &invoice.state() {
1499                    InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
1500                    InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
1501                    _ => return None,
1502                };
1503                let preimage = (!invoice.r_preimage.is_empty())
1504                    .then_some(invoice.r_preimage.encode_hex::<String>());
1505                Some(PaymentDetails {
1506                    payment_hash: Some(
1507                        sha256::Hash::from_slice(&invoice.r_hash)
1508                            .expect("Could not convert payment hash"),
1509                    ),
1510                    preimage,
1511                    payment_kind: PaymentKind::Bolt11,
1512                    amount: Amount::from_msats(invoice.value_msat as u64),
1513                    direction: PaymentDirection::Inbound,
1514                    status,
1515                    timestamp_secs,
1516                })
1517            })
1518            .collect::<Vec<_>>();
1519
1520        payments.append(&mut incoming_payments);
1521        payments.sort_by_key(|p| p.timestamp_secs);
1522
1523        Ok(ListTransactionsResponse {
1524            transactions: payments,
1525        })
1526    }
1527
1528    fn create_offer(
1529        &self,
1530        _amount_msat: Option<Amount>,
1531        _description: Option<String>,
1532        _expiry_secs: Option<u32>,
1533        _quantity: Option<u64>,
1534    ) -> Result<String, LightningRpcError> {
1535        Err(LightningRpcError::Bolt12Error {
1536            failure_reason: "LND Does not support Bolt12".to_string(),
1537        })
1538    }
1539
1540    async fn pay_offer(
1541        &self,
1542        _offer: String,
1543        _quantity: Option<u64>,
1544        _amount: Option<Amount>,
1545        _payer_note: Option<String>,
1546    ) -> Result<Preimage, LightningRpcError> {
1547        Err(LightningRpcError::Bolt12Error {
1548            failure_reason: "LND Does not support Bolt12".to_string(),
1549        })
1550    }
1551}
1552
1553fn route_hints_to_lnd(
1554    route_hints: &[fedimint_ln_common::route_hints::RouteHint],
1555) -> Vec<tonic_lnd::lnrpc::RouteHint> {
1556    route_hints
1557        .iter()
1558        .map(|hint| tonic_lnd::lnrpc::RouteHint {
1559            hop_hints: hint
1560                .0
1561                .iter()
1562                .map(|hop| tonic_lnd::lnrpc::HopHint {
1563                    node_id: hop.src_node_id.serialize().encode_hex(),
1564                    chan_id: hop.short_channel_id,
1565                    fee_base_msat: hop.base_msat,
1566                    fee_proportional_millionths: hop.proportional_millionths,
1567                    cltv_expiry_delta: u32::from(hop.cltv_expiry_delta),
1568                })
1569                .collect(),
1570        })
1571        .collect()
1572}
1573
1574fn wire_features_to_lnd_feature_vec(features_wire_encoded: &[u8]) -> anyhow::Result<Vec<i32>> {
1575    ensure!(
1576        features_wire_encoded.len() <= 1_000,
1577        "Will not process feature bit vectors larger than 1000 byte"
1578    );
1579
1580    let lnd_features = features_wire_encoded
1581        .iter()
1582        .rev()
1583        .enumerate()
1584        .flat_map(|(byte_idx, &feature_byte)| {
1585            (0..8).filter_map(move |bit_idx| {
1586                if (feature_byte & (1u8 << bit_idx)) != 0 {
1587                    Some(
1588                        i32::try_from(byte_idx * 8 + bit_idx)
1589                            .expect("Index will never exceed i32::MAX for feature vectors <8MB"),
1590                    )
1591                } else {
1592                    None
1593                }
1594            })
1595        })
1596        .collect::<Vec<_>>();
1597
1598    Ok(lnd_features)
1599}
1600
1601/// Utility struct for logging payment hashes. Useful for debugging.
1602struct PrettyPaymentHash<'a>(&'a Vec<u8>);
1603
1604impl Display for PrettyPaymentHash<'_> {
1605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1606        write!(f, "payment_hash={}", self.0.encode_hex::<String>())
1607    }
1608}
1609
1610#[cfg(test)]
1611mod tests;