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