1pub mod ldk;
2pub mod lnd;
3pub mod metrics;
4
5use std::fmt::Debug;
6use std::str::FromStr;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use bitcoin::Network;
11use bitcoin::hashes::sha256;
12use fedimint_core::Amount;
13use fedimint_core::encoding::{Decodable, Encodable};
14use fedimint_core::envs::{FM_IN_DEVIMINT_ENV, is_env_var_set};
15use fedimint_core::secp256k1::PublicKey;
16use fedimint_core::task::TaskGroup;
17use fedimint_core::time::now;
18use fedimint_core::util::{FmtCompactResult as _, backoff_util, retry};
19use fedimint_gateway_common::{
20 ChannelInfo, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, ConnectPeerRequest,
21 GetInvoiceRequest, GetInvoiceResponse, LightningInfo, ListTransactionsResponse,
22 OpenChannelRequest, SendOnchainRequest, SetChannelFeesRequest,
23};
24use fedimint_ln_common::PrunedInvoice;
25pub use fedimint_ln_common::contracts::Preimage;
26use fedimint_ln_common::route_hints::RouteHint;
27use fedimint_logging::LOG_LIGHTNING;
28use fedimint_metrics::HistogramExt as _;
29use futures::future::BoxFuture;
30use futures::stream::BoxStream;
31use lightning_invoice::Bolt11Invoice;
32use serde::{Deserialize, Serialize};
33use thiserror::Error;
34use tracing::{info, trace, warn};
35
36pub const MAX_LIGHTNING_RETRIES: u32 = 10;
37
38pub type RouteHtlcStream<'a> = BoxStream<'a, InterceptPaymentRequest>;
39
40pub type Lnv2HoldInvoiceFilter =
46 Arc<dyn Fn(sha256::Hash) -> BoxFuture<'static, bool> + Send + Sync + 'static>;
47
48#[derive(
49 Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
50)]
51pub enum LightningRpcError {
52 #[error("Failed to connect to Lightning node")]
53 FailedToConnect,
54 #[error("Failed to retrieve node info: {failure_reason}")]
55 FailedToGetNodeInfo { failure_reason: String },
56 #[error("Failed to retrieve route hints: {failure_reason}")]
57 FailedToGetRouteHints { failure_reason: String },
58 #[error("Payment failed: {failure_reason}")]
59 FailedPayment { failure_reason: String },
60 #[error("Failed to route HTLCs: {failure_reason}")]
61 FailedToRouteHtlcs { failure_reason: String },
62 #[error("Failed to complete HTLC: {failure_reason}")]
63 FailedToCompleteHtlc { failure_reason: String },
64 #[error("Failed to open channel: {failure_reason}")]
65 FailedToOpenChannel { failure_reason: String },
66 #[error("Failed to close channel: {failure_reason}")]
67 FailedToCloseChannelsWithPeer { failure_reason: String },
68 #[error("Failed to set channel fees: {failure_reason}")]
69 FailedToSetChannelFees { failure_reason: String },
70 #[error("Failed to get Invoice: {failure_reason}")]
71 FailedToGetInvoice { failure_reason: String },
72 #[error("Failed to list transactions: {failure_reason}")]
73 FailedToListTransactions { failure_reason: String },
74 #[error("Failed to get funding address: {failure_reason}")]
75 FailedToGetLnOnchainAddress { failure_reason: String },
76 #[error("Failed to withdraw funds on-chain: {failure_reason}")]
77 FailedToWithdrawOnchain { failure_reason: String },
78 #[error("Failed to connect to peer: {failure_reason}")]
79 FailedToConnectToPeer { failure_reason: String },
80 #[error("Failed to list active channels: {failure_reason}")]
81 FailedToListChannels { failure_reason: String },
82 #[error("Failed to get balances: {failure_reason}")]
83 FailedToGetBalances { failure_reason: String },
84 #[error("Failed to sync to chain: {failure_reason}")]
85 FailedToSyncToChain { failure_reason: String },
86 #[error("Invalid metadata: {failure_reason}")]
87 InvalidMetadata { failure_reason: String },
88 #[error("Bolt12 Error: {failure_reason}")]
89 Bolt12Error { failure_reason: String },
90 #[error("HTLC completion cannot reach the requested outcome: {failure_reason}")]
93 HtlcCompletionRejected { failure_reason: String },
94}
95
96#[derive(Clone, Debug)]
98pub struct LightningContext {
99 pub lnrpc: Arc<dyn ILnRpcClient>,
100 pub lightning_public_key: PublicKey,
101 pub lightning_alias: String,
102 pub lightning_network: Network,
103}
104
105#[async_trait]
109pub trait ILnRpcClient: Debug + Send + Sync {
110 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
112
113 async fn routehints(
118 &self,
119 num_route_hints: usize,
120 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
121
122 async fn pay(
144 &self,
145 invoice: Bolt11Invoice,
146 max_delay: u64,
147 max_fee: Amount,
148 ) -> Result<PayInvoiceResponse, LightningRpcError> {
149 self.pay_private(
150 PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
151 failure_reason: "Invoice has no amount".to_string(),
152 })?,
153 max_delay,
154 max_fee,
155 )
156 .await
157 }
158
159 async fn pay_private(
166 &self,
167 _invoice: PrunedInvoice,
168 _max_delay: u64,
169 _max_fee: Amount,
170 ) -> Result<PayInvoiceResponse, LightningRpcError> {
171 Err(LightningRpcError::FailedPayment {
172 failure_reason: "Private payments not supported".to_string(),
173 })
174 }
175
176 fn supports_private_payments(&self) -> bool {
180 false
181 }
182
183 async fn outbound_payment_exists(
194 &self,
195 payment_hash: sha256::Hash,
196 ) -> Result<bool, LightningRpcError>;
197
198 async fn route_htlcs<'a>(
209 self: Box<Self>,
210 task_group: &TaskGroup,
211 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
212
213 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
223
224 async fn create_invoice(
229 &self,
230 create_invoice_request: CreateInvoiceRequest,
231 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
232
233 async fn get_ln_onchain_address(
236 &self,
237 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
238
239 async fn send_onchain(
242 &self,
243 payload: SendOnchainRequest,
244 ) -> Result<SendOnchainResponse, LightningRpcError>;
245
246 async fn open_channel(
248 &self,
249 payload: OpenChannelRequest,
250 ) -> Result<OpenChannelResponse, LightningRpcError>;
251
252 async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError>;
254
255 async fn close_channels_with_peer(
257 &self,
258 payload: CloseChannelsWithPeerRequest,
259 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
260
261 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
263
264 async fn set_channel_fees(
268 &self,
269 payload: SetChannelFeesRequest,
270 ) -> Result<(), LightningRpcError>;
271
272 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
275
276 async fn get_invoice(
277 &self,
278 get_invoice_request: GetInvoiceRequest,
279 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
280
281 async fn list_transactions(
282 &self,
283 start_secs: u64,
284 end_secs: u64,
285 ) -> Result<ListTransactionsResponse, LightningRpcError>;
286
287 fn create_offer(
288 &self,
289 amount: Option<Amount>,
290 description: Option<String>,
291 expiry_secs: Option<u32>,
292 quantity: Option<u64>,
293 ) -> Result<String, LightningRpcError>;
294
295 async fn pay_offer(
296 &self,
297 offer: String,
298 quantity: Option<u64>,
299 amount: Option<Amount>,
300 payer_note: Option<String>,
301 ) -> Result<Preimage, LightningRpcError>;
302
303 fn sync_wallet(&self) -> Result<(), LightningRpcError>;
304}
305
306impl dyn ILnRpcClient {
307 pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
311 if num_route_hints == 0 {
312 return vec![];
313 }
314
315 let route_hints =
316 self.routehints(num_route_hints as usize)
317 .await
318 .unwrap_or(GetRouteHintsResponse {
319 route_hints: Vec::new(),
320 });
321 route_hints.route_hints
322 }
323
324 pub async fn parsed_node_info(&self) -> LightningInfo {
327 if let Ok(info) = self.info().await
328 && let Ok(network) =
329 Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
330 failure_reason: format!("Invalid network {}: {e}", info.network),
331 })
332 {
333 return LightningInfo::Connected {
334 public_key: info.pub_key,
335 alias: info.alias,
336 network,
337 block_height: info.block_height as u64,
338 synced_to_chain: info.synced_to_chain,
339 };
340 }
341
342 LightningInfo::NotConnected
343 }
344
345 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
347 if is_env_var_set(FM_IN_DEVIMINT_ENV) {
351 self.sync_wallet()?;
352 }
353
354 retry(
356 "Wait for chain sync",
357 backoff_util::background_backoff(),
358 || async {
359 let info = self.info().await?;
360 let block_height = info.block_height;
361 if info.synced_to_chain {
362 Ok(())
363 } else {
364 warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
365 Err(anyhow::anyhow!("Not synced yet"))
366 }
367 },
368 )
369 .await
370 .map_err(|e| LightningRpcError::FailedToSyncToChain {
371 failure_reason: format!("Failed to sync to chain: {e:?}"),
372 })?;
373
374 info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
375 Ok(())
376 }
377}
378
379#[derive(Debug, Serialize, Deserialize, Clone)]
380pub struct GetNodeInfoResponse {
381 pub pub_key: PublicKey,
382 pub alias: String,
383 pub network: String,
384 pub block_height: u32,
385 pub synced_to_chain: bool,
386}
387
388pub const NO_INCOMING_CIRCUIT: (u64, u64) = (0, 0);
399
400#[derive(Debug, Serialize, Deserialize, Clone)]
401pub struct InterceptPaymentRequest {
402 pub payment_hash: sha256::Hash,
403 pub amount_msat: u64,
408 pub incoming_amount_msat: u64,
411 pub expiry: u32,
412 pub incoming_chan_id: u64,
413 pub short_channel_id: Option<u64>,
414 pub htlc_id: u64,
415}
416
417#[derive(Debug, Serialize, Deserialize, Clone)]
418pub struct InterceptPaymentResponse {
419 pub incoming_chan_id: u64,
420 pub htlc_id: u64,
421 pub payment_hash: sha256::Hash,
422 pub action: PaymentAction,
423}
424
425impl InterceptPaymentResponse {
426 pub fn incoming_circuit(&self) -> Option<(u64, u64)> {
435 let circuit = (self.incoming_chan_id, self.htlc_id);
436 (circuit != NO_INCOMING_CIRCUIT).then_some(circuit)
437 }
438}
439
440#[derive(Debug, Serialize, Deserialize, Clone)]
441pub enum PaymentAction {
442 Settle(Preimage),
443 Cancel,
444 Forward,
445}
446
447#[derive(Debug, Serialize, Deserialize, Clone)]
448pub struct GetRouteHintsResponse {
449 pub route_hints: Vec<RouteHint>,
450}
451
452#[derive(Debug, Serialize, Deserialize, Clone)]
453pub struct PayInvoiceResponse {
454 pub preimage: Preimage,
455}
456
457#[derive(Debug, Serialize, Deserialize, Clone)]
458pub struct CreateInvoiceRequest {
459 pub payment_hash: Option<sha256::Hash>,
460 pub amount_msat: u64,
461 pub expiry_secs: u32,
462 pub description: Option<InvoiceDescription>,
463}
464
465#[derive(Debug, Serialize, Deserialize, Clone)]
466pub enum InvoiceDescription {
467 Direct(String),
468 Hash(sha256::Hash),
469}
470
471#[derive(Debug, Serialize, Deserialize, Clone)]
472pub struct CreateInvoiceResponse {
473 pub invoice: String,
474}
475
476#[derive(Debug, Serialize, Deserialize, Clone)]
477pub struct GetLnOnchainAddressResponse {
478 pub address: String,
479}
480
481#[derive(Debug, Serialize, Deserialize, Clone)]
482pub struct SendOnchainResponse {
483 pub txid: String,
484}
485
486#[derive(Debug, Serialize, Deserialize, Clone)]
487pub struct OpenChannelResponse {
488 pub funding_txid: String,
489}
490
491#[derive(Debug, Serialize, Deserialize, Clone)]
492pub struct ListChannelsResponse {
493 pub channels: Vec<ChannelInfo>,
494}
495
496#[derive(Debug, Serialize, Deserialize, Clone)]
497pub struct GetBalancesResponse {
498 pub onchain_balance_sats: u64,
499 pub lightning_balance_msats: u64,
500 pub inbound_lightning_liquidity_msats: u64,
501}
502
503pub struct LnRpcTracked {
515 inner: Arc<dyn ILnRpcClient>,
516 name: &'static str,
517}
518
519impl std::fmt::Debug for LnRpcTracked {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 f.debug_struct("LnRpcTracked")
522 .field("name", &self.name)
523 .field("inner", &self.inner)
524 .finish()
525 }
526}
527
528impl LnRpcTracked {
529 #[allow(clippy::new_ret_no_self)]
534 pub fn new(inner: Arc<dyn ILnRpcClient>, name: &'static str) -> Arc<dyn ILnRpcClient> {
535 Arc::new(Self { inner, name })
536 }
537
538 fn record_call<T, E>(&self, method: &str, result: &Result<T, E>) {
539 let result_label = if result.is_ok() { "success" } else { "error" };
540 metrics::LN_RPC_REQUESTS_TOTAL
541 .with_label_values(&[method, self.name, result_label])
542 .inc();
543 }
544}
545
546macro_rules! tracked_call {
547 ($self:ident, $method:expr, $call:expr) => {{
548 trace!(
549 target: LOG_LIGHTNING,
550 method = $method,
551 name = $self.name,
552 "starting lightning rpc"
553 );
554 let start = now();
555 let timer = metrics::LN_RPC_DURATION_SECONDS
556 .with_label_values(&[$method, $self.name])
557 .start_timer_ext();
558 let result = $call;
559 timer.observe_duration();
560 $self.record_call($method, &result);
561 let duration_ms = now()
562 .duration_since(start)
563 .unwrap_or_default()
564 .as_secs_f64()
565 * 1000.0;
566 trace!(
567 target: LOG_LIGHTNING,
568 method = $method,
569 name = $self.name,
570 duration_ms,
571 error = %result.fmt_compact_result(),
572 "completed lightning rpc"
573 );
574 result
575 }};
576}
577
578#[async_trait]
579impl ILnRpcClient for LnRpcTracked {
580 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
581 tracked_call!(self, "info", self.inner.info().await)
582 }
583
584 async fn routehints(
585 &self,
586 num_route_hints: usize,
587 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
588 tracked_call!(
589 self,
590 "routehints",
591 self.inner.routehints(num_route_hints).await
592 )
593 }
594
595 async fn pay(
596 &self,
597 invoice: Bolt11Invoice,
598 max_delay: u64,
599 max_fee: Amount,
600 ) -> Result<PayInvoiceResponse, LightningRpcError> {
601 tracked_call!(
602 self,
603 "pay",
604 self.inner.pay(invoice, max_delay, max_fee).await
605 )
606 }
607
608 async fn pay_private(
609 &self,
610 invoice: PrunedInvoice,
611 max_delay: u64,
612 max_fee: Amount,
613 ) -> Result<PayInvoiceResponse, LightningRpcError> {
614 tracked_call!(
615 self,
616 "pay_private",
617 self.inner.pay_private(invoice, max_delay, max_fee).await
618 )
619 }
620
621 fn supports_private_payments(&self) -> bool {
622 self.inner.supports_private_payments()
623 }
624
625 async fn outbound_payment_exists(
626 &self,
627 payment_hash: sha256::Hash,
628 ) -> Result<bool, LightningRpcError> {
629 tracked_call!(
630 self,
631 "outbound_payment_exists",
632 self.inner.outbound_payment_exists(payment_hash).await
633 )
634 }
635
636 async fn route_htlcs<'a>(
637 self: Box<Self>,
638 _task_group: &TaskGroup,
639 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
640 panic!(
644 "route_htlcs should not be called on LnRpcTracked. \
645 Wrap the Arc returned from route_htlcs instead."
646 );
647 }
648
649 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
650 tracked_call!(self, "complete_htlc", self.inner.complete_htlc(htlc).await)
651 }
652
653 async fn create_invoice(
654 &self,
655 create_invoice_request: CreateInvoiceRequest,
656 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
657 tracked_call!(
658 self,
659 "create_invoice",
660 self.inner.create_invoice(create_invoice_request).await
661 )
662 }
663
664 async fn get_ln_onchain_address(
665 &self,
666 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
667 tracked_call!(
668 self,
669 "get_ln_onchain_address",
670 self.inner.get_ln_onchain_address().await
671 )
672 }
673
674 async fn send_onchain(
675 &self,
676 payload: SendOnchainRequest,
677 ) -> Result<SendOnchainResponse, LightningRpcError> {
678 tracked_call!(self, "send_onchain", self.inner.send_onchain(payload).await)
679 }
680
681 async fn open_channel(
682 &self,
683 payload: OpenChannelRequest,
684 ) -> Result<OpenChannelResponse, LightningRpcError> {
685 tracked_call!(self, "open_channel", self.inner.open_channel(payload).await)
686 }
687
688 async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
689 tracked_call!(self, "connect_peer", self.inner.connect_peer(payload).await)
690 }
691
692 async fn close_channels_with_peer(
693 &self,
694 payload: CloseChannelsWithPeerRequest,
695 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
696 tracked_call!(
697 self,
698 "close_channels_with_peer",
699 self.inner.close_channels_with_peer(payload).await
700 )
701 }
702
703 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
704 tracked_call!(self, "list_channels", self.inner.list_channels().await)
705 }
706
707 async fn set_channel_fees(
708 &self,
709 payload: SetChannelFeesRequest,
710 ) -> Result<(), LightningRpcError> {
711 tracked_call!(
712 self,
713 "set_channel_fees",
714 self.inner.set_channel_fees(payload).await
715 )
716 }
717
718 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
719 tracked_call!(self, "get_balances", self.inner.get_balances().await)
720 }
721
722 async fn get_invoice(
723 &self,
724 get_invoice_request: GetInvoiceRequest,
725 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
726 tracked_call!(
727 self,
728 "get_invoice",
729 self.inner.get_invoice(get_invoice_request).await
730 )
731 }
732
733 async fn list_transactions(
734 &self,
735 start_secs: u64,
736 end_secs: u64,
737 ) -> Result<ListTransactionsResponse, LightningRpcError> {
738 tracked_call!(
739 self,
740 "list_transactions",
741 self.inner.list_transactions(start_secs, end_secs).await
742 )
743 }
744
745 fn create_offer(
746 &self,
747 amount: Option<Amount>,
748 description: Option<String>,
749 expiry_secs: Option<u32>,
750 quantity: Option<u64>,
751 ) -> Result<String, LightningRpcError> {
752 tracked_call!(
753 self,
754 "create_offer",
755 self.inner
756 .create_offer(amount, description, expiry_secs, quantity)
757 )
758 }
759
760 async fn pay_offer(
761 &self,
762 offer: String,
763 quantity: Option<u64>,
764 amount: Option<Amount>,
765 payer_note: Option<String>,
766 ) -> Result<Preimage, LightningRpcError> {
767 tracked_call!(
768 self,
769 "pay_offer",
770 self.inner
771 .pay_offer(offer, quantity, amount, payer_note)
772 .await
773 )
774 }
775
776 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
777 tracked_call!(self, "sync_wallet", self.inner.sync_wallet())
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use bitcoin::hashes::{Hash as _, sha256};
784
785 use super::{InterceptPaymentResponse, NO_INCOMING_CIRCUIT, PaymentAction, Preimage};
786
787 fn response(incoming_chan_id: u64, htlc_id: u64) -> InterceptPaymentResponse {
788 InterceptPaymentResponse {
789 incoming_chan_id,
790 htlc_id,
791 payment_hash: sha256::Hash::all_zeros(),
792 action: PaymentAction::Settle(Preimage([0; 32])),
793 }
794 }
795
796 #[test]
797 fn payment_without_incoming_circuit_is_recognized() {
798 let (chan_id, htlc_id) = NO_INCOMING_CIRCUIT;
799 assert_eq!(response(chan_id, htlc_id).incoming_circuit(), None);
800 }
801
802 #[test]
803 fn intercepted_forward_keeps_its_circuit() {
804 assert_eq!(response(101, 0).incoming_circuit(), Some((101, 0)));
808 assert_eq!(response(101, 7).incoming_circuit(), Some((101, 7)));
809 }
810}