Skip to main content

fedimint_lightning/
lnd.rs

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