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