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,
331 incoming_amount_msat: hold.amt_paid_msat as u64,
332 expiry: hold.expiry as u32,
335 short_channel_id: Some(0),
336 incoming_chan_id,
341 htlc_id,
342 };
343
344 match gateway_sender.send(intercept).await {
345 Ok(()) => {}
346 Err(err) => {
347 warn!(
348 target: LOG_LIGHTNING,
349 err = %err.fmt_compact(),
350 "Hold Invoice Subscription failed to send Intercept to gateway"
351 );
352 let _ = self_copy.cancel_hold_invoice(hold.r_hash).await;
353 }
354 }
355 }
356 }
357 });
358
359 Ok(())
360 }
361
362 async fn spawn_lnv2_invoice_subscription(
368 &self,
369 task_group: &TaskGroup,
370 gateway_sender: HtlcSubscriptionSender,
371 ) -> Result<(), LightningRpcError> {
372 let mut client = self.connect().await?;
373
374 let list_response = client
375 .lightning()
376 .list_invoices(ListInvoiceRequest {
377 pending_only: true,
378 index_offset: 0,
379 num_max_invoices: u64::MAX,
380 reversed: false,
381 ..Default::default()
382 })
383 .await
384 .map_err(|status| {
385 warn!(target: LOG_LIGHTNING, status = %status, "Failed to list all invoices");
386 LightningRpcError::FailedToRouteHtlcs {
387 failure_reason: "Failed to list all invoices".to_string(),
388 }
389 })?
390 .into_inner();
391
392 let self_copy = self.clone();
393 let hold_group = task_group.make_subgroup();
394 let subgroup = task_group.clone();
398
399 for invoice in &list_response.invoices {
409 if invoice.r_preimage.is_empty() {
410 info!(
411 target: LOG_LIGHTNING,
412 payment_hash = %PrettyPaymentHash(&invoice.r_hash),
413 "Monitoring pre-existing pending LNv2 invoice",
414 );
415 self.spawn_lnv2_hold_invoice_subscription(
416 &hold_group,
417 subgroup.clone(),
418 gateway_sender.clone(),
419 invoice.r_hash.clone(),
420 )
421 .await?;
422 }
423 }
424
425 let add_index = list_response.last_index_offset;
432 task_group.spawn("LND Invoice Subscription", move |handle| async move {
433 let future_stream = client.lightning().subscribe_invoices(InvoiceSubscription {
434 add_index,
435 settle_index: u64::MAX, });
437 let mut invoice_stream = tokio::select! {
438 stream = future_stream => {
439 match stream {
440 Ok(stream) => stream.into_inner(),
441 Err(err) => {
442 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Failed to subscribe to all invoice updates");
443 subgroup.shutdown();
444 return;
445 }
446 }
447 },
448 () = handle.make_shutdown_rx() => {
449 info!(target: LOG_LIGHTNING, "LND Invoice Subscription received shutdown signal");
450 return;
451 }
452 };
453
454 info!(target: LOG_LIGHTNING, "LND Invoice Subscription: starting to process invoice updates");
455 while let Some(invoice) = tokio::select! {
456 () = handle.make_shutdown_rx() => {
457 info!(target: LOG_LIGHTNING, "LND Invoice Subscription task received shutdown signal");
458 None
459 }
460 invoice_update = invoice_stream.message() => {
461 match invoice_update {
462 Ok(invoice) => invoice,
463 Err(err) => {
464 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over invoice update stream");
465 None
466 }
467 }
468 }
469 } {
470 let payment_hash = invoice.r_hash.clone();
475
476 debug!(
477 target: LOG_LIGHTNING,
478 payment_hash = %PrettyPaymentHash(&payment_hash),
479 state = %invoice.state,
480 "LND HOLD Invoice Update",
481 );
482
483 if invoice.r_preimage.is_empty() && invoice.state() == InvoiceState::Open {
484 info!(
485 target: LOG_LIGHTNING,
486 payment_hash = %PrettyPaymentHash(&payment_hash),
487 "Monitoring new LNv2 invoice",
488 );
489 if let Err(err) = self_copy
490 .spawn_lnv2_hold_invoice_subscription(
491 &hold_group,
492 subgroup.clone(),
493 gateway_sender.clone(),
494 payment_hash.clone(),
495 )
496 .await
497 {
498 warn!(
504 target: LOG_LIGHTNING,
505 err = %err.fmt_compact(),
506 payment_hash = %PrettyPaymentHash(&payment_hash),
507 "Failed to spawn HOLD invoice subscription task, shutting down payment-stream subgroup to trigger gateway reconnect",
508 );
509 subgroup.shutdown();
510 }
511 }
512 }
513
514 if !handle.is_shutting_down() {
515 warn!(target: LOG_LIGHTNING, "LND Invoice Subscription exited unexpectedly, shutting down payment-stream subgroup to trigger gateway reconnect");
516 subgroup.shutdown();
517 }
518 });
519
520 Ok(())
521 }
522
523 async fn spawn_lnv1_htlc_interceptor(
527 &self,
528 task_group: &TaskGroup,
529 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
530 lnd_rx: mpsc::Receiver<ForwardHtlcInterceptResponse>,
531 gateway_sender: HtlcSubscriptionSender,
532 ) -> Result<(), LightningRpcError> {
533 let mut client = self.connect().await?;
534
535 client
538 .lightning()
539 .get_info(GetInfoRequest {})
540 .await
541 .map_err(|status| LightningRpcError::FailedToGetNodeInfo {
542 failure_reason: format!("Failed to get node info {status:?}"),
543 })?;
544
545 let subgroup = task_group.clone();
551 task_group.spawn("LND HTLC Subscription", |handle| async move {
552 let future_stream = client
553 .router()
554 .htlc_interceptor(ReceiverStream::new(lnd_rx));
555 let mut htlc_stream = tokio::select! {
556 stream = future_stream => {
557 match stream {
558 Ok(stream) => stream.into_inner(),
559 Err(e) => {
560 crit!(target: LOG_LIGHTNING, err = %e.fmt_compact(), "Failed to establish htlc stream");
561 subgroup.shutdown();
562 return;
563 }
564 }
565 },
566 () = handle.make_shutdown_rx() => {
567 info!(target: LOG_LIGHTNING, "LND HTLC Subscription received shutdown signal while trying to intercept HTLC stream, exiting...");
568 return;
569 }
570 };
571
572 debug!(target: LOG_LIGHTNING, "LND HTLC Subscription: starting to process stream");
573 while let Some(htlc) = tokio::select! {
582 () = handle.make_shutdown_rx() => {
583 info!(target: LOG_LIGHTNING, "LND HTLC Subscription task received shutdown signal");
584 None
585 }
586 htlc_message = htlc_stream.message() => {
587 match htlc_message {
588 Ok(htlc) => htlc,
589 Err(err) => {
590 warn!(target: LOG_LIGHTNING, err = %err.fmt_compact(), "Error received over HTLC stream");
591 None
592 }
593 }}
594 } {
595 trace!(target: LOG_LIGHTNING, ?htlc, "LND Handling HTLC");
596
597 let Some(incoming_circuit_key) = htlc.incoming_circuit_key else {
598 warn!(
603 target: LOG_LIGHTNING,
604 payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
605 scid = htlc.outgoing_requested_chan_id,
606 amount_msat = htlc.outgoing_amount_msat,
607 "Cannot route HTLC: incoming_circuit_key is None"
608 );
609 continue;
610 };
611
612 let chan_id = incoming_circuit_key.chan_id;
613 let htlc_id = incoming_circuit_key.htlc_id;
614
615 let intercept = InterceptPaymentRequest {
617 payment_hash: Hash::from_slice(&htlc.payment_hash).expect("Failed to convert payment Hash"),
618 amount_msat: htlc.outgoing_amount_msat,
623 incoming_amount_msat: htlc.incoming_amount_msat,
624 expiry: htlc.incoming_expiry,
625 short_channel_id: Some(htlc.outgoing_requested_chan_id),
626 incoming_chan_id: chan_id,
627 htlc_id,
628 };
629
630 match gateway_sender.send(intercept).await {
631 Ok(()) => {}
632 Err(err) => {
633 warn!(
634 target: LOG_LIGHTNING,
635 err = %err.fmt_compact(),
636 payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
637 scid = htlc.outgoing_requested_chan_id,
638 amount_msat = htlc.outgoing_amount_msat,
639 "Failed to send HTLC to gatewayd for processing"
640 );
641 let _ = Self::cancel_htlc(incoming_circuit_key, lnd_sender.clone())
642 .await
643 .map_err(|err| {
644 warn!(
645 target: LOG_LIGHTNING,
646 err = %err.fmt_compact(),
647 payment_hash = %PrettyPaymentHash(&htlc.payment_hash),
648 chan_id,
649 htlc_id,
650 "Failed to cancel HTLC"
651 );
652 });
653 }
654 }
655 }
656
657 if !handle.is_shutting_down() {
660 warn!(target: LOG_LIGHTNING, "LND HTLC Subscription exited unexpectedly, shutting down payment-stream subgroup to trigger gateway reconnect");
661 subgroup.shutdown();
662 }
663 });
664
665 Ok(())
666 }
667
668 async fn spawn_interceptor(
670 &self,
671 task_group: &TaskGroup,
672 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
673 lnd_rx: mpsc::Receiver<ForwardHtlcInterceptResponse>,
674 gateway_sender: HtlcSubscriptionSender,
675 ) -> Result<(), LightningRpcError> {
676 self.spawn_lnv1_htlc_interceptor(task_group, lnd_sender, lnd_rx, gateway_sender.clone())
677 .await?;
678
679 self.spawn_lnv2_invoice_subscription(task_group, gateway_sender)
680 .await?;
681
682 Ok(())
683 }
684
685 async fn cancel_htlc(
686 key: CircuitKey,
687 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
688 ) -> Result<(), LightningRpcError> {
689 let response = ForwardHtlcInterceptResponse {
691 incoming_circuit_key: Some(key),
692 action: ResolveHoldForwardAction::Fail.into(),
693 preimage: vec![],
694 failure_message: vec![],
695 failure_code: FailureCode::TemporaryChannelFailure.into(),
696 ..Default::default()
697 };
698 Self::send_lnd_response(lnd_sender, response).await
699 }
700
701 async fn send_lnd_response(
702 lnd_sender: mpsc::Sender<ForwardHtlcInterceptResponse>,
703 response: ForwardHtlcInterceptResponse,
704 ) -> Result<(), LightningRpcError> {
705 lnd_sender.send(response).await.map_err(|send_error| {
707 LightningRpcError::FailedToCompleteHtlc {
708 failure_reason: format!(
709 "Failed to send ForwardHtlcInterceptResponse to LND {send_error:?}"
710 ),
711 }
712 })
713 }
714
715 async fn lookup_payment(
716 &self,
717 payment_hash: Vec<u8>,
718 client: &mut LndClient,
719 ) -> Result<Option<String>, LightningRpcError> {
720 loop {
723 let payments = client
724 .router()
725 .track_payment_v2(TrackPaymentRequest {
726 payment_hash: payment_hash.clone(),
727 no_inflight_updates: true,
728 })
729 .await;
730
731 match payments {
732 Ok(payments) => {
733 match payments.into_inner().message().await {
735 Ok(Some(payment)) => {
736 if payment.status() == PaymentStatus::Succeeded {
737 return Ok(Some(payment.payment_preimage));
738 }
739
740 let failure_reason = payment.failure_reason();
741 return Err(LightningRpcError::FailedPayment {
742 failure_reason: format!("{failure_reason:?}"),
743 });
744 }
745 outcome => {
749 warn!(
750 target: LOG_LIGHTNING,
751 payment_hash = %PrettyPaymentHash(&payment_hash),
752 outcome = ?outcome,
753 "Payment tracking stream ended or faulted. Trying again in 5 seconds"
754 );
755 sleep(Duration::from_secs(5)).await;
756 }
757 }
758 }
759 Err(err) => {
760 if err.code() == Code::NotFound {
763 return Ok(None);
764 }
765
766 warn!(
767 target: LOG_LIGHTNING,
768 payment_hash = %PrettyPaymentHash(&payment_hash),
769 err = %err.fmt_compact(),
770 "Could not get the status of payment. Trying again in 5 seconds"
771 );
772 sleep(Duration::from_secs(5)).await;
773 }
774 }
775 }
776 }
777
778 async fn lookup_invoice(
784 client: &mut LndClient,
785 payment_hash: &[u8],
786 ) -> Result<Option<Invoice>, LightningRpcError> {
787 match client
788 .invoices()
789 .lookup_invoice_v2(LookupInvoiceMsg {
790 invoice_ref: Some(InvoiceRef::PaymentHash(payment_hash.to_vec())),
791 lookup_modifier: 0,
792 })
793 .await
794 {
795 Ok(invoice) => Ok(Some(invoice.into_inner())),
796 Err(err) if err.code() == Code::NotFound => Ok(None),
797 Err(err) => Err(LightningRpcError::FailedToCompleteHtlc {
798 failure_reason: format!("Failed to look up invoice: {}", err.fmt_compact()),
799 }),
800 }
801 }
802
803 async fn settle_hold_invoice(
809 &self,
810 payment_hash: Vec<u8>,
811 preimage: Preimage,
812 ) -> Result<(), LightningRpcError> {
813 let mut client = self.connect().await?;
814 let invoice = Self::lookup_invoice(&mut client, &payment_hash).await?;
815 match hold_invoice_action(
816 PaymentActionKind::Settle,
817 invoice.as_ref().map(Invoice::state),
818 ) {
819 Ok(HoldInvoiceAction::Complete) => {}
820 Ok(HoldInvoiceAction::AlreadyComplete) => {
821 info!(
822 target: LOG_LIGHTNING,
823 payment_hash = %PrettyPaymentHash(&payment_hash),
824 "HOLD invoice was already settled",
825 );
826 return Ok(());
827 }
828 Err(error) => {
829 warn!(
830 target: LOG_LIGHTNING,
831 state = ?invoice.as_ref().map(Invoice::state),
832 payment_hash = %PrettyPaymentHash(&payment_hash),
833 failure_reason = error.failure_reason,
834 "Cannot settle HOLD invoice",
835 );
836 return Err(if error.permanent {
837 LightningRpcError::HtlcCompletionRejected {
838 failure_reason: error.failure_reason.to_owned(),
839 }
840 } else {
841 LightningRpcError::FailedToCompleteHtlc {
842 failure_reason: error.failure_reason.to_owned(),
843 }
844 });
845 }
846 }
847
848 client
849 .invoices()
850 .settle_invoice(SettleInvoiceMsg {
851 preimage: preimage.0.to_vec(),
852 })
853 .await
854 .map_err(|err| {
855 warn!(
856 target: LOG_LIGHTNING,
857 err = %err.fmt_compact(),
858 payment_hash = %PrettyPaymentHash(&payment_hash),
859 "Failed to settle HOLD invoice",
860 );
861 LightningRpcError::FailedToCompleteHtlc {
862 failure_reason: "Failed to settle HOLD invoice".to_string(),
863 }
864 })?;
865
866 info!(
867 target: LOG_LIGHTNING,
868 payment_hash = %PrettyPaymentHash(&payment_hash),
869 "Successfully settled HOLD invoice",
870 );
871
872 Ok(())
873 }
874
875 async fn cancel_hold_invoice(&self, payment_hash: Vec<u8>) -> Result<(), LightningRpcError> {
882 let mut client = self.connect().await?;
883 let invoice = Self::lookup_invoice(&mut client, &payment_hash).await?;
884 match hold_invoice_action(
885 PaymentActionKind::Cancel,
886 invoice.as_ref().map(Invoice::state),
887 ) {
888 Ok(HoldInvoiceAction::Complete) => {}
889 Ok(HoldInvoiceAction::AlreadyComplete) => {
890 info!(
891 target: LOG_LIGHTNING,
892 payment_hash = %PrettyPaymentHash(&payment_hash),
893 "HOLD invoice was already canceled",
894 );
895 return Ok(());
896 }
897 Err(error) => {
898 warn!(
899 target: LOG_LIGHTNING,
900 state = ?invoice.as_ref().map(Invoice::state),
901 payment_hash = %PrettyPaymentHash(&payment_hash),
902 failure_reason = error.failure_reason,
903 "Cannot cancel HOLD invoice",
904 );
905 return Err(LightningRpcError::HtlcCompletionRejected {
906 failure_reason: error.failure_reason.to_owned(),
907 });
908 }
909 }
910
911 client
912 .invoices()
913 .cancel_invoice(CancelInvoiceMsg {
914 payment_hash: payment_hash.clone(),
915 })
916 .await
917 .map_err(|err| {
918 warn!(
919 target: LOG_LIGHTNING,
920 err = %err.fmt_compact(),
921 payment_hash = %PrettyPaymentHash(&payment_hash),
922 "Failed to cancel HOLD invoice",
923 );
924 LightningRpcError::FailedToCompleteHtlc {
925 failure_reason: "Failed to cancel HOLD invoice".to_string(),
926 }
927 })?;
928
929 info!(
930 target: LOG_LIGHTNING,
931 payment_hash = %PrettyPaymentHash(&payment_hash),
932 "Successfully canceled HOLD invoice",
933 );
934
935 Ok(())
936 }
937}
938
939impl fmt::Debug for GatewayLndClient {
940 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
941 write!(f, "LndClient")
942 }
943}
944
945#[async_trait]
946impl ILnRpcClient for GatewayLndClient {
947 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
948 let mut client = self.connect().await?;
949 let info = client
950 .lightning()
951 .get_info(GetInfoRequest {})
952 .await
953 .map_err(|status| LightningRpcError::FailedToGetNodeInfo {
954 failure_reason: format!("Failed to get node info {status:?}"),
955 })?
956 .into_inner();
957
958 let pub_key: PublicKey =
959 info.identity_pubkey
960 .parse()
961 .map_err(|e| LightningRpcError::FailedToGetNodeInfo {
962 failure_reason: format!("Failed to parse public key {e:?}"),
963 })?;
964
965 let network = match info
966 .chains
967 .first()
968 .ok_or_else(|| LightningRpcError::FailedToGetNodeInfo {
969 failure_reason: "Failed to parse node network".to_string(),
970 })?
971 .network
972 .as_str()
973 {
974 "mainnet" => "bitcoin",
977 other => other,
978 }
979 .to_string();
980
981 return Ok(GetNodeInfoResponse {
982 pub_key,
983 alias: info.alias,
984 network,
985 block_height: info.block_height,
986 synced_to_chain: info.synced_to_chain,
987 });
988 }
989
990 async fn routehints(
991 &self,
992 num_route_hints: usize,
993 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
994 let mut client = self.connect().await?;
995 let mut channels = client
996 .lightning()
997 .list_channels(ListChannelsRequest {
998 active_only: true,
999 inactive_only: false,
1000 public_only: false,
1001 private_only: false,
1002 peer: vec![],
1003 peer_alias_lookup: false,
1004 })
1005 .await
1006 .map_err(|status| LightningRpcError::FailedToGetRouteHints {
1007 failure_reason: format!("Failed to list channels {status:?}"),
1008 })?
1009 .into_inner()
1010 .channels;
1011
1012 channels.sort_by_key(|b| std::cmp::Reverse(b.remote_balance));
1014 channels.truncate(num_route_hints);
1015
1016 let mut route_hints: Vec<RouteHint> = vec![];
1017 for chan in &channels {
1018 let info = client
1019 .lightning()
1020 .get_chan_info(ChanInfoRequest {
1021 chan_id: chan.chan_id,
1022 ..Default::default()
1023 })
1024 .await
1025 .map_err(|status| LightningRpcError::FailedToGetRouteHints {
1026 failure_reason: format!("Failed to get channel info {status:?}"),
1027 })?
1028 .into_inner();
1029
1030 let policy = if info.node1_pub == chan.remote_pubkey {
1036 info.node1_policy
1037 } else {
1038 info.node2_policy
1039 };
1040 let Some(policy) = policy else {
1041 continue;
1042 };
1043 let src_node_id =
1044 PublicKey::from_str(&chan.remote_pubkey).expect("Failed to parse pubkey");
1045 let short_channel_id = chan.chan_id;
1046 let base_msat = policy.fee_base_msat as u32;
1047 let proportional_millionths = policy.fee_rate_milli_msat as u32;
1048 let cltv_expiry_delta = policy.time_lock_delta;
1049 let htlc_maximum_msat = Some(policy.max_htlc_msat);
1050 let htlc_minimum_msat = Some(policy.min_htlc as u64);
1051
1052 let route_hint_hop = RouteHintHop {
1053 src_node_id,
1054 short_channel_id,
1055 base_msat,
1056 proportional_millionths,
1057 cltv_expiry_delta: cltv_expiry_delta as u16,
1058 htlc_minimum_msat,
1059 htlc_maximum_msat,
1060 };
1061 route_hints.push(RouteHint(vec![route_hint_hop]));
1062 }
1063
1064 Ok(GetRouteHintsResponse { route_hints })
1065 }
1066
1067 async fn pay_private(
1068 &self,
1069 invoice: PrunedInvoice,
1070 max_delay: u64,
1071 max_fee: Amount,
1072 ) -> Result<PayInvoiceResponse, LightningRpcError> {
1073 let payment_hash = invoice.payment_hash.to_byte_array().to_vec();
1074 info!(
1075 target: LOG_LIGHTNING,
1076 payment_hash = %PrettyPaymentHash(&payment_hash),
1077 "LND Paying invoice",
1078 );
1079 let mut client = self.connect().await?;
1080
1081 debug!(
1082 target: LOG_LIGHTNING,
1083 payment_hash = %PrettyPaymentHash(&payment_hash),
1084 "pay_private checking if payment for invoice exists"
1085 );
1086
1087 let preimage: Vec<u8> = match self
1089 .lookup_payment(invoice.payment_hash.to_byte_array().to_vec(), &mut client)
1090 .await?
1091 {
1092 Some(preimage) => {
1093 info!(
1094 target: LOG_LIGHTNING,
1095 payment_hash = %PrettyPaymentHash(&payment_hash),
1096 "LND payment already exists for invoice",
1097 );
1098 hex::FromHex::from_hex(preimage.as_str()).map_err(|error| {
1099 LightningRpcError::FailedPayment {
1100 failure_reason: format!("Failed to convert preimage {error:?}"),
1101 }
1102 })?
1103 }
1104 _ => {
1105 let fee_limit_msat: i64 =
1109 max_fee
1110 .msats
1111 .try_into()
1112 .map_err(|error| LightningRpcError::FailedPayment {
1113 failure_reason: format!(
1114 "max_fee_msat exceeds valid LND fee limit ranges {error:?}"
1115 ),
1116 })?;
1117
1118 let amt_msat = invoice.amount.msats.try_into().map_err(|error| {
1119 LightningRpcError::FailedPayment {
1120 failure_reason: format!("amount exceeds valid LND amount ranges {error:?}"),
1121 }
1122 })?;
1123 let final_cltv_delta =
1124 invoice.min_final_cltv_delta.try_into().map_err(|error| {
1125 LightningRpcError::FailedPayment {
1126 failure_reason: format!(
1127 "final cltv delta exceeds valid LND range {error:?}"
1128 ),
1129 }
1130 })?;
1131 if max_delay == 0 {
1135 return Err(LightningRpcError::FailedPayment {
1136 failure_reason: "a max delay of zero would disable LND's CLTV limit"
1137 .to_string(),
1138 });
1139 }
1140 let cltv_limit =
1141 max_delay
1142 .try_into()
1143 .map_err(|error| LightningRpcError::FailedPayment {
1144 failure_reason: format!("max delay exceeds valid LND range {error:?}"),
1145 })?;
1146
1147 let dest_features = wire_features_to_lnd_feature_vec(&invoice.destination_features)
1148 .map_err(|e| LightningRpcError::FailedPayment {
1149 failure_reason: e.to_string(),
1150 })?;
1151
1152 debug!(
1153 target: LOG_LIGHTNING,
1154 payment_hash = %PrettyPaymentHash(&payment_hash),
1155 "LND payment does not exist, will attempt to pay",
1156 );
1157 let payments = client
1158 .router()
1159 .send_payment_v2(SendPaymentRequest {
1160 amt_msat,
1161 dest: invoice.destination.serialize().to_vec(),
1162 dest_features,
1163 payment_hash: invoice.payment_hash.to_byte_array().to_vec(),
1164 payment_addr: invoice.payment_secret.to_vec(),
1165 route_hints: route_hints_to_lnd(&invoice.route_hints),
1166 final_cltv_delta,
1167 cltv_limit,
1168 no_inflight_updates: false,
1169 timeout_seconds: self.payment_timeout_secs,
1170 fee_limit_msat,
1171 time_pref: self.time_pref,
1172 ..Default::default()
1173 })
1174 .await
1175 .map_err(|status| {
1176 warn!(
1177 target: LOG_LIGHTNING,
1178 status = %status,
1179 payment_hash = %PrettyPaymentHash(&payment_hash),
1180 "LND payment request failed",
1181 );
1182 LightningRpcError::FailedPayment {
1183 failure_reason: format!("Failed to make outgoing payment {status:?}"),
1184 }
1185 })?;
1186
1187 debug!(
1188 target: LOG_LIGHTNING,
1189 payment_hash = %PrettyPaymentHash(&payment_hash),
1190 "LND payment request sent, waiting for payment status...",
1191 );
1192 let mut messages = payments.into_inner();
1193 loop {
1194 match messages.message().await {
1195 Ok(Some(payment)) if payment.status() == PaymentStatus::Succeeded => {
1196 info!(
1197 target: LOG_LIGHTNING,
1198 payment_hash = %PrettyPaymentHash(&payment_hash),
1199 "LND payment succeeded for invoice",
1200 );
1201 break hex::FromHex::from_hex(payment.payment_preimage.as_str())
1202 .map_err(|error| LightningRpcError::FailedPayment {
1203 failure_reason: format!("Failed to convert preimage {error:?}"),
1204 })?;
1205 }
1206 Ok(Some(payment)) if payment.status() == PaymentStatus::Failed => {
1207 warn!(
1210 target: LOG_LIGHTNING,
1211 payment_hash = %PrettyPaymentHash(&payment_hash),
1212 status = ?payment.status(),
1213 "LND payment failed",
1214 );
1215 let failure_reason = payment.failure_reason();
1216 return Err(LightningRpcError::FailedPayment {
1217 failure_reason: format!("{failure_reason:?}"),
1218 });
1219 }
1220 Ok(Some(payment)) => {
1226 debug!(
1227 target: LOG_LIGHTNING,
1228 payment_hash = %PrettyPaymentHash(&payment_hash),
1229 status = ?payment.status(),
1230 "LND payment is in flight",
1231 );
1232 continue;
1233 }
1234 stream_end_or_fault => {
1242 warn!(
1243 target: LOG_LIGHTNING,
1244 payment_hash = %PrettyPaymentHash(&payment_hash),
1245 outcome = ?stream_end_or_fault,
1246 "LND payment status stream ended or faulted before a terminal status; resuming tracking",
1247 );
1248 match self
1249 .lookup_payment(payment_hash.clone(), &mut client)
1250 .await?
1251 {
1252 Some(preimage) => {
1253 break hex::FromHex::from_hex(preimage.as_str()).map_err(
1254 |error| LightningRpcError::FailedPayment {
1255 failure_reason: format!(
1256 "Failed to convert preimage {error:?}"
1257 ),
1258 },
1259 )?;
1260 }
1261 None => {
1262 return Err(LightningRpcError::FailedPayment {
1263 failure_reason: format!(
1264 "LND has no record of dispatched payment for hash {:?}",
1265 invoice.payment_hash
1266 ),
1267 });
1268 }
1269 }
1270 }
1271 }
1272 }
1273 }
1274 };
1275 Ok(PayInvoiceResponse {
1276 preimage: Preimage(preimage.try_into().expect("Failed to create preimage")),
1277 })
1278 }
1279
1280 fn supports_private_payments(&self) -> bool {
1283 true
1284 }
1285
1286 async fn outbound_payment_exists(
1287 &self,
1288 payment_hash: sha256::Hash,
1289 ) -> Result<bool, LightningRpcError> {
1290 let payment_hash_bytes = payment_hash.to_byte_array().to_vec();
1291 let mut client = self.connect().await?;
1292
1293 let stream = match client
1298 .router()
1299 .track_payment_v2(TrackPaymentRequest {
1300 payment_hash: payment_hash_bytes.clone(),
1301 no_inflight_updates: false,
1302 })
1303 .await
1304 {
1305 Ok(stream) => stream,
1306 Err(status) if status.code() == Code::NotFound => return Ok(false),
1307 Err(status) => {
1308 return Err(LightningRpcError::FailedPayment {
1309 failure_reason: format!(
1310 "Failed to look up payment {}: {status:?}",
1311 PrettyPaymentHash(&payment_hash_bytes),
1312 ),
1313 });
1314 }
1315 };
1316
1317 match stream.into_inner().message().await {
1318 Ok(Some(_)) => Ok(true),
1319 Err(status) if status.code() == Code::NotFound => Ok(false),
1320 outcome => Err(LightningRpcError::FailedPayment {
1325 failure_reason: format!(
1326 "Payment lookup stream gave no answer for {}: {outcome:?}",
1327 PrettyPaymentHash(&payment_hash_bytes),
1328 ),
1329 }),
1330 }
1331 }
1332
1333 async fn route_htlcs<'a>(
1334 self: Box<Self>,
1335 task_group: &TaskGroup,
1336 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
1337 const CHANNEL_SIZE: usize = 100;
1338
1339 let (gateway_sender, gateway_receiver) =
1341 mpsc::channel::<InterceptPaymentRequest>(CHANNEL_SIZE);
1342
1343 let (lnd_sender, lnd_rx) = mpsc::channel::<ForwardHtlcInterceptResponse>(CHANNEL_SIZE);
1344
1345 self.spawn_interceptor(
1346 task_group,
1347 lnd_sender.clone(),
1348 lnd_rx,
1349 gateway_sender.clone(),
1350 )
1351 .await?;
1352 let new_client = Arc::new(Self {
1353 address: self.address.clone(),
1354 tls_cert: self.tls_cert.clone(),
1355 macaroon: self.macaroon.clone(),
1356 time_pref: self.time_pref,
1357 payment_timeout_secs: self.payment_timeout_secs,
1358 lnd_sender: Some(lnd_sender.clone()),
1359 lnv2_filter: self.lnv2_filter.clone(),
1360 });
1361 Ok((Box::pin(ReceiverStream::new(gateway_receiver)), new_client))
1362 }
1363
1364 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
1365 let incoming_circuit = htlc.incoming_circuit();
1366 let InterceptPaymentResponse {
1367 action,
1368 payment_hash,
1369 incoming_chan_id: _,
1370 htlc_id: _,
1371 } = htlc;
1372
1373 let (action, preimage) = match action {
1374 PaymentAction::Settle(preimage) => (ResolveHoldForwardAction::Settle, preimage),
1375 PaymentAction::Cancel => (ResolveHoldForwardAction::Fail, Preimage([0; 32])),
1376 PaymentAction::Forward => (ResolveHoldForwardAction::Resume, Preimage([0; 32])),
1377 };
1378
1379 let Some((chan_id, htlc_id)) = incoming_circuit else {
1386 return match action {
1389 ResolveHoldForwardAction::Settle => {
1390 self.settle_hold_invoice(payment_hash.to_byte_array().to_vec(), preimage)
1391 .await
1392 }
1393 _ => {
1397 self.cancel_hold_invoice(payment_hash.to_byte_array().to_vec())
1398 .await
1399 }
1400 };
1401 };
1402
1403 let Some(lnd_sender) = self.lnd_sender.clone() else {
1405 crit!("Gatewayd has not started to route HTLCs");
1406 return Err(LightningRpcError::FailedToCompleteHtlc {
1407 failure_reason: "Gatewayd has not started to route HTLCs".to_string(),
1408 });
1409 };
1410
1411 let response = ForwardHtlcInterceptResponse {
1412 incoming_circuit_key: Some(CircuitKey { chan_id, htlc_id }),
1413 action: action.into(),
1414 preimage: preimage.0.to_vec(),
1415 failure_message: vec![],
1416 failure_code: FailureCode::TemporaryChannelFailure.into(),
1417 ..Default::default()
1418 };
1419
1420 Self::send_lnd_response(lnd_sender, response).await
1421 }
1422
1423 async fn create_invoice(
1424 &self,
1425 create_invoice_request: CreateInvoiceRequest,
1426 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
1427 let mut client = self.connect().await?;
1428 let description = create_invoice_request
1429 .description
1430 .unwrap_or(InvoiceDescription::Direct(String::new()));
1431
1432 if let Some(payment_hash_value) = create_invoice_request.payment_hash {
1433 let payment_hash = payment_hash_value.to_byte_array().to_vec();
1434 let hold_invoice_request = match description {
1435 InvoiceDescription::Direct(description) => AddHoldInvoiceRequest {
1436 memo: description,
1437 hash: payment_hash.clone(),
1438 value_msat: create_invoice_request.amount_msat as i64,
1439 expiry: i64::from(create_invoice_request.expiry_secs),
1440 ..Default::default()
1441 },
1442 InvoiceDescription::Hash(desc_hash) => AddHoldInvoiceRequest {
1443 description_hash: desc_hash.to_byte_array().to_vec(),
1444 hash: payment_hash.clone(),
1445 value_msat: create_invoice_request.amount_msat as i64,
1446 expiry: i64::from(create_invoice_request.expiry_secs),
1447 ..Default::default()
1448 },
1449 };
1450
1451 let hold_invoice_response = client
1452 .invoices()
1453 .add_hold_invoice(hold_invoice_request)
1454 .await
1455 .map_err(|e| LightningRpcError::FailedToGetInvoice {
1456 failure_reason: e.to_string(),
1457 })?;
1458
1459 let invoice = hold_invoice_response.into_inner().payment_request;
1460 Ok(CreateInvoiceResponse { invoice })
1461 } else {
1462 let invoice = match description {
1463 InvoiceDescription::Direct(description) => Invoice {
1464 memo: description,
1465 value_msat: create_invoice_request.amount_msat as i64,
1466 expiry: i64::from(create_invoice_request.expiry_secs),
1467 ..Default::default()
1468 },
1469 InvoiceDescription::Hash(desc_hash) => Invoice {
1470 description_hash: desc_hash.to_byte_array().to_vec(),
1471 value_msat: create_invoice_request.amount_msat as i64,
1472 expiry: i64::from(create_invoice_request.expiry_secs),
1473 ..Default::default()
1474 },
1475 };
1476
1477 let add_invoice_response =
1478 client.lightning().add_invoice(invoice).await.map_err(|e| {
1479 LightningRpcError::FailedToGetInvoice {
1480 failure_reason: e.to_string(),
1481 }
1482 })?;
1483
1484 let invoice = add_invoice_response.into_inner().payment_request;
1485 Ok(CreateInvoiceResponse { invoice })
1486 }
1487 }
1488
1489 async fn get_ln_onchain_address(
1490 &self,
1491 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
1492 let mut client = self.connect().await?;
1493
1494 match client
1495 .wallet()
1496 .next_addr(AddrRequest {
1497 account: String::new(), r#type: 4, change: false,
1500 })
1501 .await
1502 {
1503 Ok(response) => Ok(GetLnOnchainAddressResponse {
1504 address: response.into_inner().addr,
1505 }),
1506 Err(e) => Err(LightningRpcError::FailedToGetLnOnchainAddress {
1507 failure_reason: format!("Failed to get funding address {e:?}"),
1508 }),
1509 }
1510 }
1511
1512 async fn send_onchain(
1513 &self,
1514 SendOnchainRequest {
1515 address,
1516 amount,
1517 fee_rate_sats_per_vbyte,
1518 }: SendOnchainRequest,
1519 ) -> Result<SendOnchainResponse, LightningRpcError> {
1520 #[allow(deprecated)]
1521 let request = match amount {
1522 BitcoinAmountOrAll::All => SendCoinsRequest {
1523 addr: address.assume_checked().to_string(),
1524 amount: 0,
1525 target_conf: 0,
1526 sat_per_vbyte: fee_rate_sats_per_vbyte,
1527 sat_per_byte: 0,
1528 send_all: true,
1529 label: String::new(),
1530 min_confs: 0,
1531 spend_unconfirmed: true,
1532 ..Default::default()
1533 },
1534 BitcoinAmountOrAll::Amount(amount) => SendCoinsRequest {
1535 addr: address.assume_checked().to_string(),
1536 amount: amount.to_sat() as i64,
1537 target_conf: 0,
1538 sat_per_vbyte: fee_rate_sats_per_vbyte,
1539 sat_per_byte: 0,
1540 send_all: false,
1541 label: String::new(),
1542 min_confs: 0,
1543 spend_unconfirmed: true,
1544 ..Default::default()
1545 },
1546 };
1547
1548 match self.connect().await?.lightning().send_coins(request).await {
1549 Ok(res) => Ok(SendOnchainResponse {
1550 txid: res.into_inner().txid,
1551 }),
1552 Err(e) => Err(LightningRpcError::FailedToWithdrawOnchain {
1553 failure_reason: format!("Failed to withdraw funds on-chain {e:?}"),
1554 }),
1555 }
1556 }
1557
1558 async fn open_channel(
1559 &self,
1560 crate::OpenChannelRequest {
1561 pubkey,
1562 host,
1563 channel_size_sats,
1564 push_amount_sats,
1565 fee_rate_sats_per_vbyte,
1566 base_fee_msat,
1567 parts_per_million,
1568 }: crate::OpenChannelRequest,
1569 ) -> Result<OpenChannelResponse, LightningRpcError> {
1570 let mut client = self.connect().await?;
1571
1572 self.connect_peer_if_needed(&mut client, pubkey, host)
1573 .await?;
1574
1575 let mut open_request = OpenChannelRequest {
1578 node_pubkey: pubkey.serialize().to_vec(),
1579 local_funding_amount: channel_size_sats.try_into().expect("u64 -> i64"),
1580 push_sat: push_amount_sats.try_into().expect("u64 -> i64"),
1581 ..Default::default()
1582 };
1583 if let Some(rate) = fee_rate_sats_per_vbyte {
1584 open_request.sat_per_vbyte = rate;
1585 }
1586 if let Some(base_fee) = base_fee_msat {
1587 open_request.base_fee = base_fee;
1588 open_request.use_base_fee = true;
1589 }
1590 if let Some(ppm) = parts_per_million {
1591 open_request.fee_rate = ppm;
1592 open_request.use_fee_rate = true;
1593 }
1594
1595 match client.lightning().open_channel_sync(open_request).await {
1597 Ok(res) => Ok(OpenChannelResponse {
1598 funding_txid: match res.into_inner().funding_txid {
1599 Some(txid) => match txid {
1600 FundingTxid::FundingTxidBytes(mut bytes) => {
1601 bytes.reverse();
1602 hex::encode(bytes)
1603 }
1604 FundingTxid::FundingTxidStr(str) => str,
1605 },
1606 None => String::new(),
1607 },
1608 }),
1609 Err(e) => Err(LightningRpcError::FailedToOpenChannel {
1610 failure_reason: format!("Failed to open channel {e:?}"),
1611 }),
1612 }
1613 }
1614
1615 async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
1616 let mut client = self.connect().await?;
1617 self.connect_peer_if_needed(
1618 &mut client,
1619 payload.node_address.pubkey,
1620 payload.node_address.host_with_port(),
1621 )
1622 .await
1623 }
1624
1625 async fn close_channels_with_peer(
1626 &self,
1627 CloseChannelsWithPeerRequest {
1628 pubkey,
1629 force,
1630 sats_per_vbyte,
1631 }: CloseChannelsWithPeerRequest,
1632 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
1633 let mut client = self.connect().await?;
1634
1635 let channels_with_peer = client
1636 .lightning()
1637 .list_channels(ListChannelsRequest {
1638 active_only: false,
1639 inactive_only: false,
1640 public_only: false,
1641 private_only: false,
1642 peer: pubkey.serialize().to_vec(),
1643 peer_alias_lookup: false,
1644 })
1645 .await
1646 .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1647 failure_reason: format!("Failed to list channels {e:?}"),
1648 })?
1649 .into_inner()
1650 .channels;
1651
1652 for channel in &channels_with_peer {
1653 let channel_point =
1654 bitcoin::OutPoint::from_str(&channel.channel_point).map_err(|e| {
1655 LightningRpcError::FailedToCloseChannelsWithPeer {
1656 failure_reason: format!("Failed to parse channel point {e:?}"),
1657 }
1658 })?;
1659
1660 if force {
1661 client
1662 .lightning()
1663 .close_channel(CloseChannelRequest {
1664 channel_point: Some(ChannelPoint {
1665 funding_txid: Some(
1666 tonic_lnd::lnrpc::channel_point::FundingTxid::FundingTxidBytes(
1667 <bitcoin::Txid as AsRef<[u8]>>::as_ref(&channel_point.txid)
1668 .to_vec(),
1669 ),
1670 ),
1671 output_index: channel_point.vout,
1672 }),
1673 force,
1674 ..Default::default()
1675 })
1676 .await
1677 .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1678 failure_reason: format!("Failed to close channel {e:?}"),
1679 })?;
1680 } else {
1681 client
1682 .lightning()
1683 .close_channel(CloseChannelRequest {
1684 channel_point: Some(ChannelPoint {
1685 funding_txid: Some(
1686 tonic_lnd::lnrpc::channel_point::FundingTxid::FundingTxidBytes(
1687 <bitcoin::Txid as AsRef<[u8]>>::as_ref(&channel_point.txid)
1688 .to_vec(),
1689 ),
1690 ),
1691 output_index: channel_point.vout,
1692 }),
1693 force,
1694 sat_per_vbyte: sats_per_vbyte.unwrap_or_default(),
1695 ..Default::default()
1696 })
1697 .await
1698 .map_err(|e| LightningRpcError::FailedToCloseChannelsWithPeer {
1699 failure_reason: format!("Failed to close channel {e:?}"),
1700 })?;
1701 }
1702 }
1703
1704 Ok(CloseChannelsWithPeerResponse {
1705 num_channels_closed: channels_with_peer.len() as u32,
1706 })
1707 }
1708
1709 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
1710 let mut client = self.connect().await?;
1711
1712 let peer_addresses: BTreeMap<String, String> = client
1714 .lightning()
1715 .list_peers(ListPeersRequest {
1716 latest_error: false,
1717 })
1718 .await
1719 .map(|resp| {
1720 resp.into_inner()
1721 .peers
1722 .into_iter()
1723 .filter_map(|peer| {
1724 if peer.address.is_empty() {
1725 None
1726 } else {
1727 Some((peer.pub_key, peer.address))
1728 }
1729 })
1730 .collect()
1731 })
1732 .unwrap_or_default();
1733
1734 let fee_report: BTreeMap<u64, (u64, u64)> = client
1737 .lightning()
1738 .fee_report(FeeReportRequest {})
1739 .await
1740 .map(|resp| {
1741 resp.into_inner()
1742 .channel_fees
1743 .into_iter()
1744 .map(|report| {
1745 let base_fee_msat = u64::try_from(report.base_fee_msat).unwrap_or_default();
1746 let parts_per_million =
1747 u64::try_from(report.fee_per_mil).unwrap_or_default();
1748 (report.chan_id, (base_fee_msat, parts_per_million))
1749 })
1750 .collect()
1751 })
1752 .unwrap_or_default();
1753
1754 match client
1755 .lightning()
1756 .list_channels(ListChannelsRequest {
1757 active_only: false,
1758 inactive_only: false,
1759 public_only: false,
1760 private_only: false,
1761 peer: vec![],
1762 peer_alias_lookup: true,
1763 })
1764 .await
1765 {
1766 Ok(response) => Ok(ListChannelsResponse {
1767 channels: response
1768 .into_inner()
1769 .channels
1770 .into_iter()
1771 .map(|channel| {
1772 let channel_size_sats = channel.capacity.try_into().expect("i64 -> u64");
1773
1774 let local_balance_sats: u64 =
1775 channel.local_balance.try_into().expect("i64 -> u64");
1776 let local_channel_reserve_sats: u64 = match channel.local_constraints {
1777 Some(constraints) => constraints.chan_reserve_sat,
1778 None => 0,
1779 };
1780
1781 let outbound_liquidity_sats =
1782 local_balance_sats.saturating_sub(local_channel_reserve_sats);
1783
1784 let remote_balance_sats: u64 =
1785 channel.remote_balance.try_into().expect("i64 -> u64");
1786 let remote_channel_reserve_sats: u64 = match channel.remote_constraints {
1787 Some(constraints) => constraints.chan_reserve_sat,
1788 None => 0,
1789 };
1790
1791 let inbound_liquidity_sats =
1792 remote_balance_sats.saturating_sub(remote_channel_reserve_sats);
1793
1794 let funding_outpoint = OutPoint::from_str(&channel.channel_point).ok();
1795
1796 let remote_address = peer_addresses.get(&channel.remote_pubkey).cloned();
1797
1798 let (base_fee_msat, parts_per_million) =
1799 match fee_report.get(&channel.chan_id) {
1800 Some((base, ppm)) => (Some(*base), Some(*ppm)),
1801 None => (None, None),
1802 };
1803
1804 ChannelInfo {
1805 remote_pubkey: PublicKey::from_str(&channel.remote_pubkey)
1806 .expect("Lightning node returned invalid remote channel pubkey"),
1807 channel_size_sats,
1808 outbound_liquidity_sats,
1809 inbound_liquidity_sats,
1810 is_active: channel.active,
1811 funding_outpoint,
1812 remote_node_alias: if channel.peer_alias.is_empty() {
1813 None
1814 } else {
1815 Some(channel.peer_alias.clone())
1816 },
1817 remote_address,
1818 base_fee_msat,
1819 parts_per_million,
1820 }
1821 })
1822 .collect(),
1823 }),
1824 Err(e) => Err(LightningRpcError::FailedToListChannels {
1825 failure_reason: format!("Failed to list active channels {e:?}"),
1826 }),
1827 }
1828 }
1829
1830 async fn set_channel_fees(
1831 &self,
1832 payload: SetChannelFeesRequest,
1833 ) -> Result<(), LightningRpcError> {
1834 let mut client = self.connect().await?;
1835
1836 let target = format!(
1842 "{}:{}",
1843 payload.funding_outpoint.txid, payload.funding_outpoint.vout
1844 );
1845 let channel = client
1846 .lightning()
1847 .list_channels(ListChannelsRequest::default())
1848 .await
1849 .map_err(|e| LightningRpcError::FailedToSetChannelFees {
1850 failure_reason: format!("Failed to list channels: {e:?}"),
1851 })?
1852 .into_inner()
1853 .channels
1854 .into_iter()
1855 .find(|c| c.channel_point == target)
1856 .ok_or_else(|| LightningRpcError::FailedToSetChannelFees {
1857 failure_reason: format!("No channel found with funding outpoint {target}"),
1858 })?;
1859
1860 let our_pubkey = client
1861 .lightning()
1862 .get_info(GetInfoRequest {})
1863 .await
1864 .map_err(|e| LightningRpcError::FailedToSetChannelFees {
1865 failure_reason: format!("Failed to get node info: {e:?}"),
1866 })?
1867 .into_inner()
1868 .identity_pubkey;
1869
1870 let edge = client
1871 .lightning()
1872 .get_chan_info(ChanInfoRequest {
1873 chan_id: channel.chan_id,
1874 ..Default::default()
1875 })
1876 .await
1877 .map_err(|e| LightningRpcError::FailedToSetChannelFees {
1878 failure_reason: format!("Failed to get channel info: {e:?}"),
1879 })?
1880 .into_inner();
1881
1882 let current_policy = if edge.node1_pub == our_pubkey {
1886 edge.node1_policy
1887 } else if edge.node2_pub == our_pubkey {
1888 edge.node2_policy
1889 } else {
1890 edge.node1_policy
1891 };
1892
1893 let fee_rate_ppm = u32::try_from(payload.parts_per_million).map_err(|_| {
1894 LightningRpcError::FailedToSetChannelFees {
1895 failure_reason: format!(
1896 "parts_per_million {} does not fit in u32",
1897 payload.parts_per_million,
1898 ),
1899 }
1900 })?;
1901
1902 let base_fee_msat = i64::try_from(payload.base_fee_msat).map_err(|_| {
1903 LightningRpcError::FailedToSetChannelFees {
1904 failure_reason: format!(
1905 "base_fee_msat {} does not fit in i64",
1906 payload.base_fee_msat,
1907 ),
1908 }
1909 })?;
1910
1911 let time_lock_delta = current_policy
1915 .as_ref()
1916 .map(|p| p.time_lock_delta)
1917 .unwrap_or(40);
1918 let max_htlc_msat = current_policy
1919 .as_ref()
1920 .map(|p| p.max_htlc_msat)
1921 .unwrap_or(0);
1922 let min_htlc_msat = current_policy
1923 .as_ref()
1924 .map(|p| p.min_htlc as u64)
1925 .unwrap_or(0);
1926
1927 let chan_point = ChannelPoint {
1928 funding_txid: Some(FundingTxid::FundingTxidBytes(
1929 <bitcoin::Txid as AsRef<[u8]>>::as_ref(&payload.funding_outpoint.txid).to_vec(),
1930 )),
1931 output_index: payload.funding_outpoint.vout,
1932 };
1933
1934 let request = PolicyUpdateRequest {
1935 base_fee_msat,
1936 fee_rate_ppm,
1937 time_lock_delta,
1938 max_htlc_msat,
1939 min_htlc_msat,
1940 min_htlc_msat_specified: false,
1941 scope: Some(PolicyUpdateScope::ChanPoint(chan_point)),
1942 ..Default::default()
1943 };
1944
1945 let response = client
1946 .lightning()
1947 .update_channel_policy(request)
1948 .await
1949 .map_err(|e| LightningRpcError::FailedToSetChannelFees {
1950 failure_reason: format!("update_channel_policy failed: {e:?}"),
1951 })?
1952 .into_inner();
1953
1954 if !response.failed_updates.is_empty() {
1955 let details = response
1956 .failed_updates
1957 .iter()
1958 .map(|f| {
1959 let outpoint = f
1960 .outpoint
1961 .as_ref()
1962 .map(|op| format!("{}:{}", op.txid_str, op.output_index))
1963 .unwrap_or_else(|| "<unknown outpoint>".to_string());
1964 let reason = UpdateFailure::try_from(f.reason)
1965 .map(|r| r.as_str_name())
1966 .unwrap_or("UPDATE_FAILURE_UNKNOWN");
1967 format!("{outpoint}: {reason} ({})", f.update_error)
1968 })
1969 .collect::<Vec<_>>()
1970 .join("; ");
1971 return Err(LightningRpcError::FailedToSetChannelFees {
1972 failure_reason: format!("update_channel_policy reported failures: {details}"),
1973 });
1974 }
1975
1976 Ok(())
1977 }
1978
1979 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
1980 let mut client = self.connect().await?;
1981
1982 let wallet_balance_response = client
1983 .lightning()
1984 .wallet_balance(WalletBalanceRequest {
1985 ..Default::default()
1986 })
1987 .await
1988 .map_err(|e| LightningRpcError::FailedToGetBalances {
1989 failure_reason: format!("Failed to get on-chain balance {e:?}"),
1990 })?
1991 .into_inner();
1992
1993 let channel_balance_response = client
1994 .lightning()
1995 .channel_balance(ChannelBalanceRequest {})
1996 .await
1997 .map_err(|e| LightningRpcError::FailedToGetBalances {
1998 failure_reason: format!("Failed to get lightning balance {e:?}"),
1999 })?
2000 .into_inner();
2001 let total_outbound = channel_balance_response.local_balance.unwrap_or_default();
2002 let unsettled_outbound = channel_balance_response
2003 .unsettled_local_balance
2004 .unwrap_or_default();
2005 let pending_outbound = channel_balance_response
2006 .pending_open_local_balance
2007 .unwrap_or_default();
2008 let lightning_balance_msats = total_outbound
2009 .msat
2010 .saturating_sub(unsettled_outbound.msat)
2011 .saturating_sub(pending_outbound.msat);
2012
2013 let total_inbound = channel_balance_response.remote_balance.unwrap_or_default();
2014 let unsettled_inbound = channel_balance_response
2015 .unsettled_remote_balance
2016 .unwrap_or_default();
2017 let pending_inbound = channel_balance_response
2018 .pending_open_remote_balance
2019 .unwrap_or_default();
2020 let inbound_lightning_liquidity_msats = total_inbound
2021 .msat
2022 .saturating_sub(unsettled_inbound.msat)
2023 .saturating_sub(pending_inbound.msat);
2024
2025 Ok(GetBalancesResponse {
2026 onchain_balance_sats: (wallet_balance_response.total_balance
2027 + wallet_balance_response.reserved_balance_anchor_chan)
2028 as u64,
2029 lightning_balance_msats,
2030 inbound_lightning_liquidity_msats,
2031 })
2032 }
2033
2034 async fn get_invoice(
2035 &self,
2036 get_invoice_request: GetInvoiceRequest,
2037 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
2038 let mut client = self.connect().await?;
2039 let invoice = client
2040 .invoices()
2041 .lookup_invoice_v2(LookupInvoiceMsg {
2042 invoice_ref: Some(InvoiceRef::PaymentHash(
2043 get_invoice_request.payment_hash.consensus_encode_to_vec(),
2044 )),
2045 ..Default::default()
2046 })
2047 .await;
2048 let invoice = match invoice {
2049 Ok(invoice) => invoice.into_inner(),
2050 Err(_) => return Ok(None),
2051 };
2052 let preimage: [u8; 32] = invoice
2053 .clone()
2054 .r_preimage
2055 .try_into()
2056 .expect("Could not convert preimage");
2057 let status = match &invoice.state() {
2058 InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
2059 InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
2060 _ => fedimint_gateway_common::PaymentStatus::Pending,
2061 };
2062
2063 Ok(Some(GetInvoiceResponse {
2064 preimage: Some(preimage.consensus_encode_to_hex()),
2065 payment_hash: Some(
2066 sha256::Hash::from_slice(&invoice.r_hash).expect("Could not convert payment hash"),
2067 ),
2068 amount: Amount::from_msats(invoice.value_msat as u64),
2069 created_at: UNIX_EPOCH + Duration::from_secs(invoice.creation_date as u64),
2070 status,
2071 }))
2072 }
2073
2074 async fn list_transactions(
2075 &self,
2076 start_secs: u64,
2077 end_secs: u64,
2078 ) -> Result<ListTransactionsResponse, LightningRpcError> {
2079 let mut client = self.connect().await?;
2080 let payments = client
2081 .lightning()
2082 .list_payments(ListPaymentsRequest {
2083 ..Default::default()
2085 })
2086 .await
2087 .map_err(|err| LightningRpcError::FailedToListTransactions {
2088 failure_reason: err.to_string(),
2089 })?
2090 .into_inner();
2091
2092 let mut payments = payments
2093 .payments
2094 .iter()
2095 .filter_map(|payment| {
2096 let timestamp_secs = (payment.creation_time_ns / 1_000_000_000) as u64;
2097 if timestamp_secs < start_secs || timestamp_secs >= end_secs {
2098 return None;
2099 }
2100 let payment_hash = sha256::Hash::from_str(&payment.payment_hash).ok();
2101 let preimage = (!payment.payment_preimage.is_empty())
2102 .then_some(payment.payment_preimage.clone());
2103 let status = match &payment.status() {
2104 PaymentStatus::Succeeded => fedimint_gateway_common::PaymentStatus::Succeeded,
2105 PaymentStatus::Failed => fedimint_gateway_common::PaymentStatus::Failed,
2106 _ => fedimint_gateway_common::PaymentStatus::Pending,
2107 };
2108 Some(PaymentDetails {
2109 payment_hash,
2110 preimage,
2111 payment_kind: PaymentKind::Bolt11,
2112 amount: Amount::from_msats(payment.value_msat as u64),
2113 direction: PaymentDirection::Outbound,
2114 status,
2115 timestamp_secs,
2116 })
2117 })
2118 .collect::<Vec<_>>();
2119
2120 let invoices = client
2121 .lightning()
2122 .list_invoices(ListInvoiceRequest {
2123 pending_only: false,
2124 ..Default::default()
2126 })
2127 .await
2128 .map_err(|err| LightningRpcError::FailedToListTransactions {
2129 failure_reason: err.to_string(),
2130 })?
2131 .into_inner();
2132
2133 let mut incoming_payments = invoices
2134 .invoices
2135 .iter()
2136 .filter_map(|invoice| {
2137 let timestamp_secs = invoice.settle_date as u64;
2138 if timestamp_secs < start_secs || timestamp_secs >= end_secs {
2139 return None;
2140 }
2141 let status = match &invoice.state() {
2142 InvoiceState::Settled => fedimint_gateway_common::PaymentStatus::Succeeded,
2143 InvoiceState::Canceled => fedimint_gateway_common::PaymentStatus::Failed,
2144 _ => return None,
2145 };
2146 let preimage = (!invoice.r_preimage.is_empty())
2147 .then_some(invoice.r_preimage.encode_hex::<String>());
2148 Some(PaymentDetails {
2149 payment_hash: Some(
2150 sha256::Hash::from_slice(&invoice.r_hash)
2151 .expect("Could not convert payment hash"),
2152 ),
2153 preimage,
2154 payment_kind: PaymentKind::Bolt11,
2155 amount: Amount::from_msats(invoice.value_msat as u64),
2156 direction: PaymentDirection::Inbound,
2157 status,
2158 timestamp_secs,
2159 })
2160 })
2161 .collect::<Vec<_>>();
2162
2163 payments.append(&mut incoming_payments);
2164 payments.sort_by_key(|p| p.timestamp_secs);
2165
2166 Ok(ListTransactionsResponse {
2167 transactions: payments,
2168 })
2169 }
2170
2171 fn create_offer(
2172 &self,
2173 _amount_msat: Option<Amount>,
2174 _description: Option<String>,
2175 _expiry_secs: Option<u32>,
2176 _quantity: Option<u64>,
2177 ) -> Result<String, LightningRpcError> {
2178 Err(LightningRpcError::Bolt12Error {
2179 failure_reason: "LND Does not support Bolt12".to_string(),
2180 })
2181 }
2182
2183 async fn pay_offer(
2184 &self,
2185 _offer: String,
2186 _quantity: Option<u64>,
2187 _amount: Option<Amount>,
2188 _payer_note: Option<String>,
2189 ) -> Result<Preimage, LightningRpcError> {
2190 Err(LightningRpcError::Bolt12Error {
2191 failure_reason: "LND Does not support Bolt12".to_string(),
2192 })
2193 }
2194
2195 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
2196 Ok(())
2198 }
2199}
2200
2201fn route_hints_to_lnd(
2202 route_hints: &[fedimint_ln_common::route_hints::RouteHint],
2203) -> Vec<tonic_lnd::lnrpc::RouteHint> {
2204 route_hints
2205 .iter()
2206 .map(|hint| tonic_lnd::lnrpc::RouteHint {
2207 hop_hints: hint
2208 .0
2209 .iter()
2210 .map(|hop| tonic_lnd::lnrpc::HopHint {
2211 node_id: hop.src_node_id.serialize().encode_hex(),
2212 chan_id: hop.short_channel_id,
2213 fee_base_msat: hop.base_msat,
2214 fee_proportional_millionths: hop.proportional_millionths,
2215 cltv_expiry_delta: u32::from(hop.cltv_expiry_delta),
2216 })
2217 .collect(),
2218 })
2219 .collect()
2220}
2221
2222fn wire_features_to_lnd_feature_vec(features_wire_encoded: &[u8]) -> anyhow::Result<Vec<i32>> {
2223 ensure!(
2224 features_wire_encoded.len() <= 1_000,
2225 "Will not process feature bit vectors larger than 1000 byte"
2226 );
2227
2228 let lnd_features = features_wire_encoded
2229 .iter()
2230 .rev()
2231 .enumerate()
2232 .flat_map(|(byte_idx, &feature_byte)| {
2233 (0..8).filter_map(move |bit_idx| {
2234 if (feature_byte & (1u8 << bit_idx)) != 0 {
2235 Some(
2236 i32::try_from(byte_idx * 8 + bit_idx)
2237 .expect("Index will never exceed i32::MAX for feature vectors <8MB"),
2238 )
2239 } else {
2240 None
2241 }
2242 })
2243 })
2244 .collect::<Vec<_>>();
2245
2246 Ok(lnd_features)
2247}
2248
2249struct PrettyPaymentHash<'a>(&'a Vec<u8>);
2251
2252impl Display for PrettyPaymentHash<'_> {
2253 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2254 write!(f, "payment_hash={}", self.0.encode_hex::<String>())
2255 }
2256}
2257
2258#[cfg(test)]
2259mod tests;