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