Skip to main content

fedimint_lightning/
lnd.rs

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