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(
139 &self,
140 invoice: Bolt11Invoice,
141 max_delay: u64,
142 max_fee: Amount,
143 ) -> Result<PayInvoiceResponse, LightningRpcError> {
144 self.pay_private(
145 PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
146 failure_reason: "Invoice has no amount".to_string(),
147 })?,
148 max_delay,
149 max_fee,
150 )
151 .await
152 }
153
154 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 connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError>;
228
229 async fn close_channels_with_peer(
231 &self,
232 payload: CloseChannelsWithPeerRequest,
233 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
234
235 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
237
238 async fn set_channel_fees(
242 &self,
243 payload: SetChannelFeesRequest,
244 ) -> Result<(), LightningRpcError>;
245
246 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
249
250 async fn get_invoice(
251 &self,
252 get_invoice_request: GetInvoiceRequest,
253 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
254
255 async fn list_transactions(
256 &self,
257 start_secs: u64,
258 end_secs: u64,
259 ) -> Result<ListTransactionsResponse, LightningRpcError>;
260
261 fn create_offer(
262 &self,
263 amount: Option<Amount>,
264 description: Option<String>,
265 expiry_secs: Option<u32>,
266 quantity: Option<u64>,
267 ) -> Result<String, LightningRpcError>;
268
269 async fn pay_offer(
270 &self,
271 offer: String,
272 quantity: Option<u64>,
273 amount: Option<Amount>,
274 payer_note: Option<String>,
275 ) -> Result<Preimage, LightningRpcError>;
276
277 fn sync_wallet(&self) -> Result<(), LightningRpcError>;
278}
279
280impl dyn ILnRpcClient {
281 pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
285 if num_route_hints == 0 {
286 return vec![];
287 }
288
289 let route_hints =
290 self.routehints(num_route_hints as usize)
291 .await
292 .unwrap_or(GetRouteHintsResponse {
293 route_hints: Vec::new(),
294 });
295 route_hints.route_hints
296 }
297
298 pub async fn parsed_node_info(&self) -> LightningInfo {
301 if let Ok(info) = self.info().await
302 && let Ok(network) =
303 Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
304 failure_reason: format!("Invalid network {}: {e}", info.network),
305 })
306 {
307 return LightningInfo::Connected {
308 public_key: info.pub_key,
309 alias: info.alias,
310 network,
311 block_height: info.block_height as u64,
312 synced_to_chain: info.synced_to_chain,
313 };
314 }
315
316 LightningInfo::NotConnected
317 }
318
319 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
321 if is_env_var_set(FM_IN_DEVIMINT_ENV) {
325 self.sync_wallet()?;
326 }
327
328 retry(
330 "Wait for chain sync",
331 backoff_util::background_backoff(),
332 || async {
333 let info = self.info().await?;
334 let block_height = info.block_height;
335 if info.synced_to_chain {
336 Ok(())
337 } else {
338 warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
339 Err(anyhow::anyhow!("Not synced yet"))
340 }
341 },
342 )
343 .await
344 .map_err(|e| LightningRpcError::FailedToSyncToChain {
345 failure_reason: format!("Failed to sync to chain: {e:?}"),
346 })?;
347
348 info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
349 Ok(())
350 }
351}
352
353#[derive(Debug, Serialize, Deserialize, Clone)]
354pub struct GetNodeInfoResponse {
355 pub pub_key: PublicKey,
356 pub alias: String,
357 pub network: String,
358 pub block_height: u32,
359 pub synced_to_chain: bool,
360}
361
362pub const NO_INCOMING_CIRCUIT: (u64, u64) = (0, 0);
373
374#[derive(Debug, Serialize, Deserialize, Clone)]
375pub struct InterceptPaymentRequest {
376 pub payment_hash: sha256::Hash,
377 pub amount_msat: u64,
378 pub expiry: u32,
379 pub incoming_chan_id: u64,
380 pub short_channel_id: Option<u64>,
381 pub htlc_id: u64,
382}
383
384#[derive(Debug, Serialize, Deserialize, Clone)]
385pub struct InterceptPaymentResponse {
386 pub incoming_chan_id: u64,
387 pub htlc_id: u64,
388 pub payment_hash: sha256::Hash,
389 pub action: PaymentAction,
390}
391
392impl InterceptPaymentResponse {
393 pub fn incoming_circuit(&self) -> Option<(u64, u64)> {
402 let circuit = (self.incoming_chan_id, self.htlc_id);
403 (circuit != NO_INCOMING_CIRCUIT).then_some(circuit)
404 }
405}
406
407#[derive(Debug, Serialize, Deserialize, Clone)]
408pub enum PaymentAction {
409 Settle(Preimage),
410 Cancel,
411 Forward,
412}
413
414#[derive(Debug, Serialize, Deserialize, Clone)]
415pub struct GetRouteHintsResponse {
416 pub route_hints: Vec<RouteHint>,
417}
418
419#[derive(Debug, Serialize, Deserialize, Clone)]
420pub struct PayInvoiceResponse {
421 pub preimage: Preimage,
422}
423
424#[derive(Debug, Serialize, Deserialize, Clone)]
425pub struct CreateInvoiceRequest {
426 pub payment_hash: Option<sha256::Hash>,
427 pub amount_msat: u64,
428 pub expiry_secs: u32,
429 pub description: Option<InvoiceDescription>,
430}
431
432#[derive(Debug, Serialize, Deserialize, Clone)]
433pub enum InvoiceDescription {
434 Direct(String),
435 Hash(sha256::Hash),
436}
437
438#[derive(Debug, Serialize, Deserialize, Clone)]
439pub struct CreateInvoiceResponse {
440 pub invoice: String,
441}
442
443#[derive(Debug, Serialize, Deserialize, Clone)]
444pub struct GetLnOnchainAddressResponse {
445 pub address: String,
446}
447
448#[derive(Debug, Serialize, Deserialize, Clone)]
449pub struct SendOnchainResponse {
450 pub txid: String,
451}
452
453#[derive(Debug, Serialize, Deserialize, Clone)]
454pub struct OpenChannelResponse {
455 pub funding_txid: String,
456}
457
458#[derive(Debug, Serialize, Deserialize, Clone)]
459pub struct ListChannelsResponse {
460 pub channels: Vec<ChannelInfo>,
461}
462
463#[derive(Debug, Serialize, Deserialize, Clone)]
464pub struct GetBalancesResponse {
465 pub onchain_balance_sats: u64,
466 pub lightning_balance_msats: u64,
467 pub inbound_lightning_liquidity_msats: u64,
468}
469
470pub struct LnRpcTracked {
482 inner: Arc<dyn ILnRpcClient>,
483 name: &'static str,
484}
485
486impl std::fmt::Debug for LnRpcTracked {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 f.debug_struct("LnRpcTracked")
489 .field("name", &self.name)
490 .field("inner", &self.inner)
491 .finish()
492 }
493}
494
495impl LnRpcTracked {
496 #[allow(clippy::new_ret_no_self)]
501 pub fn new(inner: Arc<dyn ILnRpcClient>, name: &'static str) -> Arc<dyn ILnRpcClient> {
502 Arc::new(Self { inner, name })
503 }
504
505 fn record_call<T, E>(&self, method: &str, result: &Result<T, E>) {
506 let result_label = if result.is_ok() { "success" } else { "error" };
507 metrics::LN_RPC_REQUESTS_TOTAL
508 .with_label_values(&[method, self.name, result_label])
509 .inc();
510 }
511}
512
513macro_rules! tracked_call {
514 ($self:ident, $method:expr, $call:expr) => {{
515 trace!(
516 target: LOG_LIGHTNING,
517 method = $method,
518 name = $self.name,
519 "starting lightning rpc"
520 );
521 let start = now();
522 let timer = metrics::LN_RPC_DURATION_SECONDS
523 .with_label_values(&[$method, $self.name])
524 .start_timer_ext();
525 let result = $call;
526 timer.observe_duration();
527 $self.record_call($method, &result);
528 let duration_ms = now()
529 .duration_since(start)
530 .unwrap_or_default()
531 .as_secs_f64()
532 * 1000.0;
533 trace!(
534 target: LOG_LIGHTNING,
535 method = $method,
536 name = $self.name,
537 duration_ms,
538 error = %result.fmt_compact_result(),
539 "completed lightning rpc"
540 );
541 result
542 }};
543}
544
545#[async_trait]
546impl ILnRpcClient for LnRpcTracked {
547 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
548 tracked_call!(self, "info", self.inner.info().await)
549 }
550
551 async fn routehints(
552 &self,
553 num_route_hints: usize,
554 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
555 tracked_call!(
556 self,
557 "routehints",
558 self.inner.routehints(num_route_hints).await
559 )
560 }
561
562 async fn pay(
563 &self,
564 invoice: Bolt11Invoice,
565 max_delay: u64,
566 max_fee: Amount,
567 ) -> Result<PayInvoiceResponse, LightningRpcError> {
568 tracked_call!(
569 self,
570 "pay",
571 self.inner.pay(invoice, max_delay, max_fee).await
572 )
573 }
574
575 async fn pay_private(
576 &self,
577 invoice: PrunedInvoice,
578 max_delay: u64,
579 max_fee: Amount,
580 ) -> Result<PayInvoiceResponse, LightningRpcError> {
581 tracked_call!(
582 self,
583 "pay_private",
584 self.inner.pay_private(invoice, max_delay, max_fee).await
585 )
586 }
587
588 fn supports_private_payments(&self) -> bool {
589 self.inner.supports_private_payments()
590 }
591
592 async fn route_htlcs<'a>(
593 self: Box<Self>,
594 _task_group: &TaskGroup,
595 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
596 panic!(
600 "route_htlcs should not be called on LnRpcTracked. \
601 Wrap the Arc returned from route_htlcs instead."
602 );
603 }
604
605 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
606 tracked_call!(self, "complete_htlc", self.inner.complete_htlc(htlc).await)
607 }
608
609 async fn create_invoice(
610 &self,
611 create_invoice_request: CreateInvoiceRequest,
612 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
613 tracked_call!(
614 self,
615 "create_invoice",
616 self.inner.create_invoice(create_invoice_request).await
617 )
618 }
619
620 async fn get_ln_onchain_address(
621 &self,
622 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
623 tracked_call!(
624 self,
625 "get_ln_onchain_address",
626 self.inner.get_ln_onchain_address().await
627 )
628 }
629
630 async fn send_onchain(
631 &self,
632 payload: SendOnchainRequest,
633 ) -> Result<SendOnchainResponse, LightningRpcError> {
634 tracked_call!(self, "send_onchain", self.inner.send_onchain(payload).await)
635 }
636
637 async fn open_channel(
638 &self,
639 payload: OpenChannelRequest,
640 ) -> Result<OpenChannelResponse, LightningRpcError> {
641 tracked_call!(self, "open_channel", self.inner.open_channel(payload).await)
642 }
643
644 async fn connect_peer(&self, payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
645 tracked_call!(self, "connect_peer", self.inner.connect_peer(payload).await)
646 }
647
648 async fn close_channels_with_peer(
649 &self,
650 payload: CloseChannelsWithPeerRequest,
651 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
652 tracked_call!(
653 self,
654 "close_channels_with_peer",
655 self.inner.close_channels_with_peer(payload).await
656 )
657 }
658
659 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
660 tracked_call!(self, "list_channels", self.inner.list_channels().await)
661 }
662
663 async fn set_channel_fees(
664 &self,
665 payload: SetChannelFeesRequest,
666 ) -> Result<(), LightningRpcError> {
667 tracked_call!(
668 self,
669 "set_channel_fees",
670 self.inner.set_channel_fees(payload).await
671 )
672 }
673
674 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
675 tracked_call!(self, "get_balances", self.inner.get_balances().await)
676 }
677
678 async fn get_invoice(
679 &self,
680 get_invoice_request: GetInvoiceRequest,
681 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
682 tracked_call!(
683 self,
684 "get_invoice",
685 self.inner.get_invoice(get_invoice_request).await
686 )
687 }
688
689 async fn list_transactions(
690 &self,
691 start_secs: u64,
692 end_secs: u64,
693 ) -> Result<ListTransactionsResponse, LightningRpcError> {
694 tracked_call!(
695 self,
696 "list_transactions",
697 self.inner.list_transactions(start_secs, end_secs).await
698 )
699 }
700
701 fn create_offer(
702 &self,
703 amount: Option<Amount>,
704 description: Option<String>,
705 expiry_secs: Option<u32>,
706 quantity: Option<u64>,
707 ) -> Result<String, LightningRpcError> {
708 tracked_call!(
709 self,
710 "create_offer",
711 self.inner
712 .create_offer(amount, description, expiry_secs, quantity)
713 )
714 }
715
716 async fn pay_offer(
717 &self,
718 offer: String,
719 quantity: Option<u64>,
720 amount: Option<Amount>,
721 payer_note: Option<String>,
722 ) -> Result<Preimage, LightningRpcError> {
723 tracked_call!(
724 self,
725 "pay_offer",
726 self.inner
727 .pay_offer(offer, quantity, amount, payer_note)
728 .await
729 )
730 }
731
732 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
733 tracked_call!(self, "sync_wallet", self.inner.sync_wallet())
734 }
735}
736
737#[cfg(test)]
738mod tests {
739 use bitcoin::hashes::{Hash as _, sha256};
740
741 use super::{InterceptPaymentResponse, NO_INCOMING_CIRCUIT, PaymentAction, Preimage};
742
743 fn response(incoming_chan_id: u64, htlc_id: u64) -> InterceptPaymentResponse {
744 InterceptPaymentResponse {
745 incoming_chan_id,
746 htlc_id,
747 payment_hash: sha256::Hash::all_zeros(),
748 action: PaymentAction::Settle(Preimage([0; 32])),
749 }
750 }
751
752 #[test]
753 fn payment_without_incoming_circuit_is_recognized() {
754 let (chan_id, htlc_id) = NO_INCOMING_CIRCUIT;
755 assert_eq!(response(chan_id, htlc_id).incoming_circuit(), None);
756 }
757
758 #[test]
759 fn intercepted_forward_keeps_its_circuit() {
760 assert_eq!(response(101, 0).incoming_circuit(), Some((101, 0)));
764 assert_eq!(response(101, 7).incoming_circuit(), Some((101, 7)));
765 }
766}