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