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::util::{backoff_util, retry};
18use fedimint_gateway_common::{
19 ChannelInfo, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, GetInvoiceRequest,
20 GetInvoiceResponse, LightningInfo, ListTransactionsResponse, OpenChannelRequest,
21 SendOnchainRequest,
22};
23use fedimint_ln_common::PrunedInvoice;
24pub use fedimint_ln_common::contracts::Preimage;
25use fedimint_ln_common::route_hints::RouteHint;
26use fedimint_logging::LOG_LIGHTNING;
27use fedimint_metrics::HistogramExt as _;
28use futures::future::BoxFuture;
29use futures::stream::BoxStream;
30use lightning_invoice::Bolt11Invoice;
31use serde::{Deserialize, Serialize};
32use thiserror::Error;
33use tracing::{info, warn};
34
35pub const MAX_LIGHTNING_RETRIES: u32 = 10;
36
37pub type RouteHtlcStream<'a> = BoxStream<'a, InterceptPaymentRequest>;
38
39pub type Lnv2HoldInvoiceFilter =
45 Arc<dyn Fn(sha256::Hash) -> BoxFuture<'static, bool> + Send + Sync + 'static>;
46
47#[derive(
48 Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
49)]
50pub enum LightningRpcError {
51 #[error("Failed to connect to Lightning node")]
52 FailedToConnect,
53 #[error("Failed to retrieve node info: {failure_reason}")]
54 FailedToGetNodeInfo { failure_reason: String },
55 #[error("Failed to retrieve route hints: {failure_reason}")]
56 FailedToGetRouteHints { failure_reason: String },
57 #[error("Payment failed: {failure_reason}")]
58 FailedPayment { failure_reason: String },
59 #[error("Failed to route HTLCs: {failure_reason}")]
60 FailedToRouteHtlcs { failure_reason: String },
61 #[error("Failed to complete HTLC: {failure_reason}")]
62 FailedToCompleteHtlc { failure_reason: String },
63 #[error("Failed to open channel: {failure_reason}")]
64 FailedToOpenChannel { failure_reason: String },
65 #[error("Failed to close channel: {failure_reason}")]
66 FailedToCloseChannelsWithPeer { failure_reason: String },
67 #[error("Failed to get Invoice: {failure_reason}")]
68 FailedToGetInvoice { failure_reason: String },
69 #[error("Failed to list transactions: {failure_reason}")]
70 FailedToListTransactions { failure_reason: String },
71 #[error("Failed to get funding address: {failure_reason}")]
72 FailedToGetLnOnchainAddress { failure_reason: String },
73 #[error("Failed to withdraw funds on-chain: {failure_reason}")]
74 FailedToWithdrawOnchain { failure_reason: String },
75 #[error("Failed to connect to peer: {failure_reason}")]
76 FailedToConnectToPeer { failure_reason: String },
77 #[error("Failed to list active channels: {failure_reason}")]
78 FailedToListChannels { failure_reason: String },
79 #[error("Failed to get balances: {failure_reason}")]
80 FailedToGetBalances { failure_reason: String },
81 #[error("Failed to sync to chain: {failure_reason}")]
82 FailedToSyncToChain { failure_reason: String },
83 #[error("Invalid metadata: {failure_reason}")]
84 InvalidMetadata { failure_reason: String },
85 #[error("Bolt12 Error: {failure_reason}")]
86 Bolt12Error { failure_reason: String },
87 #[error("HTLC completion cannot reach the requested outcome: {failure_reason}")]
90 HtlcCompletionRejected { failure_reason: String },
91}
92
93#[derive(Clone, Debug)]
95pub struct LightningContext {
96 pub lnrpc: Arc<dyn ILnRpcClient>,
97 pub lightning_public_key: PublicKey,
98 pub lightning_alias: String,
99 pub lightning_network: Network,
100}
101
102#[async_trait]
106pub trait ILnRpcClient: Debug + Send + Sync {
107 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
109
110 async fn routehints(
115 &self,
116 num_route_hints: usize,
117 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
118
119 async fn pay(
136 &self,
137 invoice: Bolt11Invoice,
138 max_delay: u64,
139 max_fee: Amount,
140 ) -> Result<PayInvoiceResponse, LightningRpcError> {
141 self.pay_private(
142 PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
143 failure_reason: "Invoice has no amount".to_string(),
144 })?,
145 max_delay,
146 max_fee,
147 )
148 .await
149 }
150
151 async fn pay_private(
161 &self,
162 _invoice: PrunedInvoice,
163 _max_delay: u64,
164 _max_fee: Amount,
165 ) -> Result<PayInvoiceResponse, LightningRpcError> {
166 Err(LightningRpcError::FailedPayment {
167 failure_reason: "Private payments not supported".to_string(),
168 })
169 }
170
171 fn supports_private_payments(&self) -> bool {
175 false
176 }
177
178 async fn route_htlcs<'a>(
189 self: Box<Self>,
190 task_group: &TaskGroup,
191 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
192
193 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
197
198 async fn create_invoice(
203 &self,
204 create_invoice_request: CreateInvoiceRequest,
205 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
206
207 async fn get_ln_onchain_address(
210 &self,
211 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
212
213 async fn send_onchain(
216 &self,
217 payload: SendOnchainRequest,
218 ) -> Result<SendOnchainResponse, LightningRpcError>;
219
220 async fn open_channel(
222 &self,
223 payload: OpenChannelRequest,
224 ) -> Result<OpenChannelResponse, LightningRpcError>;
225
226 async fn close_channels_with_peer(
228 &self,
229 payload: CloseChannelsWithPeerRequest,
230 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
231
232 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
234
235 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
238
239 async fn get_invoice(
240 &self,
241 get_invoice_request: GetInvoiceRequest,
242 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
243
244 async fn list_transactions(
245 &self,
246 start_secs: u64,
247 end_secs: u64,
248 ) -> Result<ListTransactionsResponse, LightningRpcError>;
249
250 fn create_offer(
251 &self,
252 amount: Option<Amount>,
253 description: Option<String>,
254 expiry_secs: Option<u32>,
255 quantity: Option<u64>,
256 ) -> Result<String, LightningRpcError>;
257
258 async fn pay_offer(
259 &self,
260 offer: String,
261 quantity: Option<u64>,
262 amount: Option<Amount>,
263 payer_note: Option<String>,
264 ) -> Result<Preimage, LightningRpcError>;
265
266 fn sync_wallet(&self) -> Result<(), LightningRpcError>;
267}
268
269impl dyn ILnRpcClient {
270 pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
274 if num_route_hints == 0 {
275 return vec![];
276 }
277
278 let route_hints =
279 self.routehints(num_route_hints as usize)
280 .await
281 .unwrap_or(GetRouteHintsResponse {
282 route_hints: Vec::new(),
283 });
284 route_hints.route_hints
285 }
286
287 pub async fn parsed_node_info(&self) -> LightningInfo {
290 if let Ok(info) = self.info().await
291 && let Ok(network) =
292 Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
293 failure_reason: format!("Invalid network {}: {e}", info.network),
294 })
295 {
296 return LightningInfo::Connected {
297 public_key: info.pub_key,
298 alias: info.alias,
299 network,
300 block_height: info.block_height as u64,
301 synced_to_chain: info.synced_to_chain,
302 };
303 }
304
305 LightningInfo::NotConnected
306 }
307
308 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
310 if is_env_var_set(FM_IN_DEVIMINT_ENV) {
314 self.sync_wallet()?;
315 }
316
317 retry(
319 "Wait for chain sync",
320 backoff_util::background_backoff(),
321 || async {
322 let info = self.info().await?;
323 let block_height = info.block_height;
324 if info.synced_to_chain {
325 Ok(())
326 } else {
327 warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
328 Err(anyhow::anyhow!("Not synced yet"))
329 }
330 },
331 )
332 .await
333 .map_err(|e| LightningRpcError::FailedToSyncToChain {
334 failure_reason: format!("Failed to sync to chain: {e:?}"),
335 })?;
336
337 info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
338 Ok(())
339 }
340}
341
342#[derive(Debug, Serialize, Deserialize, Clone)]
343pub struct GetNodeInfoResponse {
344 pub pub_key: PublicKey,
345 pub alias: String,
346 pub network: String,
347 pub block_height: u32,
348 pub synced_to_chain: bool,
349}
350
351pub const NO_INCOMING_CIRCUIT: (u64, u64) = (0, 0);
362
363#[derive(Debug, Serialize, Deserialize, Clone)]
364pub struct InterceptPaymentRequest {
365 pub payment_hash: sha256::Hash,
366 pub amount_msat: u64,
367 pub expiry: u32,
368 pub incoming_chan_id: u64,
369 pub short_channel_id: Option<u64>,
370 pub htlc_id: u64,
371}
372
373#[derive(Debug, Serialize, Deserialize, Clone)]
374pub struct InterceptPaymentResponse {
375 pub incoming_chan_id: u64,
376 pub htlc_id: u64,
377 pub payment_hash: sha256::Hash,
378 pub action: PaymentAction,
379}
380
381impl InterceptPaymentResponse {
382 pub fn incoming_circuit(&self) -> Option<(u64, u64)> {
391 let circuit = (self.incoming_chan_id, self.htlc_id);
392 (circuit != NO_INCOMING_CIRCUIT).then_some(circuit)
393 }
394}
395
396#[derive(Debug, Serialize, Deserialize, Clone)]
397pub enum PaymentAction {
398 Settle(Preimage),
399 Cancel,
400 Forward,
401}
402
403#[derive(Debug, Serialize, Deserialize, Clone)]
404pub struct GetRouteHintsResponse {
405 pub route_hints: Vec<RouteHint>,
406}
407
408#[derive(Debug, Serialize, Deserialize, Clone)]
409pub struct PayInvoiceResponse {
410 pub preimage: Preimage,
411}
412
413#[derive(Debug, Serialize, Deserialize, Clone)]
414pub struct CreateInvoiceRequest {
415 pub payment_hash: Option<sha256::Hash>,
416 pub amount_msat: u64,
417 pub expiry_secs: u32,
418 pub description: Option<InvoiceDescription>,
419}
420
421#[derive(Debug, Serialize, Deserialize, Clone)]
422pub enum InvoiceDescription {
423 Direct(String),
424 Hash(sha256::Hash),
425}
426
427#[derive(Debug, Serialize, Deserialize, Clone)]
428pub struct CreateInvoiceResponse {
429 pub invoice: String,
430}
431
432#[derive(Debug, Serialize, Deserialize, Clone)]
433pub struct GetLnOnchainAddressResponse {
434 pub address: String,
435}
436
437#[derive(Debug, Serialize, Deserialize, Clone)]
438pub struct SendOnchainResponse {
439 pub txid: String,
440}
441
442#[derive(Debug, Serialize, Deserialize, Clone)]
443pub struct OpenChannelResponse {
444 pub funding_txid: String,
445}
446
447#[derive(Debug, Serialize, Deserialize, Clone)]
448pub struct ListChannelsResponse {
449 pub channels: Vec<ChannelInfo>,
450}
451
452#[derive(Debug, Serialize, Deserialize, Clone)]
453pub struct GetBalancesResponse {
454 pub onchain_balance_sats: u64,
455 pub lightning_balance_msats: u64,
456 pub inbound_lightning_liquidity_msats: u64,
457}
458
459pub struct LnRpcTracked {
471 inner: Arc<dyn ILnRpcClient>,
472 name: &'static str,
473}
474
475impl std::fmt::Debug for LnRpcTracked {
476 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477 f.debug_struct("LnRpcTracked")
478 .field("name", &self.name)
479 .field("inner", &self.inner)
480 .finish()
481 }
482}
483
484impl LnRpcTracked {
485 #[allow(clippy::new_ret_no_self)]
490 pub fn new(inner: Arc<dyn ILnRpcClient>, name: &'static str) -> Arc<dyn ILnRpcClient> {
491 Arc::new(Self { inner, name })
492 }
493
494 fn record_call<T, E>(&self, method: &str, result: &Result<T, E>) {
495 let result_label = if result.is_ok() { "success" } else { "error" };
496 metrics::LN_RPC_REQUESTS_TOTAL
497 .with_label_values(&[method, self.name, result_label])
498 .inc();
499 }
500}
501
502#[async_trait]
503impl ILnRpcClient for LnRpcTracked {
504 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
505 let timer = metrics::LN_RPC_DURATION_SECONDS
506 .with_label_values(&["info", self.name])
507 .start_timer_ext();
508 let result = self.inner.info().await;
509 timer.observe_duration();
510 self.record_call("info", &result);
511 result
512 }
513
514 async fn routehints(
515 &self,
516 num_route_hints: usize,
517 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
518 let timer = metrics::LN_RPC_DURATION_SECONDS
519 .with_label_values(&["routehints", self.name])
520 .start_timer_ext();
521 let result = self.inner.routehints(num_route_hints).await;
522 timer.observe_duration();
523 self.record_call("routehints", &result);
524 result
525 }
526
527 async fn pay(
528 &self,
529 invoice: Bolt11Invoice,
530 max_delay: u64,
531 max_fee: Amount,
532 ) -> Result<PayInvoiceResponse, LightningRpcError> {
533 let timer = metrics::LN_RPC_DURATION_SECONDS
534 .with_label_values(&["pay", self.name])
535 .start_timer_ext();
536 let result = self.inner.pay(invoice, max_delay, max_fee).await;
537 timer.observe_duration();
538 self.record_call("pay", &result);
539 result
540 }
541
542 async fn pay_private(
543 &self,
544 invoice: PrunedInvoice,
545 max_delay: u64,
546 max_fee: Amount,
547 ) -> Result<PayInvoiceResponse, LightningRpcError> {
548 let timer = metrics::LN_RPC_DURATION_SECONDS
549 .with_label_values(&["pay_private", self.name])
550 .start_timer_ext();
551 let result = self.inner.pay_private(invoice, max_delay, max_fee).await;
552 timer.observe_duration();
553 self.record_call("pay_private", &result);
554 result
555 }
556
557 fn supports_private_payments(&self) -> bool {
558 self.inner.supports_private_payments()
559 }
560
561 async fn route_htlcs<'a>(
562 self: Box<Self>,
563 _task_group: &TaskGroup,
564 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
565 panic!(
569 "route_htlcs should not be called on LnRpcTracked. \
570 Wrap the Arc returned from route_htlcs instead."
571 );
572 }
573
574 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
575 let timer = metrics::LN_RPC_DURATION_SECONDS
576 .with_label_values(&["complete_htlc", self.name])
577 .start_timer_ext();
578 let result = self.inner.complete_htlc(htlc).await;
579 timer.observe_duration();
580 self.record_call("complete_htlc", &result);
581 result
582 }
583
584 async fn create_invoice(
585 &self,
586 create_invoice_request: CreateInvoiceRequest,
587 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
588 let timer = metrics::LN_RPC_DURATION_SECONDS
589 .with_label_values(&["create_invoice", self.name])
590 .start_timer_ext();
591 let result = self.inner.create_invoice(create_invoice_request).await;
592 timer.observe_duration();
593 self.record_call("create_invoice", &result);
594 result
595 }
596
597 async fn get_ln_onchain_address(
598 &self,
599 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
600 let timer = metrics::LN_RPC_DURATION_SECONDS
601 .with_label_values(&["get_ln_onchain_address", self.name])
602 .start_timer_ext();
603 let result = self.inner.get_ln_onchain_address().await;
604 timer.observe_duration();
605 self.record_call("get_ln_onchain_address", &result);
606 result
607 }
608
609 async fn send_onchain(
610 &self,
611 payload: SendOnchainRequest,
612 ) -> Result<SendOnchainResponse, LightningRpcError> {
613 let timer = metrics::LN_RPC_DURATION_SECONDS
614 .with_label_values(&["send_onchain", self.name])
615 .start_timer_ext();
616 let result = self.inner.send_onchain(payload).await;
617 timer.observe_duration();
618 self.record_call("send_onchain", &result);
619 result
620 }
621
622 async fn open_channel(
623 &self,
624 payload: OpenChannelRequest,
625 ) -> Result<OpenChannelResponse, LightningRpcError> {
626 let timer = metrics::LN_RPC_DURATION_SECONDS
627 .with_label_values(&["open_channel", self.name])
628 .start_timer_ext();
629 let result = self.inner.open_channel(payload).await;
630 timer.observe_duration();
631 self.record_call("open_channel", &result);
632 result
633 }
634
635 async fn close_channels_with_peer(
636 &self,
637 payload: CloseChannelsWithPeerRequest,
638 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
639 let timer = metrics::LN_RPC_DURATION_SECONDS
640 .with_label_values(&["close_channels_with_peer", self.name])
641 .start_timer_ext();
642 let result = self.inner.close_channels_with_peer(payload).await;
643 timer.observe_duration();
644 self.record_call("close_channels_with_peer", &result);
645 result
646 }
647
648 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
649 let timer = metrics::LN_RPC_DURATION_SECONDS
650 .with_label_values(&["list_channels", self.name])
651 .start_timer_ext();
652 let result = self.inner.list_channels().await;
653 timer.observe_duration();
654 self.record_call("list_channels", &result);
655 result
656 }
657
658 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
659 let timer = metrics::LN_RPC_DURATION_SECONDS
660 .with_label_values(&["get_balances", self.name])
661 .start_timer_ext();
662 let result = self.inner.get_balances().await;
663 timer.observe_duration();
664 self.record_call("get_balances", &result);
665 result
666 }
667
668 async fn get_invoice(
669 &self,
670 get_invoice_request: GetInvoiceRequest,
671 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
672 let timer = metrics::LN_RPC_DURATION_SECONDS
673 .with_label_values(&["get_invoice", self.name])
674 .start_timer_ext();
675 let result = self.inner.get_invoice(get_invoice_request).await;
676 timer.observe_duration();
677 self.record_call("get_invoice", &result);
678 result
679 }
680
681 async fn list_transactions(
682 &self,
683 start_secs: u64,
684 end_secs: u64,
685 ) -> Result<ListTransactionsResponse, LightningRpcError> {
686 let timer = metrics::LN_RPC_DURATION_SECONDS
687 .with_label_values(&["list_transactions", self.name])
688 .start_timer_ext();
689 let result = self.inner.list_transactions(start_secs, end_secs).await;
690 timer.observe_duration();
691 self.record_call("list_transactions", &result);
692 result
693 }
694
695 fn create_offer(
696 &self,
697 amount: Option<Amount>,
698 description: Option<String>,
699 expiry_secs: Option<u32>,
700 quantity: Option<u64>,
701 ) -> Result<String, LightningRpcError> {
702 let timer = metrics::LN_RPC_DURATION_SECONDS
703 .with_label_values(&["create_offer", self.name])
704 .start_timer_ext();
705 let result = self
706 .inner
707 .create_offer(amount, description, expiry_secs, quantity);
708 timer.observe_duration();
709 self.record_call("create_offer", &result);
710 result
711 }
712
713 async fn pay_offer(
714 &self,
715 offer: String,
716 quantity: Option<u64>,
717 amount: Option<Amount>,
718 payer_note: Option<String>,
719 ) -> Result<Preimage, LightningRpcError> {
720 let timer = metrics::LN_RPC_DURATION_SECONDS
721 .with_label_values(&["pay_offer", self.name])
722 .start_timer_ext();
723 let result = self
724 .inner
725 .pay_offer(offer, quantity, amount, payer_note)
726 .await;
727 timer.observe_duration();
728 self.record_call("pay_offer", &result);
729 result
730 }
731
732 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
733 let timer = metrics::LN_RPC_DURATION_SECONDS
734 .with_label_values(&["sync_wallet", self.name])
735 .start_timer_ext();
736 let result = self.inner.sync_wallet();
737 timer.observe_duration();
738 self.record_call("sync_wallet", &result);
739 result
740 }
741}
742
743#[cfg(test)]
744mod tests {
745 use bitcoin::hashes::{Hash as _, sha256};
746
747 use super::{InterceptPaymentResponse, NO_INCOMING_CIRCUIT, PaymentAction, Preimage};
748
749 fn response(incoming_chan_id: u64, htlc_id: u64) -> InterceptPaymentResponse {
750 InterceptPaymentResponse {
751 incoming_chan_id,
752 htlc_id,
753 payment_hash: sha256::Hash::all_zeros(),
754 action: PaymentAction::Settle(Preimage([0; 32])),
755 }
756 }
757
758 #[test]
759 fn payment_without_incoming_circuit_is_recognized() {
760 let (chan_id, htlc_id) = NO_INCOMING_CIRCUIT;
761 assert_eq!(response(chan_id, htlc_id).incoming_circuit(), None);
762 }
763
764 #[test]
765 fn intercepted_forward_keeps_its_circuit() {
766 assert_eq!(response(101, 0).incoming_circuit(), Some((101, 0)));
770 assert_eq!(response(101, 7).incoming_circuit(), Some((101, 7)));
771 }
772}