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