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