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