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, GetInvoiceRequest,
21 GetInvoiceResponse, LightningInfo, ListTransactionsResponse, OpenChannelRequest,
22 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}
91
92#[derive(Clone, Debug)]
94pub struct LightningContext {
95 pub lnrpc: Arc<dyn ILnRpcClient>,
96 pub lightning_public_key: PublicKey,
97 pub lightning_alias: String,
98 pub lightning_network: Network,
99}
100
101#[async_trait]
105pub trait ILnRpcClient: Debug + Send + Sync {
106 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
108
109 async fn routehints(
114 &self,
115 num_route_hints: usize,
116 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
117
118 async fn pay(
135 &self,
136 invoice: Bolt11Invoice,
137 max_delay: u64,
138 max_fee: Amount,
139 ) -> Result<PayInvoiceResponse, LightningRpcError> {
140 self.pay_private(
141 PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
142 failure_reason: "Invoice has no amount".to_string(),
143 })?,
144 max_delay,
145 max_fee,
146 )
147 .await
148 }
149
150 async fn pay_private(
160 &self,
161 _invoice: PrunedInvoice,
162 _max_delay: u64,
163 _max_fee: Amount,
164 ) -> Result<PayInvoiceResponse, LightningRpcError> {
165 Err(LightningRpcError::FailedPayment {
166 failure_reason: "Private payments not supported".to_string(),
167 })
168 }
169
170 fn supports_private_payments(&self) -> bool {
174 false
175 }
176
177 async fn route_htlcs<'a>(
188 self: Box<Self>,
189 task_group: &TaskGroup,
190 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
191
192 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
196
197 async fn create_invoice(
202 &self,
203 create_invoice_request: CreateInvoiceRequest,
204 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
205
206 async fn get_ln_onchain_address(
209 &self,
210 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
211
212 async fn send_onchain(
215 &self,
216 payload: SendOnchainRequest,
217 ) -> Result<SendOnchainResponse, LightningRpcError>;
218
219 async fn open_channel(
221 &self,
222 payload: OpenChannelRequest,
223 ) -> Result<OpenChannelResponse, LightningRpcError>;
224
225 async fn close_channels_with_peer(
227 &self,
228 payload: CloseChannelsWithPeerRequest,
229 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
230
231 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
233
234 async fn set_channel_fees(
238 &self,
239 payload: SetChannelFeesRequest,
240 ) -> Result<(), LightningRpcError>;
241
242 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
245
246 async fn get_invoice(
247 &self,
248 get_invoice_request: GetInvoiceRequest,
249 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
250
251 async fn list_transactions(
252 &self,
253 start_secs: u64,
254 end_secs: u64,
255 ) -> Result<ListTransactionsResponse, LightningRpcError>;
256
257 fn create_offer(
258 &self,
259 amount: Option<Amount>,
260 description: Option<String>,
261 expiry_secs: Option<u32>,
262 quantity: Option<u64>,
263 ) -> Result<String, LightningRpcError>;
264
265 async fn pay_offer(
266 &self,
267 offer: String,
268 quantity: Option<u64>,
269 amount: Option<Amount>,
270 payer_note: Option<String>,
271 ) -> Result<Preimage, LightningRpcError>;
272
273 fn sync_wallet(&self) -> Result<(), LightningRpcError>;
274}
275
276impl dyn ILnRpcClient {
277 pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
281 if num_route_hints == 0 {
282 return vec![];
283 }
284
285 let route_hints =
286 self.routehints(num_route_hints as usize)
287 .await
288 .unwrap_or(GetRouteHintsResponse {
289 route_hints: Vec::new(),
290 });
291 route_hints.route_hints
292 }
293
294 pub async fn parsed_node_info(&self) -> LightningInfo {
297 if let Ok(info) = self.info().await
298 && let Ok(network) =
299 Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
300 failure_reason: format!("Invalid network {}: {e}", info.network),
301 })
302 {
303 return LightningInfo::Connected {
304 public_key: info.pub_key,
305 alias: info.alias,
306 network,
307 block_height: info.block_height as u64,
308 synced_to_chain: info.synced_to_chain,
309 };
310 }
311
312 LightningInfo::NotConnected
313 }
314
315 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
317 if is_env_var_set(FM_IN_DEVIMINT_ENV) {
321 self.sync_wallet()?;
322 }
323
324 retry(
326 "Wait for chain sync",
327 backoff_util::background_backoff(),
328 || async {
329 let info = self.info().await?;
330 let block_height = info.block_height;
331 if info.synced_to_chain {
332 Ok(())
333 } else {
334 warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
335 Err(anyhow::anyhow!("Not synced yet"))
336 }
337 },
338 )
339 .await
340 .map_err(|e| LightningRpcError::FailedToSyncToChain {
341 failure_reason: format!("Failed to sync to chain: {e:?}"),
342 })?;
343
344 info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
345 Ok(())
346 }
347}
348
349#[derive(Debug, Serialize, Deserialize, Clone)]
350pub struct GetNodeInfoResponse {
351 pub pub_key: PublicKey,
352 pub alias: String,
353 pub network: String,
354 pub block_height: u32,
355 pub synced_to_chain: bool,
356}
357
358#[derive(Debug, Serialize, Deserialize, Clone)]
359pub struct InterceptPaymentRequest {
360 pub payment_hash: sha256::Hash,
361 pub amount_msat: u64,
362 pub expiry: u32,
363 pub incoming_chan_id: u64,
364 pub short_channel_id: Option<u64>,
365 pub htlc_id: u64,
366}
367
368#[derive(Debug, Serialize, Deserialize, Clone)]
369pub struct InterceptPaymentResponse {
370 pub incoming_chan_id: u64,
371 pub htlc_id: u64,
372 pub payment_hash: sha256::Hash,
373 pub action: PaymentAction,
374}
375
376#[derive(Debug, Serialize, Deserialize, Clone)]
377pub enum PaymentAction {
378 Settle(Preimage),
379 Cancel,
380 Forward,
381}
382
383#[derive(Debug, Serialize, Deserialize, Clone)]
384pub struct GetRouteHintsResponse {
385 pub route_hints: Vec<RouteHint>,
386}
387
388#[derive(Debug, Serialize, Deserialize, Clone)]
389pub struct PayInvoiceResponse {
390 pub preimage: Preimage,
391}
392
393#[derive(Debug, Serialize, Deserialize, Clone)]
394pub struct CreateInvoiceRequest {
395 pub payment_hash: Option<sha256::Hash>,
396 pub amount_msat: u64,
397 pub expiry_secs: u32,
398 pub description: Option<InvoiceDescription>,
399}
400
401#[derive(Debug, Serialize, Deserialize, Clone)]
402pub enum InvoiceDescription {
403 Direct(String),
404 Hash(sha256::Hash),
405}
406
407#[derive(Debug, Serialize, Deserialize, Clone)]
408pub struct CreateInvoiceResponse {
409 pub invoice: String,
410}
411
412#[derive(Debug, Serialize, Deserialize, Clone)]
413pub struct GetLnOnchainAddressResponse {
414 pub address: String,
415}
416
417#[derive(Debug, Serialize, Deserialize, Clone)]
418pub struct SendOnchainResponse {
419 pub txid: String,
420}
421
422#[derive(Debug, Serialize, Deserialize, Clone)]
423pub struct OpenChannelResponse {
424 pub funding_txid: String,
425}
426
427#[derive(Debug, Serialize, Deserialize, Clone)]
428pub struct ListChannelsResponse {
429 pub channels: Vec<ChannelInfo>,
430}
431
432#[derive(Debug, Serialize, Deserialize, Clone)]
433pub struct GetBalancesResponse {
434 pub onchain_balance_sats: u64,
435 pub lightning_balance_msats: u64,
436 pub inbound_lightning_liquidity_msats: u64,
437}
438
439pub struct LnRpcTracked {
451 inner: Arc<dyn ILnRpcClient>,
452 name: &'static str,
453}
454
455impl std::fmt::Debug for LnRpcTracked {
456 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457 f.debug_struct("LnRpcTracked")
458 .field("name", &self.name)
459 .field("inner", &self.inner)
460 .finish()
461 }
462}
463
464impl LnRpcTracked {
465 #[allow(clippy::new_ret_no_self)]
470 pub fn new(inner: Arc<dyn ILnRpcClient>, name: &'static str) -> Arc<dyn ILnRpcClient> {
471 Arc::new(Self { inner, name })
472 }
473
474 fn record_call<T, E>(&self, method: &str, result: &Result<T, E>) {
475 let result_label = if result.is_ok() { "success" } else { "error" };
476 metrics::LN_RPC_REQUESTS_TOTAL
477 .with_label_values(&[method, self.name, result_label])
478 .inc();
479 }
480}
481
482macro_rules! tracked_call {
483 ($self:ident, $method:expr, $call:expr) => {{
484 trace!(
485 target: LOG_LIGHTNING,
486 method = $method,
487 name = $self.name,
488 "starting lightning rpc"
489 );
490 let start = now();
491 let timer = metrics::LN_RPC_DURATION_SECONDS
492 .with_label_values(&[$method, $self.name])
493 .start_timer_ext();
494 let result = $call;
495 timer.observe_duration();
496 $self.record_call($method, &result);
497 let duration_ms = now()
498 .duration_since(start)
499 .unwrap_or_default()
500 .as_secs_f64()
501 * 1000.0;
502 trace!(
503 target: LOG_LIGHTNING,
504 method = $method,
505 name = $self.name,
506 duration_ms,
507 error = %result.fmt_compact_result(),
508 "completed lightning rpc"
509 );
510 result
511 }};
512}
513
514#[async_trait]
515impl ILnRpcClient for LnRpcTracked {
516 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
517 tracked_call!(self, "info", self.inner.info().await)
518 }
519
520 async fn routehints(
521 &self,
522 num_route_hints: usize,
523 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
524 tracked_call!(
525 self,
526 "routehints",
527 self.inner.routehints(num_route_hints).await
528 )
529 }
530
531 async fn pay(
532 &self,
533 invoice: Bolt11Invoice,
534 max_delay: u64,
535 max_fee: Amount,
536 ) -> Result<PayInvoiceResponse, LightningRpcError> {
537 tracked_call!(
538 self,
539 "pay",
540 self.inner.pay(invoice, max_delay, max_fee).await
541 )
542 }
543
544 async fn pay_private(
545 &self,
546 invoice: PrunedInvoice,
547 max_delay: u64,
548 max_fee: Amount,
549 ) -> Result<PayInvoiceResponse, LightningRpcError> {
550 tracked_call!(
551 self,
552 "pay_private",
553 self.inner.pay_private(invoice, max_delay, max_fee).await
554 )
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 tracked_call!(self, "complete_htlc", self.inner.complete_htlc(htlc).await)
576 }
577
578 async fn create_invoice(
579 &self,
580 create_invoice_request: CreateInvoiceRequest,
581 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
582 tracked_call!(
583 self,
584 "create_invoice",
585 self.inner.create_invoice(create_invoice_request).await
586 )
587 }
588
589 async fn get_ln_onchain_address(
590 &self,
591 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
592 tracked_call!(
593 self,
594 "get_ln_onchain_address",
595 self.inner.get_ln_onchain_address().await
596 )
597 }
598
599 async fn send_onchain(
600 &self,
601 payload: SendOnchainRequest,
602 ) -> Result<SendOnchainResponse, LightningRpcError> {
603 tracked_call!(self, "send_onchain", self.inner.send_onchain(payload).await)
604 }
605
606 async fn open_channel(
607 &self,
608 payload: OpenChannelRequest,
609 ) -> Result<OpenChannelResponse, LightningRpcError> {
610 tracked_call!(self, "open_channel", self.inner.open_channel(payload).await)
611 }
612
613 async fn close_channels_with_peer(
614 &self,
615 payload: CloseChannelsWithPeerRequest,
616 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
617 tracked_call!(
618 self,
619 "close_channels_with_peer",
620 self.inner.close_channels_with_peer(payload).await
621 )
622 }
623
624 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
625 tracked_call!(self, "list_channels", self.inner.list_channels().await)
626 }
627
628 async fn set_channel_fees(
629 &self,
630 payload: SetChannelFeesRequest,
631 ) -> Result<(), LightningRpcError> {
632 tracked_call!(
633 self,
634 "set_channel_fees",
635 self.inner.set_channel_fees(payload).await
636 )
637 }
638
639 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
640 tracked_call!(self, "get_balances", self.inner.get_balances().await)
641 }
642
643 async fn get_invoice(
644 &self,
645 get_invoice_request: GetInvoiceRequest,
646 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
647 tracked_call!(
648 self,
649 "get_invoice",
650 self.inner.get_invoice(get_invoice_request).await
651 )
652 }
653
654 async fn list_transactions(
655 &self,
656 start_secs: u64,
657 end_secs: u64,
658 ) -> Result<ListTransactionsResponse, LightningRpcError> {
659 tracked_call!(
660 self,
661 "list_transactions",
662 self.inner.list_transactions(start_secs, end_secs).await
663 )
664 }
665
666 fn create_offer(
667 &self,
668 amount: Option<Amount>,
669 description: Option<String>,
670 expiry_secs: Option<u32>,
671 quantity: Option<u64>,
672 ) -> Result<String, LightningRpcError> {
673 tracked_call!(
674 self,
675 "create_offer",
676 self.inner
677 .create_offer(amount, description, expiry_secs, quantity)
678 )
679 }
680
681 async fn pay_offer(
682 &self,
683 offer: String,
684 quantity: Option<u64>,
685 amount: Option<Amount>,
686 payer_note: Option<String>,
687 ) -> Result<Preimage, LightningRpcError> {
688 tracked_call!(
689 self,
690 "pay_offer",
691 self.inner
692 .pay_offer(offer, quantity, amount, payer_note)
693 .await
694 )
695 }
696
697 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
698 tracked_call!(self, "sync_wallet", self.inner.sync_wallet())
699 }
700}