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