1use std::fmt::{self, Display};
2use std::str::FromStr;
3use std::sync::Arc;
4use std::time::{Duration, UNIX_EPOCH};
5
6use anyhow::ensure;
7use async_trait::async_trait;
8use bitcoin::OutPoint;
9use bitcoin::hashes::{Hash, sha256};
10use fedimint_core::encoding::Encodable;
11use fedimint_core::task::{TaskGroup, sleep};
12use fedimint_core::util::FmtCompact;
13use fedimint_core::{Amount, BitcoinAmountOrAll, crit, secp256k1};
14use fedimint_gateway_common::{
15 ListTransactionsResponse, PaymentDetails, PaymentDirection, PaymentKind,
16};
17use fedimint_ln_common::PrunedInvoice;
18use fedimint_ln_common::contracts::Preimage;
19use fedimint_ln_common::route_hints::{RouteHint, RouteHintHop};
20use fedimint_logging::LOG_LIGHTNING;
21use hex::ToHex;
22use secp256k1::PublicKey;
23use tokio::sync::mpsc;
24use tokio_stream::wrappers::ReceiverStream;
25use tonic_lnd::invoicesrpc::lookup_invoice_msg::InvoiceRef;
26use tonic_lnd::invoicesrpc::{
27 AddHoldInvoiceRequest, CancelInvoiceMsg, LookupInvoiceMsg, SettleInvoiceMsg,
28 SubscribeSingleInvoiceRequest,
29};
30use tonic_lnd::lnrpc::channel_point::FundingTxid;
31use tonic_lnd::lnrpc::failure::FailureCode;
32use tonic_lnd::lnrpc::invoice::InvoiceState;
33use tonic_lnd::lnrpc::payment::PaymentStatus;
34use tonic_lnd::lnrpc::{
35 ChanInfoRequest, ChannelBalanceRequest, ChannelPoint, CloseChannelRequest, ConnectPeerRequest,
36 GetInfoRequest, Invoice, InvoiceSubscription, LightningAddress, ListChannelsRequest,
37 ListInvoiceRequest, ListPaymentsRequest, ListPeersRequest, OpenChannelRequest,
38 SendCoinsRequest, WalletBalanceRequest,
39};
40use tonic_lnd::routerrpc::{
41 CircuitKey, ForwardHtlcInterceptResponse, ResolveHoldForwardAction, SendPaymentRequest,
42 TrackPaymentRequest,
43};
44use tonic_lnd::tonic::Code;
45use tonic_lnd::walletrpc::AddrRequest;
46use tonic_lnd::{Client as LndClient, connect};
47use tracing::{debug, info, trace, warn};
48
49use super::{
50 ChannelInfo, ILnRpcClient, LightningRpcError, ListChannelsResponse, Lnv2HoldInvoiceFilter,
51 MAX_LIGHTNING_RETRIES, RouteHtlcStream,
52};
53use crate::{
54 CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest,
55 CreateInvoiceResponse, GetBalancesResponse, GetInvoiceRequest, GetInvoiceResponse,
56 GetLnOnchainAddressResponse, GetNodeInfoResponse, GetRouteHintsResponse,
57 InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription, NO_INCOMING_CIRCUIT,
58 OpenChannelResponse, PayInvoiceResponse, PaymentAction, SendOnchainRequest,
59 SendOnchainResponse,
60};
61
62type HtlcSubscriptionSender = mpsc::Sender<InterceptPaymentRequest>;
63
64const LND_PAYMENT_TIMEOUT_SECONDS: i32 = 180;
65
66#[derive(Debug, Clone, Copy, Eq, PartialEq)]
67enum HoldInvoiceAction {
68 Complete,
69 AlreadyComplete,
70}
71
72#[derive(Debug, Clone, Copy, Eq, PartialEq)]
73struct HoldInvoiceStateError {
74 failure_reason: &'static str,
75 permanent: bool,
76}
77
78fn hold_invoice_action(
79 requested_action: PaymentActionKind,
80 invoice_state: Option<InvoiceState>,
81) -> Result<HoldInvoiceAction, HoldInvoiceStateError> {
82 match (requested_action, invoice_state) {
83 (PaymentActionKind::Settle, Some(InvoiceState::Accepted))
84 | (PaymentActionKind::Cancel, Some(InvoiceState::Open | InvoiceState::Accepted)) => {
85 Ok(HoldInvoiceAction::Complete)
86 }
87 (PaymentActionKind::Settle, Some(InvoiceState::Settled))
88 | (PaymentActionKind::Cancel, Some(InvoiceState::Canceled)) => {
89 Ok(HoldInvoiceAction::AlreadyComplete)
90 }
91 (PaymentActionKind::Settle, Some(InvoiceState::Canceled)) => Err(HoldInvoiceStateError {
92 failure_reason: "HOLD invoice was canceled instead of settled",
93 permanent: true,
94 }),
95 (PaymentActionKind::Cancel, Some(InvoiceState::Settled)) => Err(HoldInvoiceStateError {
96 failure_reason: "HOLD invoice was settled instead of canceled",
97 permanent: true,
98 }),
99 (PaymentActionKind::Settle, Some(InvoiceState::Open)) => Err(HoldInvoiceStateError {
100 failure_reason: "HOLD invoice is open and has no accepted HTLC to settle",
101 permanent: false,
102 }),
103 (_, None) => Err(HoldInvoiceStateError {
104 failure_reason: "HOLD invoice does not exist",
105 permanent: true,
106 }),
107 }
108}
109
110#[derive(Debug, Clone, Copy, Eq, PartialEq)]
111enum PaymentActionKind {
112 Settle,
113 Cancel,
114}
115
116#[derive(Clone)]
117pub struct GatewayLndClient {
118 address: String,
120 tls_cert: String,
121 macaroon: String,
122 lnd_sender: Option<mpsc::Sender<ForwardHtlcInterceptResponse>>,
123 lnv2_filter: Lnv2HoldInvoiceFilter,
129}
130
131impl GatewayLndClient {
132 pub fn new(
133 address: String,
134 tls_cert: String,
135 macaroon: String,
136 lnd_sender: Option<mpsc::Sender<ForwardHtlcInterceptResponse>>,
137 lnv2_filter: Lnv2HoldInvoiceFilter,
138 ) -> Self {
139 info!(
140 target: LOG_LIGHTNING,
141 address = %address,
142 tls_cert_path = %tls_cert,
143 macaroon = %macaroon,
144 "Gateway configured to connect to LND LnRpcClient",
145 );
146 GatewayLndClient {
147 address,
148 tls_cert,
149 macaroon,
150 lnd_sender,
151 lnv2_filter,
152 }
153 }
154
155 async fn connect(&self) -> Result<LndClient, LightningRpcError> {
156 let mut retries = 0;
157 let client = loop {
158 if retries >= MAX_LIGHTNING_RETRIES {
159 return Err(LightningRpcError::FailedToConnect);
160 }
161
162 retries += 1;
163
164 match connect(
165 self.address.clone(),
166 self.tls_cert.clone(),
167 self.macaroon.clone(),
168 )
169 .await
170 {
171 Ok(client) => break client,
172 Err(err) => {
173 debug!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Couldn't connect to LND, retrying in 1 second...");
174 sleep(Duration::from_secs(1)).await;
175 }
176 }
177 };
178
179 Ok(client)
180 }
181
182 async fn spawn_lnv2_hold_invoice_subscription(
187 &self,
188 task_group: &TaskGroup,
189 payment_stream_group: TaskGroup,
190 gateway_sender: HtlcSubscriptionSender,
191 payment_hash: Vec<u8>,
192 ) -> Result<(), LightningRpcError> {
193 let mut client = self.connect().await?;
194
195 let self_copy = self.clone();
196 let r_hash = payment_hash.clone();
197 task_group.spawn("LND HOLD Invoice Subscription", |handle| async move {
198 let future_stream =
199 client
200 .invoices()
201 .subscribe_single_invoice(SubscribeSingleInvoiceRequest {
202 r_hash: r_hash.clone(),
203 });
204
205 let mut hold_stream = tokio::select! {
206 stream = future_stream => {
207 match stream {
208 Ok(stream) => stream.into_inner(),
209 Err(err) => {
210 crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to subscribe to hold invoice updates, shutting down payment-stream subgroup to trigger gateway reconnect");
211 payment_stream_group.shutdown();
212 return;
213 }
214 }
215 },
216 () = handle.make_shutdown_rx() => {
217 info!(target: LOG_LIGHTNING, "LND HOLD Invoice Subscription received shutdown signal");
218 return;
219 }
220 };
221
222 loop {
223 let hold = tokio::select! {
224 () = handle.make_shutdown_rx() => {
225 info!(target: LOG_LIGHTNING, "LND HOLD Invoice Subscription received shutdown signal");
226 break;
227 }
228 hold_update = hold_stream.message() => {
229 match hold_update {
230 Ok(Some(hold)) => hold,
231 Ok(None) => {
232 break;
236 }
237 Err(err) => {
238 crit!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over hold invoice update stream, shutting down payment-stream subgroup to trigger gateway reconnect");
239 payment_stream_group.shutdown();
240 break;
241 }
242 }
243 }
244 };
245
246 debug!(
247 target: LOG_LIGHTNING,
248 payment_hash = %PrettyPaymentHash(&r_hash),
249 state = %hold.state,
250 "LND HOLD Invoice Update",
251 );
252
253 if hold.state() == InvoiceState::Accepted {
254 let hash = sha256::Hash::from_slice(&hold.r_hash)
263 .expect("LND payment hashes are 32 bytes");
264 if !(self_copy.lnv2_filter)(hash).await {
265 trace!(
266 target: LOG_LIGHTNING,
267 payment_hash = %PrettyPaymentHash(&hold.r_hash),
268 "Ignoring HOLD invoice not created by this gateway",
269 );
270 continue;
271 }
272
273 let (incoming_chan_id, htlc_id) = NO_INCOMING_CIRCUIT;
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 expiry: hold.expiry as u32,
281 short_channel_id: Some(0),
282 incoming_chan_id,
287 htlc_id,
288 };
289
290 match gateway_sender.send(intercept).await {
291 Ok(()) => {}
292 Err(err) => {
293 warn!(
294 target: LOG_LIGHTNING,
295 err = %err.fmt_compact(),
296 "Hold Invoice Subscription failed to send Intercept to gateway"
297 );
298 let _ = self_copy.cancel_hold_invoice(hold.r_hash).await;
299 }
300 }
301 }
302 }
303 });
304
305 Ok(())
306 }
307
308 async fn spawn_lnv2_invoice_subscription(
314 &self,
315 task_group: &TaskGroup,
316 gateway_sender: HtlcSubscriptionSender,
317 ) -> Result<(), LightningRpcError> {
318 let mut client = self.connect().await?;
319
320 let first_index_offset = client
322 .lightning()
323 .list_invoices(ListInvoiceRequest {
324 pending_only: true,
325 index_offset: 0,
326 num_max_invoices: u64::MAX,
327 reversed: false,
328 ..Default::default()
329 })
330 .await
331 .map_err(|status| {
332 warn!(target: LOG_LIGHTNING, status = %status, "Failed to list all invoices");
333 LightningRpcError::FailedToRouteHtlcs {
334 failure_reason: "Failed to list all invoices".to_string(),
335 }
336 })?
337 .into_inner()
338 .first_index_offset;
339
340 let add_index = first_index_offset.saturating_sub(1);
347
348 let self_copy = self.clone();
349 let hold_group = task_group.make_subgroup();
350 let subgroup = task_group.clone();
354 task_group.spawn("LND Invoice Subscription", move |handle| async move {
355 let future_stream = client.lightning().subscribe_invoices(InvoiceSubscription {
356 add_index,
357 settle_index: u64::MAX, });
359 let mut invoice_stream = tokio::select! {
360 stream = future_stream => {
361 match stream {
362 Ok(stream) => stream.into_inner(),
363 Err(err) => {
364 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to subscribe to all invoice updates");
365 subgroup.shutdown();
366 return;
367 }
368 }
369 },
370 () = handle.make_shutdown_rx() => {
371 info!(target: LOG_LIGHTNING, "LND Invoice Subscription received shutdown signal");
372 return;
373 }
374 };
375
376 info!(target: LOG_LIGHTNING, "LND Invoice Subscription: starting to process invoice updates");
377 while let Some(invoice) = tokio::select! {
378 () = handle.make_shutdown_rx() => {
379 info!(target: LOG_LIGHTNING, "LND Invoice Subscription task received shutdown signal");
380 None
381 }
382 invoice_update = invoice_stream.message() => {
383 match invoice_update {
384 Ok(invoice) => invoice,
385 Err(err) => {
386 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over invoice update stream");
387 None
388 }
389 }
390 }
391 } {
392 let payment_hash = invoice.r_hash.clone();
397
398 debug!(
399 target: LOG_LIGHTNING,
400 payment_hash = %PrettyPaymentHash(&payment_hash),
401 state = %invoice.state,
402 "LND HOLD Invoice Update",
403 );
404
405 if invoice.r_preimage.is_empty() && invoice.state() == InvoiceState::Open {
406 info!(
407 target: LOG_LIGHTNING,
408 payment_hash = %PrettyPaymentHash(&payment_hash),
409 "Monitoring new LNv2 invoice",
410 );
411 if let Err(err) = self_copy
412 .spawn_lnv2_hold_invoice_subscription(
413 &hold_group,
414 subgroup.clone(),
415 gateway_sender.clone(),
416 payment_hash.clone(),
417 )
418 .await
419 {
420 warn!(
426 target: LOG_LIGHTNING,
427 err = %err.fmt_compact(),
428 payment_hash = %PrettyPaymentHash(&payment_hash),
429 "Failed to spawn HOLD invoice subscription task, shutting down payment-stream subgroup to trigger gateway reconnect",
430 );
431 subgroup.shutdown();
432 }
433 }
434 }
435
436 if !handle.is_shutting_down() {
437 warn!(target: LOG_LIGHTNING, "LND Invoice Subscription exited unexpectedly, shutting down payment-stream subgroup to trigger gateway reconnect");
438 subgroup.shutdown();
439 }
440 });
441
442 Ok(())
443 }
444
445 async fn spawn_lnv1_htlc_interceptor(
449 &self,
450 task_group: &TaskGroup,
451 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
452 lnd_rx: mpsc::Receiver<ForwardHtlcInterceptResponse>,
453 gateway_sender: HtlcSubscriptionSender,
454 ) -> Result<(), LightningRpcError> {
455 let mut client = self.connect().await?;
456
457 client
460 .lightning()
461 .get_info(GetInfoRequest {})
462 .await
463 .map_err(|status| LightningRpcError::FailedToGetNodeInfo {
464 failure_reason: format!("Failed to get node info {status:?}"),
465 })?;
466
467 let subgroup = task_group.clone();
473 task_group.spawn("LND HTLC Subscription", |handle| async move {
474 let future_stream = client
475 .router()
476 .htlc_interceptor(ReceiverStream::new(lnd_rx));
477 let mut htlc_stream = tokio::select! {
478 stream = future_stream => {
479 match stream {
480 Ok(stream) => stream.into_inner(),
481 Err(e) => {
482 crit!(target: LOG_LIGHTNING, err = %e.fmt_compact(), "Failed to establish htlc stream");
483 subgroup.shutdown();
484 return;
485 }
486 }
487 },
488 () = handle.make_shutdown_rx() => {
489 info!(target: LOG_LIGHTNING, "LND HTLC Subscription received shutdown signal while trying to intercept HTLC stream, exiting...");
490 return;
491 }
492 };
493
494 debug!(target: LOG_LIGHTNING, "LND HTLC Subscription: starting to process stream");
495 while let Some(htlc) = tokio::select! {
504 () = handle.make_shutdown_rx() => {
505 info!(target: LOG_LIGHTNING, "LND HTLC Subscription task received shutdown signal");
506 None
507 }
508 htlc_message = htlc_stream.message() => {
509 match htlc_message {
510 Ok(htlc) => htlc,
511 Err(err) => {
512 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over HTLC stream");
513 None
514 }
515 }}
516 } {
517 trace!(target: LOG_LIGHTNING, ?htlc, "LND Handling HTLC");
518
519 let Some(incoming_circuit_key) = htlc.incoming_circuit_key else {
520 warn!(
525 target: LOG_LIGHTNING,
526 payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
527 scid = htlc.outgoing_requested_chan_id,
528 amount_msat = htlc.outgoing_amount_msat,
529 "Cannot route HTLC: incoming_circuit_key is None"
530 );
531 continue;
532 };
533
534 let chan_id = incoming_circuit_key.chan_id;
535 let htlc_id = incoming_circuit_key.htlc_id;
536
537 let intercept = InterceptPaymentRequest {
539 payment_hash: Hash::from_slice(&htlc.payment_hash).expect("Failed to convert payment Hash"),
540 amount_msat: htlc.outgoing_amount_msat,
541 expiry: htlc.incoming_expiry,
542 short_channel_id: Some(htlc.outgoing_requested_chan_id),
543 incoming_chan_id: chan_id,
544 htlc_id,
545 };
546
547 match gateway_sender.send(intercept).await {
548 Ok(()) => {}
549 Err(err) => {
550 warn!(
551 target: LOG_LIGHTNING,
552 err = %err.fmt_compact(),
553 payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
554 scid = htlc.outgoing_requested_chan_id,
555 amount_msat = htlc.outgoing_amount_msat,
556 "Failed to send HTLC to gatewayd for processing"
557 );
558 let _ = Self::cancel_htlc(incoming_circuit_key, lnd_sender.clone())
559 .await
560 .map_err(|err| {
561 warn!(
562 target: LOG_LIGHTNING,
563 err = %err.fmt_compact(),
564 payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
565 chan_id,
566 htlc_id,
567 "Failed to cancel HTLC"
568 );
569 });
570 }
571 }
572 }
573
574 if !handle.is_shutting_down() {
577 warn!(target: LOG_LIGHTNING, "LND HTLC Subscription exited unexpectedly, shutting down payment-stream subgroup to trigger gateway reconnect");
578 subgroup.shutdown();
579 }
580 });
581
582 Ok(())
583 }
584
585 async fn spawn_interceptor(
587 &self,
588 task_group: &TaskGroup,
589 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
590 lnd_rx: mpsc::Receiver<ForwardHtlcInterceptResponse>,
591 gateway_sender: HtlcSubscriptionSender,
592 ) -> Result<(), LightningRpcError> {
593 self.spawn_lnv1_htlc_interceptor(task_group, lnd_sender, lnd_rx, gateway_sender.clone())
594 .await?;
595
596 self.spawn_lnv2_invoice_subscription(task_group, gateway_sender)
597 .await?;
598
599 Ok(())
600 }
601
602 async fn cancel_htlc(
603 key: CircuitKey,
604 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
605 ) -> Result<(), LightningRpcError> {
606 let response = ForwardHtlcInterceptResponse {
608 incoming_circuit_key: Some(key),
609 action: ResolveHoldForwardAction::Fail.into(),
610 preimage: vec![],
611 failure_message: vec![],
612 failure_code: FailureCode::TemporaryChannelFailure.into(),
613 ..Default::default()
614 };
615 Self::send_lnd_response(lnd_sender, response).await
616 }
617
618 async fn send_lnd_response(
619 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
620 response: ForwardHtlcInterceptResponse,
621 ) -> Result<(), LightningRpcError> {
622 lnd_sender.send(response).await.map_err(|send_error| {
624 LightningRpcError::FailedToCompleteHtlc {
625 failure_reason: format!(
626 "Failed to send ForwardHtlcInterceptResponse to LND {send_error:?}"
627 ),
628 }
629 })
630 }
631
632 async fn lookup_payment(
633 &self,
634 payment_hash: Vec<u8>,
635 client: &mut LndClient,
636 ) -> Result<Option<String>, LightningRpcError> {
637 loop {
640 let payments = client
641 .router()
642 .track_payment_v2(TrackPaymentRequest {
643 payment_hash: payment_hash.clone(),
644 no_inflight_updates: true,
645 })
646 .await;
647
648 match payments {
649 Ok(payments) => {
650 if let Some(payment) =
652 payments.into_inner().message().await.map_err(|status| {
653 LightningRpcError::FailedPayment {
654 failure_reason: status.message().to_string(),
655 }
656 })?
657 {
658 if payment.status() == PaymentStatus::Succeeded {
659 return Ok(Some(payment.payment_preimage));
660 }
661
662 let failure_reason = payment.failure_reason();
663 return Err(LightningRpcError::FailedPayment {
664 failure_reason: format!("{failure_reason:?}"),
665 });
666 }
667 }
668 Err(err) => {
669 if err.code() == Code::NotFound {
672 return Ok(None);
673 }
674
675 warn!(
676 target: LOG_LIGHTNING,
677 payment_hash = %PrettyPaymentHash(&payment_hash),
678 err = %err.fmt_compact(),
679 "Could not get the status of payment. Trying again in 5 seconds"
680 );
681 sleep(Duration::from_secs(5)).await;
682 }
683 }
684 }
685 }
686
687 async fn lookup_invoice(
693 client: &mut LndClient,
694 payment_hash: &[u8],
695 ) -> Result<Option<Invoice>, LightningRpcError> {
696 match client
697 .invoices()
698 .lookup_invoice_v2(LookupInvoiceMsg {
699 invoice_ref: Some(InvoiceRef::PaymentHash(payment_hash.to_vec())),
700 lookup_modifier: 0,
701 })
702 .await
703 {
704 Ok(invoice) => Ok(Some(invoice.into_inner())),
705 Err(err) if err.code() == Code::NotFound => Ok(None),
706 Err(err) => Err(LightningRpcError::FailedToCompleteHtlc {
707 failure_reason: format!("Failed to look up invoice: {}", err.fmt_compact()),
708 }),
709 }
710 }
711
712 async fn settle_hold_invoice(
718 &self,
719 payment_hash: Vec<u8>,
720 preimage: Preimage,
721 ) -> Result<(), LightningRpcError> {
722 let mut client = self.connect().await?;
723 let invoice = Self::lookup_invoice(&mut client, &payment_hash).await?;
724 match hold_invoice_action(
725 PaymentActionKind::Settle,
726 invoice.as_ref().map(Invoice::state),
727 ) {
728 Ok(HoldInvoiceAction::Complete) => {}
729 Ok(HoldInvoiceAction::AlreadyComplete) => {
730 info!(
731 target: LOG_LIGHTNING,
732 payment_hash = %PrettyPaymentHash(&payment_hash),
733 "HOLD invoice was already settled",
734 );
735 return Ok(());
736 }
737 Err(error) => {
738 warn!(
739 target: LOG_LIGHTNING,
740 state = ?invoice.as_ref().map(Invoice::state),
741 payment_hash = %PrettyPaymentHash(&payment_hash),
742 failure_reason = error.failure_reason,
743 "Cannot settle HOLD invoice",
744 );
745 return Err(if error.permanent {
746 LightningRpcError::HtlcCompletionRejected {
747 failure_reason: error.failure_reason.to_owned(),
748 }
749 } else {
750 LightningRpcError::FailedToCompleteHtlc {
751 failure_reason: error.failure_reason.to_owned(),
752 }
753 });
754 }
755 }
756
757 client
758 .invoices()
759 .settle_invoice(SettleInvoiceMsg {
760 preimage: preimage.0.to_vec(),
761 })
762 .await
763 .map_err(|err| {
764 warn!(
765 target: LOG_LIGHTNING,
766 err = %err.fmt_compact(),
767 payment_hash = %PrettyPaymentHash(&payment_hash),
768 "Failed to settle HOLD invoice",
769 );
770 LightningRpcError::FailedToCompleteHtlc {
771 failure_reason: "Failed to settle HOLD invoice".to_string(),
772 }
773 })?;
774
775 info!(
776 target: LOG_LIGHTNING,
777 payment_hash = %PrettyPaymentHash(&payment_hash),
778 "Successfully settled HOLD invoice",
779 );
780
781 Ok(())
782 }
783
784 async fn cancel_hold_invoice(&self, payment_hash: Vec<u8>) -> Result<(), LightningRpcError> {
791 let mut client = self.connect().await?;
792 let invoice = Self::lookup_invoice(&mut client, &payment_hash).await?;
793 match hold_invoice_action(
794 PaymentActionKind::Cancel,
795 invoice.as_ref().map(Invoice::state),
796 ) {
797 Ok(HoldInvoiceAction::Complete) => {}
798 Ok(HoldInvoiceAction::AlreadyComplete) => {
799 info!(
800 target: LOG_LIGHTNING,
801 payment_hash = %PrettyPaymentHash(&payment_hash),
802 "HOLD invoice was already canceled",
803 );
804 return Ok(());
805 }
806 Err(error) => {
807 warn!(
808 target: LOG_LIGHTNING,
809 state = ?invoice.as_ref().map(Invoice::state),
810 payment_hash = %PrettyPaymentHash(&payment_hash),
811 failure_reason = error.failure_reason,
812 "Cannot cancel HOLD invoice",
813 );
814 return Err(LightningRpcError::HtlcCompletionRejected {
815 failure_reason: error.failure_reason.to_owned(),
816 });
817 }
818 }
819
820 client
821 .invoices()
822 .cancel_invoice(CancelInvoiceMsg {
823 payment_hash: payment_hash.clone(),
824 })
825 .await
826 .map_err(|err| {
827 warn!(
828 target: LOG_LIGHTNING,
829 err = %err.fmt_compact(),
830 payment_hash = %PrettyPaymentHash(&payment_hash),
831 "Failed to cancel HOLD invoice",
832 );
833 LightningRpcError::FailedToCompleteHtlc {
834 failure_reason: "Failed to cancel HOLD invoice".to_string(),
835 }
836 })?;
837
838 info!(
839 target: LOG_LIGHTNING,
840 payment_hash = %PrettyPaymentHash(&payment_hash),
841 "Successfully canceled HOLD invoice",
842 );
843
844 Ok(())
845 }
846}
847
848impl fmt::Debug for GatewayLndClient {
849 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
850 write!(f, "LndClient")
851 }
852}
853
854#[async_trait]
855impl ILnRpcClient for GatewayLndClient {
856 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
857 let mut client = self.connect().await?;
858 let info = client
859 .lightning()
860 .get_info(GetInfoRequest {})
861 .await
862 .map_err(|status| LightningRpcError::FailedToGetNodeInfo {
863 failure_reason: format!("Failed to get node info {status:?}"),
864 })?
865 .into_inner();
866
867 let pub_key: PublicKey =
868 info.identity_pubkey
869 .parse()
870 .map_err(|e| LightningRpcError::FailedToGetNodeInfo {
871 failure_reason: format!("Failed to parse public key {e:?}"),
872 })?;
873
874 let network = match info
875 .chains
876 .first()
877 .ok_or_else(|| LightningRpcError::FailedToGetNodeInfo {
878 failure_reason: "Failed to parse node network".to_string(),
879 })?
880 .network
881 .as_str()
882 {
883 "mainnet" => "bitcoin",
886 other => other,
887 }
888 .to_string();
889
890 return Ok(GetNodeInfoResponse {
891 pub_key,
892 alias: info.alias,
893 network,
894 block_height: info.block_height,
895 synced_to_chain: info.synced_to_chain,
896 });
897 }
898
899 async fn routehints(
900 &self,
901 num_route_hints: usize,
902 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
903 let mut client = self.connect().await?;
904 let mut channels = client
905 .lightning()
906 .list_channels(ListChannelsRequest {
907 active_only: true,
908 inactive_only: false,
909 public_only: false,
910 private_only: false,
911 peer: vec![],
912 peer_alias_lookup: false,
913 })
914 .await
915 .map_err(|status| LightningRpcError::FailedToGetRouteHints {
916 failure_reason: format!("Failed to list channels {status:?}"),
917 })?
918 .into_inner()
919 .channels;
920
921 channels.sort_by_key(|b| std::cmp::Reverse(b.remote_balance));
923 channels.truncate(num_route_hints);
924
925 let mut route_hints: Vec<RouteHint> = vec![];
926 for chan in &channels {
927 let info = client
928 .lightning()
929 .get_chan_info(ChanInfoRequest {
930 chan_id: chan.chan_id,
931 ..Default::default()
932 })
933 .await
934 .map_err(|status| LightningRpcError::FailedToGetRouteHints {
935 failure_reason: format!("Failed to get channel info {status:?}"),
936 })?
937 .into_inner();
938
939 let Some(policy) = info.node1_policy else {
940 continue;
941 };
942 let src_node_id =
943 PublicKey::from_str(&chan.remote_pubkey).expect("Failed to parse pubkey");
944 let short_channel_id = chan.chan_id;
945 let base_msat = policy.fee_base_msat as u32;
946 let proportional_millionths = policy.fee_rate_milli_msat as u32;
947 let cltv_expiry_delta = policy.time_lock_delta;
948 let htlc_maximum_msat = Some(policy.max_htlc_msat);
949 let htlc_minimum_msat = Some(policy.min_htlc as u64);
950
951 let route_hint_hop = RouteHintHop {
952 src_node_id,
953 short_channel_id,
954 base_msat,
955 proportional_millionths,
956 cltv_expiry_delta: cltv_expiry_delta as u16,
957 htlc_minimum_msat,
958 htlc_maximum_msat,
959 };
960 route_hints.push(RouteHint(vec![route_hint_hop]));
961 }
962
963 Ok(GetRouteHintsResponse { route_hints })
964 }
965
966 async fn pay_private(
967 &self,
968 invoice: PrunedInvoice,
969 max_delay: u64,
970 max_fee: Amount,
971 ) -> Result<PayInvoiceResponse, LightningRpcError> {
972 let payment_hash = invoice.payment_hash.to_byte_array().to_vec();
973 info!(
974 target: LOG_LIGHTNING,
975 payment_hash = %PrettyPaymentHash(&payment_hash),
976 "LND Paying invoice",
977 );
978 let mut client = self.connect().await?;
979
980 debug!(
981 target: LOG_LIGHTNING,
982 payment_hash = %PrettyPaymentHash(&payment_hash),
983 "pay_private checking if payment for invoice exists"
984 );
985
986 let preimage: Vec<u8> = match self
988 .lookup_payment(invoice.payment_hash.to_byte_array().to_vec(), &mut client)
989 .await?
990 {
991 Some(preimage) => {
992 info!(
993 target: LOG_LIGHTNING,
994 payment_hash = %PrettyPaymentHash(&payment_hash),
995 "LND payment already exists for invoice",
996 );
997 hex::FromHex::from_hex(preimage.as_str()).map_err(|error| {
998 LightningRpcError::FailedPayment {
999 failure_reason: format!("Failed to convert preimage {error:?}"),
1000 }
1001 })?
1002 }
1003 _ => {
1004 let fee_limit_msat: i64 =
1008 max_fee
1009 .msats
1010 .try_into()
1011 .map_err(|error| LightningRpcError::FailedPayment {
1012 failure_reason: format!(
1013 "max_fee_msat exceeds valid LND fee limit ranges {error:?}"
1014 ),
1015 })?;
1016
1017 let amt_msat = invoice.amount.msats.try_into().map_err(|error| {
1018 LightningRpcError::FailedPayment {
1019 failure_reason: format!("amount exceeds valid LND amount ranges {error:?}"),
1020 }
1021 })?;
1022 let final_cltv_delta =
1023 invoice.min_final_cltv_delta.try_into().map_err(|error| {
1024 LightningRpcError::FailedPayment {
1025 failure_reason: format!(
1026 "final cltv delta exceeds valid LND range {error:?}"
1027 ),
1028 }
1029 })?;
1030 let cltv_limit =
1031 max_delay
1032 .try_into()
1033 .map_err(|error| LightningRpcError::FailedPayment {
1034 failure_reason: format!("max delay exceeds valid LND range {error:?}"),
1035 })?;
1036
1037 let dest_features = wire_features_to_lnd_feature_vec(&invoice.destination_features)
1038 .map_err(|e| LightningRpcError::FailedPayment {
1039 failure_reason: e.to_string(),
1040 })?;
1041
1042 debug!(
1043 target: LOG_LIGHTNING,
1044 payment_hash = %PrettyPaymentHash(&payment_hash),
1045 "LND payment does not exist, will attempt to pay",
1046 );
1047 let payments = client
1048 .router()
1049 .send_payment_v2(SendPaymentRequest {
1050 amt_msat,
1051 dest: invoice.destination.serialize().to_vec(),
1052 dest_features,
1053 payment_hash: invoice.payment_hash.to_byte_array().to_vec(),
1054 payment_addr: invoice.payment_secret.to_vec(),
1055 route_hints: route_hints_to_lnd(&invoice.route_hints),
1056 final_cltv_delta,
1057 cltv_limit,
1058 no_inflight_updates: false,
1059 timeout_seconds: LND_PAYMENT_TIMEOUT_SECONDS,
1060 fee_limit_msat,
1061 ..Default::default()
1062 })
1063 .await
1064 .map_err(|status| {
1065 warn!(
1066 target: LOG_LIGHTNING,
1067 status = %status,
1068 payment_hash = %PrettyPaymentHash(&payment_hash),
1069 "LND payment request failed",
1070 );
1071 LightningRpcError::FailedPayment {
1072 failure_reason: format!("Failed to make outgoing payment {status:?}"),
1073 }
1074 })?;
1075
1076 debug!(
1077 target: LOG_LIGHTNING,
1078 payment_hash = %PrettyPaymentHash(&payment_hash),
1079 "LND payment request sent, waiting for payment status...",
1080 );
1081 let mut messages = payments.into_inner();
1082 loop {
1083 match messages.message().await.map_err(|error| {
1084 LightningRpcError::FailedPayment {
1085 failure_reason: format!("Failed to get payment status {error:?}"),
1086 }
1087 }) {
1088 Ok(Some(payment)) if payment.status() == PaymentStatus::Succeeded => {
1089 info!(
1090 target: LOG_LIGHTNING,
1091 payment_hash = %PrettyPaymentHash(&payment_hash),
1092 "LND payment succeeded for invoice",
1093 );
1094 break hex::FromHex::from_hex(payment.payment_preimage.as_str())
1095 .map_err(|error| LightningRpcError::FailedPayment {
1096 failure_reason: format!("Failed to convert preimage {error:?}"),
1097 })?;
1098 }
1099 Ok(Some(payment)) if payment.status() == PaymentStatus::InFlight => {
1100 debug!(
1101 target: LOG_LIGHTNING,
1102 payment_hash = %PrettyPaymentHash(&payment_hash),
1103 "LND payment is inflight",
1104 );
1105 continue;
1106 }
1107 Ok(Some(payment)) => {
1108 warn!(
1109 target: LOG_LIGHTNING,
1110 payment_hash = %PrettyPaymentHash(&payment_hash),
1111 status = %payment.status,
1112 "LND payment failed",
1113 );
1114 let failure_reason = payment.failure_reason();
1115 return Err(LightningRpcError::FailedPayment {
1116 failure_reason: format!("{failure_reason:?}"),
1117 });
1118 }
1119 Ok(None) => {
1120 warn!(
1121 target: LOG_LIGHTNING,
1122 payment_hash = %PrettyPaymentHash(&payment_hash),
1123 "LND payment failed with no payment status",
1124 );
1125 return Err(LightningRpcError::FailedPayment {
1126 failure_reason: format!(
1127 "Failed to get payment status for payment hash {:?}",
1128 invoice.payment_hash
1129 ),
1130 });
1131 }
1132 Err(err) => {
1133 warn!(
1134 target: LOG_LIGHTNING,
1135 payment_hash = %PrettyPaymentHash(&payment_hash),
1136 err = %err.fmt_compact(),
1137 "LND payment failed",
1138 );
1139 return Err(err);
1140 }
1141 }
1142 }
1143 }
1144 };
1145 Ok(PayInvoiceResponse {
1146 preimage: Preimage(preimage.try_into().expect("Failed to create preimage")),
1147 })
1148 }
1149
1150 fn supports_private_payments(&self) -> bool {
1153 true
1154 }
1155
1156 async fn route_htlcs<'a>(
1157 self: Box<Self>,
1158 task_group: &TaskGroup,
1159 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
1160 const CHANNEL_SIZE: usize = 100;
1161
1162 let (gateway_sender, gateway_receiver) =
1164 mpsc::channel::<InterceptPaymentRequest>(CHANNEL_SIZE);
1165
1166 let (lnd_sender, lnd_rx) = mpsc::channel::<ForwardHtlcInterceptResponse>(CHANNEL_SIZE);
1167
1168 self.spawn_interceptor(
1169 task_group,
1170 lnd_sender.clone(),
1171 lnd_rx,
1172 gateway_sender.clone(),
1173 )
1174 .await?;
1175 let new_client = Arc::new(Self {
1176 address: self.address.clone(),
1177 tls_cert: self.tls_cert.clone(),
1178 macaroon: self.macaroon.clone(),
1179 lnd_sender: Some(lnd_sender.clone()),
1180 lnv2_filter: self.lnv2_filter.clone(),
1181 });
1182 Ok((Box::pin(ReceiverStream::new(gateway_receiver)), new_client))
1183 }
1184
1185 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
1186 let incoming_circuit = htlc.incoming_circuit();
1187 let InterceptPaymentResponse {
1188 action,
1189 payment_hash,
1190 incoming_chan_id: _,
1191 htlc_id: _,
1192 } = htlc;
1193
1194 let (action, preimage) = match action {
1195 PaymentAction::Settle(preimage) => (ResolveHoldForwardAction::Settle, preimage),
1196 PaymentAction::Cancel => (ResolveHoldForwardAction::Fail, Preimage([0; 32])),
1197 PaymentAction::Forward => (ResolveHoldForwardAction::Resume, Preimage([0; 32])),
1198 };
1199
1200 let Some((chan_id, htlc_id)) = incoming_circuit else {
1207 return match action {
1210 ResolveHoldForwardAction::Settle => {
1211 self.settle_hold_invoice(payment_hash.to_byte_array().to_vec(), preimage)
1212 .await
1213 }
1214 _ => {
1218 self.cancel_hold_invoice(payment_hash.to_byte_array().to_vec())
1219 .await
1220 }
1221 };
1222 };
1223
1224 let Some(lnd_sender) = self.lnd_sender.clone() else {
1226 crit!("Gatewayd has not started to route HTLCs");
1227 return Err(LightningRpcError::FailedToCompleteHtlc {
1228 failure_reason: "Gatewayd has not started to route HTLCs".to_string(),
1229 });
1230 };
1231
1232 let response = ForwardHtlcInterceptResponse {
1233 incoming_circuit_key: Some(CircuitKey { chan_id, htlc_id }),
1234 action: action.into(),
1235 preimage: preimage.0.to_vec(),
1236 failure_message: vec![],
1237 failure_code: FailureCode::TemporaryChannelFailure.into(),
1238 ..Default::default()
1239 };
1240
1241 Self::send_lnd_response(lnd_sender, response).await
1242 }
1243
1244 async fn create_invoice(
1245 &self,
1246 create_invoice_request: CreateInvoiceRequest,
1247 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
1248 let mut client = self.connect().await?;
1249 let description = create_invoice_request
1250 .description
1251 .unwrap_or(InvoiceDescription::Direct(String::new()));
1252
1253 if let Some(payment_hash_value) = create_invoice_request.payment_hash {
1254 let payment_hash = payment_hash_value.to_byte_array().to_vec();
1255 let hold_invoice_request = match description {
1256 InvoiceDescription::Direct(description) => AddHoldInvoiceRequest {
1257 memo: description,
1258 hash: payment_hash.clone(),
1259 value_msat: create_invoice_request.amount_msat as i64,
1260 expiry: i64::from(create_invoice_request.expiry_secs),
1261 ..Default::default()
1262 },
1263 InvoiceDescription::Hash(desc_hash) => AddHoldInvoiceRequest {
1264 description_hash: desc_hash.to_byte_array().to_vec(),
1265 hash: payment_hash.clone(),
1266 value_msat: create_invoice_request.amount_msat as i64,
1267 expiry: i64::from(create_invoice_request.expiry_secs),
1268 ..Default::default()
1269 },
1270 };
1271
1272 let hold_invoice_response = client
1273 .invoices()
1274 .add_hold_invoice(hold_invoice_request)
1275 .await
1276 .map_err(|e| LightningRpcError::FailedToGetInvoice {
1277 failure_reason: e.to_string(),
1278 })?;
1279
1280 let invoice = hold_invoice_response.into_inner().payment_request;
1281 Ok(CreateInvoiceResponse { invoice })
1282 } else {
1283 let invoice = match description {
1284 InvoiceDescription::Direct(description) => Invoice {
1285 memo: description,
1286 value_msat: create_invoice_request.amount_msat as i64,
1287 expiry: i64::from(create_invoice_request.expiry_secs),
1288 ..Default::default()
1289 },
1290 InvoiceDescription::Hash(desc_hash) => Invoice {
1291 description_hash: desc_hash.to_byte_array().to_vec(),
1292 value_msat: create_invoice_request.amount_msat as i64,
1293 expiry: i64::from(create_invoice_request.expiry_secs),
1294 ..Default::default()
1295 },
1296 };
1297
1298 let add_invoice_response =
1299 client.lightning().add_invoice(invoice).await.map_err(|e| {
1300 LightningRpcError::FailedToGetInvoice {
1301 failure_reason: e.to_string(),
1302 }
1303 })?;
1304
1305 let invoice = add_invoice_response.into_inner().payment_request;
1306 Ok(CreateInvoiceResponse { invoice })
1307 }
1308 }
1309
1310 async fn get_ln_onchain_address(
1311 &self,
1312 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
1313 let mut client = self.connect().await?;
1314
1315 match client
1316 .wallet()
1317 .next_addr(AddrRequest {
1318 account: String::new(), r#type: 4, change: false,
1321 })
1322 .await
1323 {
1324 Ok(response) => Ok(GetLnOnchainAddressResponse {
1325 address: response.into_inner().addr,
1326 }),
1327 Err(e) => Err(LightningRpcError::FailedToGetLnOnchainAddress {
1328 failure_reason: format!("Failed to get funding address {e:?}"),
1329 }),
1330 }
1331 }
1332
1333 async fn send_onchain(
1334 &self,
1335 SendOnchainRequest {
1336 address,
1337 amount,
1338 fee_rate_sats_per_vbyte,
1339 }: SendOnchainRequest,
1340 ) -> Result<SendOnchainResponse, LightningRpcError> {
1341 #[allow(deprecated)]
1342 let request = match amount {
1343 BitcoinAmountOrAll::All => SendCoinsRequest {
1344 addr: address.assume_checked().to_string(),
1345 amount: 0,
1346 target_conf: 0,
1347 sat_per_vbyte: fee_rate_sats_per_vbyte,
1348 sat_per_byte: 0,
1349 send_all: true,
1350 label: String::new(),
1351 min_confs: 0,
1352 spend_unconfirmed: true,
1353 ..Default::default()
1354 },
1355 BitcoinAmountOrAll::Amount(amount) => SendCoinsRequest {
1356 addr: address.assume_checked().to_string(),
1357 amount: amount.to_sat() as i64,
1358 target_conf: 0,
1359 sat_per_vbyte: fee_rate_sats_per_vbyte,
1360 sat_per_byte: 0,
1361 send_all: false,
1362 label: String::new(),
1363 min_confs: 0,
1364 spend_unconfirmed: true,
1365 ..Default::default()
1366 },
1367 };
1368
1369 match self.connect().await?.lightning().send_coins(request).await {
1370 Ok(res) => Ok(SendOnchainResponse {
1371 txid: res.into_inner().txid,
1372 }),
1373 Err(e) => Err(LightningRpcError::FailedToWithdrawOnchain {
1374 failure_reason: format!("Failed to withdraw funds on-chain {e:?}"),
1375 }),
1376 }
1377 }
1378
1379 async fn open_channel(
1380 &self,
1381 crate::OpenChannelRequest {
1382 pubkey,
1383 host,
1384 channel_size_sats,
1385 push_amount_sats,
1386 }: crate::OpenChannelRequest,
1387 ) -> Result<OpenChannelResponse, LightningRpcError> {
1388 let mut client = self.connect().await?;
1389
1390 let peers = client
1391 .lightning()
1392 .list_peers(ListPeersRequest { latest_error: true })
1393 .await
1394 .map_err(|e| LightningRpcError::FailedToConnectToPeer {
1395 failure_reason: format!("Could not list peers: {e:?}"),
1396 })?
1397 .into_inner();
1398
1399 if !peers.peers.into_iter().any(|peer| {
1401 PublicKey::from_str(&peer.pub_key).expect("could not parse public key") == pubkey
1402 }) {
1403 client
1404 .lightning()
1405 .connect_peer(ConnectPeerRequest {
1406 addr: Some(LightningAddress {
1407 pubkey: pubkey.to_string(),
1408 host,
1409 }),
1410 perm: false,
1411 timeout: 10,
1412 })
1413 .await
1414 .map_err(|e| LightningRpcError::FailedToConnectToPeer {
1415 failure_reason: format!("Failed to connect to peer {e:?}"),
1416 })?;
1417 }
1418
1419 match client
1421 .lightning()
1422 .open_channel_sync(OpenChannelRequest {
1423 node_pubkey: pubkey.serialize().to_vec(),
1424 local_funding_amount: channel_size_sats.try_into().expect("u64 -> i64"),
1425 push_sat: push_amount_sats.try_into().expect("u64 -> i64"),
1426 ..Default::default()
1427 })
1428 .await
1429 {
1430 Ok(res) => Ok(OpenChannelResponse {
1431 funding_txid: match res.into_inner().funding_txid {
1432 Some(txid) => match txid {
1433 FundingTxid::FundingTxidBytes(mut bytes) => {
1434 bytes.reverse();
1435 hex::encode(bytes)
1436 }
1437 FundingTxid::FundingTxidStr(str) => str,
1438 },
1439 None => String::new(),
1440 },
1441 }),
1442 Err(e) => Err(LightningRpcError::FailedToOpenChannel {
1443 failure_reason: format!("Failed to open channel {e:?}"),
1444 }),
1445 }
1446 }
1447
1448 async fn close_channels_with_peer(
1449 &self,
1450 CloseChannelsWithPeerRequest {
1451 pubkey,
1452 force,
1453 sats_per_vbyte,
1454 }: CloseChannelsWithPeerRequest,
1455 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
1456 let mut client = self.connect().await?;
1457
1458 let channels_with_peer = client
1459 .lightning()
1460 .list_channels(ListChannelsRequest {
1461 active_only: false,
1462 inactive_only: false,
1463 public_only: false,
1464 private_only: false,
1465 peer: pubkey.serialize().to_vec(),
1466 peer_alias_lookup: false,
1467 })
1468 .await
1469 .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1470 failure_reason: format!("Failed to list channels {e:?}"),
1471 })?
1472 .into_inner()
1473 .channels;
1474
1475 for channel in &channels_with_peer {
1476 let channel_point =
1477 bitcoin::OutPoint::from_str(&channel.channel_point).map_err(|e| {
1478 LightningRpcError::FailedToCloseChannelsWithPeer {
1479 failure_reason: format!("Failed to parse channel point {e:?}"),
1480 }
1481 })?;
1482
1483 if force {
1484 client
1485 .lightning()
1486 .close_channel(CloseChannelRequest {
1487 channel_point: Some(ChannelPoint {
1488 funding_txid: Some(
1489 tonic_lnd::lnrpc::channel_point::FundingTxid::FundingTxidBytes(
1490 <bitcoin::Txid as AsRef<[u8]>>::as_ref(&channel_point.txid)
1491 .to_vec(),
1492 ),
1493 ),
1494 output_index: channel_point.vout,
1495 }),
1496 force,
1497 ..Default::default()
1498 })
1499 .await
1500 .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1501 failure_reason: format!("Failed to close channel {e:?}"),
1502 })?;
1503 } else {
1504 client
1505 .lightning()
1506 .close_channel(CloseChannelRequest {
1507 channel_point: Some(ChannelPoint {
1508 funding_txid: Some(
1509 tonic_lnd::lnrpc::channel_point::FundingTxid::FundingTxidBytes(
1510 <bitcoin::Txid as AsRef<[u8]>>::as_ref(&channel_point.txid)
1511 .to_vec(),
1512 ),
1513 ),
1514 output_index: channel_point.vout,
1515 }),
1516 force,
1517 sat_per_vbyte: sats_per_vbyte.unwrap_or_default(),
1518 ..Default::default()
1519 })
1520 .await
1521 .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1522 failure_reason: format!("Failed to close channel {e:?}"),
1523 })?;
1524 }
1525 }
1526
1527 Ok(CloseChannelsWithPeerResponse {
1528 num_channels_closed: channels_with_peer.len() as u32,
1529 })
1530 }
1531
1532 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
1533 let mut client = self.connect().await?;
1534
1535 let peer_addresses: std::collections::HashMap<String, String> = client
1537 .lightning()
1538 .list_peers(ListPeersRequest {
1539 latest_error: false,
1540 })
1541 .await
1542 .map(|resp| {
1543 resp.into_inner()
1544 .peers
1545 .into_iter()
1546 .filter_map(|peer| {
1547 if peer.address.is_empty() {
1548 None
1549 } else {
1550 Some((peer.pub_key, peer.address))
1551 }
1552 })
1553 .collect()
1554 })
1555 .unwrap_or_default();
1556
1557 match client
1558 .lightning()
1559 .list_channels(ListChannelsRequest {
1560 active_only: false,
1561 inactive_only: false,
1562 public_only: false,
1563 private_only: false,
1564 peer: vec![],
1565 peer_alias_lookup: true,
1566 })
1567 .await
1568 {
1569 Ok(response) => Ok(ListChannelsResponse {
1570 channels: response
1571 .into_inner()
1572 .channels
1573 .into_iter()
1574 .map(|channel| {
1575 let channel_size_sats = channel.capacity.try_into().expect("i64 -> u64");
1576
1577 let local_balance_sats: u64 =
1578 channel.local_balance.try_into().expect("i64 -> u64");
1579 let local_channel_reserve_sats: u64 = match channel.local_constraints {
1580 Some(constraints) => constraints.chan_reserve_sat,
1581 None => 0,
1582 };
1583
1584 let outbound_liquidity_sats =
1585 local_balance_sats.saturating_sub(local_channel_reserve_sats);
1586
1587 let remote_balance_sats: u64 =
1588 channel.remote_balance.try_into().expect("i64 -> u64");
1589 let remote_channel_reserve_sats: u64 = match channel.remote_constraints {
1590 Some(constraints) => constraints.chan_reserve_sat,
1591 None => 0,
1592 };
1593
1594 let inbound_liquidity_sats =
1595 remote_balance_sats.saturating_sub(remote_channel_reserve_sats);
1596
1597 let funding_outpoint = OutPoint::from_str(&channel.channel_point).ok();
1598
1599 let remote_address = peer_addresses.get(&channel.remote_pubkey).cloned();
1600
1601 ChannelInfo {
1602 remote_pubkey: PublicKey::from_str(&channel.remote_pubkey)
1603 .expect("Lightning node returned invalid remote channel pubkey"),
1604 channel_size_sats,
1605 outbound_liquidity_sats,
1606 inbound_liquidity_sats,
1607 is_active: channel.active,
1608 funding_outpoint,
1609 remote_node_alias: if channel.peer_alias.is_empty() {
1610 None
1611 } else {
1612 Some(channel.peer_alias.clone())
1613 },
1614 remote_address,
1615 }
1616 })
1617 .collect(),
1618 }),
1619 Err(e) => Err(LightningRpcError::FailedToListChannels {
1620 failure_reason: format!("Failed to list active channels {e:?}"),
1621 }),
1622 }
1623 }
1624
1625 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
1626 let mut client = self.connect().await?;
1627
1628 let wallet_balance_response = client
1629 .lightning()
1630 .wallet_balance(WalletBalanceRequest {
1631 ..Default::default()
1632 })
1633 .await
1634 .map_err(|e| LightningRpcError::FailedToGetBalances {
1635 failure_reason: format!("Failed to get on-chain balance {e:?}"),
1636 })?
1637 .into_inner();
1638
1639 let channel_balance_response = client
1640 .lightning()
1641 .channel_balance(ChannelBalanceRequest {})
1642 .await
1643 .map_err(|e| LightningRpcError::FailedToGetBalances {
1644 failure_reason: format!("Failed to get lightning balance {e:?}"),
1645 })?
1646 .into_inner();
1647 let total_outbound = channel_balance_response.local_balance.unwrap_or_default();
1648 let unsettled_outbound = channel_balance_response
1649 .unsettled_local_balance
1650 .unwrap_or_default();
1651 let pending_outbound = channel_balance_response
1652 .pending_open_local_balance
1653 .unwrap_or_default();
1654 let lightning_balance_msats = total_outbound
1655 .msat
1656 .saturating_sub(unsettled_outbound.msat)
1657 .saturating_sub(pending_outbound.msat);
1658
1659 let total_inbound = channel_balance_response.remote_balance.unwrap_or_default();
1660 let unsettled_inbound = channel_balance_response
1661 .unsettled_remote_balance
1662 .unwrap_or_default();
1663 let pending_inbound = channel_balance_response
1664 .pending_open_remote_balance
1665 .unwrap_or_default();
1666 let inbound_lightning_liquidity_msats = total_inbound
1667 .msat
1668 .saturating_sub(unsettled_inbound.msat)
1669 .saturating_sub(pending_inbound.msat);
1670
1671 Ok(GetBalancesResponse {
1672 onchain_balance_sats: (wallet_balance_response.total_balance
1673 + wallet_balance_response.reserved_balance_anchor_chan)
1674 as u64,
1675 lightning_balance_msats,
1676 inbound_lightning_liquidity_msats,
1677 })
1678 }
1679
1680 async fn get_invoice(
1681 &self,
1682 get_invoice_request: GetInvoiceRequest,
1683 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
1684 let mut client = self.connect().await?;
1685 let invoice = client
1686 .invoices()
1687 .lookup_invoice_v2(LookupInvoiceMsg {
1688 invoice_ref: Some(InvoiceRef::PaymentHash(
1689 get_invoice_request.payment_hash.consensus_encode_to_vec(),
1690 )),
1691 ..Default::default()
1692 })
1693 .await;
1694 let invoice = match invoice {
1695 Ok(invoice) => invoice.into_inner(),
1696 Err(_) => return Ok(None),
1697 };
1698 let preimage: [u8; 32] = invoice
1699 .clone()
1700 .r_preimage
1701 .try_into()
1702 .expect("Could not convert preimage");
1703 let status = match &invoice.state() {
1704 InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
1705 InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
1706 _ => fedimint_gateway_common::PaymentStatus::Pending,
1707 };
1708
1709 Ok(Some(GetInvoiceResponse {
1710 preimage: Some(preimage.consensus_encode_to_hex()),
1711 payment_hash: Some(
1712 sha256::Hash::from_slice(&invoice.r_hash).expect("Could not convert payment hash"),
1713 ),
1714 amount: Amount::from_msats(invoice.value_msat as u64),
1715 created_at: UNIX_EPOCH + Duration::from_secs(invoice.creation_date as u64),
1716 status,
1717 }))
1718 }
1719
1720 async fn list_transactions(
1721 &self,
1722 start_secs: u64,
1723 end_secs: u64,
1724 ) -> Result<ListTransactionsResponse, LightningRpcError> {
1725 let mut client = self.connect().await?;
1726 let payments = client
1727 .lightning()
1728 .list_payments(ListPaymentsRequest {
1729 ..Default::default()
1731 })
1732 .await
1733 .map_err(|err| LightningRpcError::FailedToListTransactions {
1734 failure_reason: err.to_string(),
1735 })?
1736 .into_inner();
1737
1738 let mut payments = payments
1739 .payments
1740 .iter()
1741 .filter_map(|payment| {
1742 let timestamp_secs = (payment.creation_time_ns / 1_000_000_000) as u64;
1743 if timestamp_secs < start_secs || timestamp_secs >= end_secs {
1744 return None;
1745 }
1746 let payment_hash = sha256::Hash::from_str(&payment.payment_hash).ok();
1747 let preimage = (!payment.payment_preimage.is_empty())
1748 .then_some(payment.payment_preimage.clone());
1749 let status = match &payment.status() {
1750 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
1751 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
1752 _ => fedimint_gateway_common::PaymentStatus::Pending,
1753 };
1754 Some(PaymentDetails {
1755 payment_hash,
1756 preimage,
1757 payment_kind: PaymentKind::Bolt11,
1758 amount: Amount::from_msats(payment.value_msat as u64),
1759 direction: PaymentDirection::Outbound,
1760 status,
1761 timestamp_secs,
1762 })
1763 })
1764 .collect::<Vec<_>>();
1765
1766 let invoices = client
1767 .lightning()
1768 .list_invoices(ListInvoiceRequest {
1769 pending_only: false,
1770 ..Default::default()
1772 })
1773 .await
1774 .map_err(|err| LightningRpcError::FailedToListTransactions {
1775 failure_reason: err.to_string(),
1776 })?
1777 .into_inner();
1778
1779 let mut incoming_payments = invoices
1780 .invoices
1781 .iter()
1782 .filter_map(|invoice| {
1783 let timestamp_secs = invoice.settle_date as u64;
1784 if timestamp_secs < start_secs || timestamp_secs >= end_secs {
1785 return None;
1786 }
1787 let status = match &invoice.state() {
1788 InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
1789 InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
1790 _ => return None,
1791 };
1792 let preimage = (!invoice.r_preimage.is_empty())
1793 .then_some(invoice.r_preimage.encode_hex::<String>());
1794 Some(PaymentDetails {
1795 payment_hash: Some(
1796 sha256::Hash::from_slice(&invoice.r_hash)
1797 .expect("Could not convert payment hash"),
1798 ),
1799 preimage,
1800 payment_kind: PaymentKind::Bolt11,
1801 amount: Amount::from_msats(invoice.value_msat as u64),
1802 direction: PaymentDirection::Inbound,
1803 status,
1804 timestamp_secs,
1805 })
1806 })
1807 .collect::<Vec<_>>();
1808
1809 payments.append(&mut incoming_payments);
1810 payments.sort_by_key(|p| p.timestamp_secs);
1811
1812 Ok(ListTransactionsResponse {
1813 transactions: payments,
1814 })
1815 }
1816
1817 fn create_offer(
1818 &self,
1819 _amount_msat: Option<Amount>,
1820 _description: Option<String>,
1821 _expiry_secs: Option<u32>,
1822 _quantity: Option<u64>,
1823 ) -> Result<String, LightningRpcError> {
1824 Err(LightningRpcError::Bolt12Error {
1825 failure_reason: "LND Does not support Bolt12".to_string(),
1826 })
1827 }
1828
1829 async fn pay_offer(
1830 &self,
1831 _offer: String,
1832 _quantity: Option<u64>,
1833 _amount: Option<Amount>,
1834 _payer_note: Option<String>,
1835 ) -> Result<Preimage, LightningRpcError> {
1836 Err(LightningRpcError::Bolt12Error {
1837 failure_reason: "LND Does not support Bolt12".to_string(),
1838 })
1839 }
1840
1841 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
1842 Ok(())
1844 }
1845}
1846
1847fn route_hints_to_lnd(
1848 route_hints: &[fedimint_ln_common::route_hints::RouteHint],
1849) -> Vec<tonic_lnd::lnrpc::RouteHint> {
1850 route_hints
1851 .iter()
1852 .map(|hint| tonic_lnd::lnrpc::RouteHint {
1853 hop_hints: hint
1854 .0
1855 .iter()
1856 .map(|hop| tonic_lnd::lnrpc::HopHint {
1857 node_id: hop.src_node_id.serialize().encode_hex(),
1858 chan_id: hop.short_channel_id,
1859 fee_base_msat: hop.base_msat,
1860 fee_proportional_millionths: hop.proportional_millionths,
1861 cltv_expiry_delta: u32::from(hop.cltv_expiry_delta),
1862 })
1863 .collect(),
1864 })
1865 .collect()
1866}
1867
1868fn wire_features_to_lnd_feature_vec(features_wire_encoded: &[u8]) -> anyhow::Result<Vec<i32>> {
1869 ensure!(
1870 features_wire_encoded.len() <= 1_000,
1871 "Will not process feature bit vectors larger than 1000 byte"
1872 );
1873
1874 let lnd_features = features_wire_encoded
1875 .iter()
1876 .rev()
1877 .enumerate()
1878 .flat_map(|(byte_idx, &feature_byte)| {
1879 (0..8).filter_map(move |bit_idx| {
1880 if (feature_byte & (1u8 << bit_idx)) != 0 {
1881 Some(
1882 i32::try_from(byte_idx * 8 + bit_idx)
1883 .expect("Index will never exceed i32::MAX for feature vectors <8MB"),
1884 )
1885 } else {
1886 None
1887 }
1888 })
1889 })
1890 .collect::<Vec<_>>();
1891
1892 Ok(lnd_features)
1893}
1894
1895struct PrettyPaymentHash<'a>(&'a Vec<u8>);
1897
1898impl Display for PrettyPaymentHash<'_> {
1899 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1900 write!(f, "payment_hash={}", self.0.encode_hex::<String>())
1901 }
1902}
1903
1904#[cfg(test)]
1905mod tests;