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::stream::BoxStream;
29use lightning_invoice::Bolt11Invoice;
30use serde::{Deserialize, Serialize};
31use thiserror::Error;
32use tracing::{info, warn};
33
34pub const MAX_LIGHTNING_RETRIES: u32 = 10;
35
36pub type RouteHtlcStream<'a> = BoxStream<'a, InterceptPaymentRequest>;
37
38#[derive(
39 Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
40)]
41pub enum LightningRpcError {
42 #[error("Failed to connect to Lightning node")]
43 FailedToConnect,
44 #[error("Failed to retrieve node info: {failure_reason}")]
45 FailedToGetNodeInfo { failure_reason: String },
46 #[error("Failed to retrieve route hints: {failure_reason}")]
47 FailedToGetRouteHints { failure_reason: String },
48 #[error("Payment failed: {failure_reason}")]
49 FailedPayment { failure_reason: String },
50 #[error("Failed to route HTLCs: {failure_reason}")]
51 FailedToRouteHtlcs { failure_reason: String },
52 #[error("Failed to complete HTLC: {failure_reason}")]
53 FailedToCompleteHtlc { failure_reason: String },
54 #[error("Failed to open channel: {failure_reason}")]
55 FailedToOpenChannel { failure_reason: String },
56 #[error("Failed to close channel: {failure_reason}")]
57 FailedToCloseChannelsWithPeer { failure_reason: String },
58 #[error("Failed to get Invoice: {failure_reason}")]
59 FailedToGetInvoice { failure_reason: String },
60 #[error("Failed to list transactions: {failure_reason}")]
61 FailedToListTransactions { failure_reason: String },
62 #[error("Failed to get funding address: {failure_reason}")]
63 FailedToGetLnOnchainAddress { failure_reason: String },
64 #[error("Failed to withdraw funds on-chain: {failure_reason}")]
65 FailedToWithdrawOnchain { failure_reason: String },
66 #[error("Failed to connect to peer: {failure_reason}")]
67 FailedToConnectToPeer { failure_reason: String },
68 #[error("Failed to list active channels: {failure_reason}")]
69 FailedToListChannels { failure_reason: String },
70 #[error("Failed to get balances: {failure_reason}")]
71 FailedToGetBalances { failure_reason: String },
72 #[error("Failed to sync to chain: {failure_reason}")]
73 FailedToSyncToChain { failure_reason: String },
74 #[error("Invalid metadata: {failure_reason}")]
75 InvalidMetadata { failure_reason: String },
76 #[error("Bolt12 Error: {failure_reason}")]
77 Bolt12Error { failure_reason: String },
78}
79
80#[derive(Clone, Debug)]
82pub struct LightningContext {
83 pub lnrpc: Arc<dyn ILnRpcClient>,
84 pub lightning_public_key: PublicKey,
85 pub lightning_alias: String,
86 pub lightning_network: Network,
87}
88
89#[async_trait]
93pub trait ILnRpcClient: Debug + Send + Sync {
94 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
96
97 async fn routehints(
102 &self,
103 num_route_hints: usize,
104 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
105
106 async fn pay(
123 &self,
124 invoice: Bolt11Invoice,
125 max_delay: u64,
126 max_fee: Amount,
127 ) -> Result<PayInvoiceResponse, LightningRpcError> {
128 self.pay_private(
129 PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
130 failure_reason: "Invoice has no amount".to_string(),
131 })?,
132 max_delay,
133 max_fee,
134 )
135 .await
136 }
137
138 async fn pay_private(
148 &self,
149 _invoice: PrunedInvoice,
150 _max_delay: u64,
151 _max_fee: Amount,
152 ) -> Result<PayInvoiceResponse, LightningRpcError> {
153 Err(LightningRpcError::FailedPayment {
154 failure_reason: "Private payments not supported".to_string(),
155 })
156 }
157
158 fn supports_private_payments(&self) -> bool {
162 false
163 }
164
165 async fn route_htlcs<'a>(
176 self: Box<Self>,
177 task_group: &TaskGroup,
178 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
179
180 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
184
185 async fn create_invoice(
190 &self,
191 create_invoice_request: CreateInvoiceRequest,
192 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
193
194 async fn get_ln_onchain_address(
197 &self,
198 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
199
200 async fn send_onchain(
203 &self,
204 payload: SendOnchainRequest,
205 ) -> Result<SendOnchainResponse, LightningRpcError>;
206
207 async fn open_channel(
209 &self,
210 payload: OpenChannelRequest,
211 ) -> Result<OpenChannelResponse, LightningRpcError>;
212
213 async fn close_channels_with_peer(
215 &self,
216 payload: CloseChannelsWithPeerRequest,
217 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
218
219 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
221
222 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
225
226 async fn get_invoice(
227 &self,
228 get_invoice_request: GetInvoiceRequest,
229 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
230
231 async fn list_transactions(
232 &self,
233 start_secs: u64,
234 end_secs: u64,
235 ) -> Result<ListTransactionsResponse, LightningRpcError>;
236
237 fn create_offer(
238 &self,
239 amount: Option<Amount>,
240 description: Option<String>,
241 expiry_secs: Option<u32>,
242 quantity: Option<u64>,
243 ) -> Result<String, LightningRpcError>;
244
245 async fn pay_offer(
246 &self,
247 offer: String,
248 quantity: Option<u64>,
249 amount: Option<Amount>,
250 payer_note: Option<String>,
251 ) -> Result<Preimage, LightningRpcError>;
252
253 fn sync_wallet(&self) -> Result<(), LightningRpcError>;
254}
255
256impl dyn ILnRpcClient {
257 pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
261 if num_route_hints == 0 {
262 return vec![];
263 }
264
265 let route_hints =
266 self.routehints(num_route_hints as usize)
267 .await
268 .unwrap_or(GetRouteHintsResponse {
269 route_hints: Vec::new(),
270 });
271 route_hints.route_hints
272 }
273
274 pub async fn parsed_node_info(&self) -> LightningInfo {
277 if let Ok(info) = self.info().await
278 && let Ok(network) =
279 Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
280 failure_reason: format!("Invalid network {}: {e}", info.network),
281 })
282 {
283 return LightningInfo::Connected {
284 public_key: info.pub_key,
285 alias: info.alias,
286 network,
287 block_height: info.block_height as u64,
288 synced_to_chain: info.synced_to_chain,
289 };
290 }
291
292 LightningInfo::NotConnected
293 }
294
295 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
297 if is_env_var_set(FM_IN_DEVIMINT_ENV) {
301 self.sync_wallet()?;
302 }
303
304 retry(
306 "Wait for chain sync",
307 backoff_util::background_backoff(),
308 || async {
309 let info = self.info().await?;
310 let block_height = info.block_height;
311 if info.synced_to_chain {
312 Ok(())
313 } else {
314 warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
315 Err(anyhow::anyhow!("Not synced yet"))
316 }
317 },
318 )
319 .await
320 .map_err(|e| LightningRpcError::FailedToSyncToChain {
321 failure_reason: format!("Failed to sync to chain: {e:?}"),
322 })?;
323
324 info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
325 Ok(())
326 }
327}
328
329#[derive(Debug, Serialize, Deserialize, Clone)]
330pub struct GetNodeInfoResponse {
331 pub pub_key: PublicKey,
332 pub alias: String,
333 pub network: String,
334 pub block_height: u32,
335 pub synced_to_chain: bool,
336}
337
338#[derive(Debug, Serialize, Deserialize, Clone)]
339pub struct InterceptPaymentRequest {
340 pub payment_hash: sha256::Hash,
341 pub amount_msat: u64,
342 pub expiry: u32,
343 pub incoming_chan_id: u64,
344 pub short_channel_id: Option<u64>,
345 pub htlc_id: u64,
346}
347
348#[derive(Debug, Serialize, Deserialize, Clone)]
349pub struct InterceptPaymentResponse {
350 pub incoming_chan_id: u64,
351 pub htlc_id: u64,
352 pub payment_hash: sha256::Hash,
353 pub action: PaymentAction,
354}
355
356#[derive(Debug, Serialize, Deserialize, Clone)]
357pub enum PaymentAction {
358 Settle(Preimage),
359 Cancel,
360 Forward,
361}
362
363#[derive(Debug, Serialize, Deserialize, Clone)]
364pub struct GetRouteHintsResponse {
365 pub route_hints: Vec<RouteHint>,
366}
367
368#[derive(Debug, Serialize, Deserialize, Clone)]
369pub struct PayInvoiceResponse {
370 pub preimage: Preimage,
371}
372
373#[derive(Debug, Serialize, Deserialize, Clone)]
374pub struct CreateInvoiceRequest {
375 pub payment_hash: Option<sha256::Hash>,
376 pub amount_msat: u64,
377 pub expiry_secs: u32,
378 pub description: Option<InvoiceDescription>,
379}
380
381#[derive(Debug, Serialize, Deserialize, Clone)]
382pub enum InvoiceDescription {
383 Direct(String),
384 Hash(sha256::Hash),
385}
386
387#[derive(Debug, Serialize, Deserialize, Clone)]
388pub struct CreateInvoiceResponse {
389 pub invoice: String,
390}
391
392#[derive(Debug, Serialize, Deserialize, Clone)]
393pub struct GetLnOnchainAddressResponse {
394 pub address: String,
395}
396
397#[derive(Debug, Serialize, Deserialize, Clone)]
398pub struct SendOnchainResponse {
399 pub txid: String,
400}
401
402#[derive(Debug, Serialize, Deserialize, Clone)]
403pub struct OpenChannelResponse {
404 pub funding_txid: String,
405}
406
407#[derive(Debug, Serialize, Deserialize, Clone)]
408pub struct ListChannelsResponse {
409 pub channels: Vec<ChannelInfo>,
410}
411
412#[derive(Debug, Serialize, Deserialize, Clone)]
413pub struct GetBalancesResponse {
414 pub onchain_balance_sats: u64,
415 pub lightning_balance_msats: u64,
416 pub inbound_lightning_liquidity_msats: u64,
417}
418
419pub struct LnRpcTracked {
431 inner: Arc<dyn ILnRpcClient>,
432 name: &'static str,
433}
434
435impl std::fmt::Debug for LnRpcTracked {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 f.debug_struct("LnRpcTracked")
438 .field("name", &self.name)
439 .field("inner", &self.inner)
440 .finish()
441 }
442}
443
444impl LnRpcTracked {
445 #[allow(clippy::new_ret_no_self)]
450 pub fn new(inner: Arc<dyn ILnRpcClient>, name: &'static str) -> Arc<dyn ILnRpcClient> {
451 Arc::new(Self { inner, name })
452 }
453
454 fn record_call<T, E>(&self, method: &str, result: &Result<T, E>) {
455 let result_label = if result.is_ok() { "success" } else { "error" };
456 metrics::LN_RPC_REQUESTS_TOTAL
457 .with_label_values(&[method, self.name, result_label])
458 .inc();
459 }
460}
461
462#[async_trait]
463impl ILnRpcClient for LnRpcTracked {
464 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
465 let timer = metrics::LN_RPC_DURATION_SECONDS
466 .with_label_values(&["info", self.name])
467 .start_timer_ext();
468 let result = self.inner.info().await;
469 timer.observe_duration();
470 self.record_call("info", &result);
471 result
472 }
473
474 async fn routehints(
475 &self,
476 num_route_hints: usize,
477 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
478 let timer = metrics::LN_RPC_DURATION_SECONDS
479 .with_label_values(&["routehints", self.name])
480 .start_timer_ext();
481 let result = self.inner.routehints(num_route_hints).await;
482 timer.observe_duration();
483 self.record_call("routehints", &result);
484 result
485 }
486
487 async fn pay(
488 &self,
489 invoice: Bolt11Invoice,
490 max_delay: u64,
491 max_fee: Amount,
492 ) -> Result<PayInvoiceResponse, LightningRpcError> {
493 let timer = metrics::LN_RPC_DURATION_SECONDS
494 .with_label_values(&["pay", self.name])
495 .start_timer_ext();
496 let result = self.inner.pay(invoice, max_delay, max_fee).await;
497 timer.observe_duration();
498 self.record_call("pay", &result);
499 result
500 }
501
502 async fn pay_private(
503 &self,
504 invoice: PrunedInvoice,
505 max_delay: u64,
506 max_fee: Amount,
507 ) -> Result<PayInvoiceResponse, LightningRpcError> {
508 let timer = metrics::LN_RPC_DURATION_SECONDS
509 .with_label_values(&["pay_private", self.name])
510 .start_timer_ext();
511 let result = self.inner.pay_private(invoice, max_delay, max_fee).await;
512 timer.observe_duration();
513 self.record_call("pay_private", &result);
514 result
515 }
516
517 fn supports_private_payments(&self) -> bool {
518 self.inner.supports_private_payments()
519 }
520
521 async fn route_htlcs<'a>(
522 self: Box<Self>,
523 _task_group: &TaskGroup,
524 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
525 panic!(
529 "route_htlcs should not be called on LnRpcTracked. \
530 Wrap the Arc returned from route_htlcs instead."
531 );
532 }
533
534 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
535 let timer = metrics::LN_RPC_DURATION_SECONDS
536 .with_label_values(&["complete_htlc", self.name])
537 .start_timer_ext();
538 let result = self.inner.complete_htlc(htlc).await;
539 timer.observe_duration();
540 self.record_call("complete_htlc", &result);
541 result
542 }
543
544 async fn create_invoice(
545 &self,
546 create_invoice_request: CreateInvoiceRequest,
547 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
548 let timer = metrics::LN_RPC_DURATION_SECONDS
549 .with_label_values(&["create_invoice", self.name])
550 .start_timer_ext();
551 let result = self.inner.create_invoice(create_invoice_request).await;
552 timer.observe_duration();
553 self.record_call("create_invoice", &result);
554 result
555 }
556
557 async fn get_ln_onchain_address(
558 &self,
559 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
560 let timer = metrics::LN_RPC_DURATION_SECONDS
561 .with_label_values(&["get_ln_onchain_address", self.name])
562 .start_timer_ext();
563 let result = self.inner.get_ln_onchain_address().await;
564 timer.observe_duration();
565 self.record_call("get_ln_onchain_address", &result);
566 result
567 }
568
569 async fn send_onchain(
570 &self,
571 payload: SendOnchainRequest,
572 ) -> Result<SendOnchainResponse, LightningRpcError> {
573 let timer = metrics::LN_RPC_DURATION_SECONDS
574 .with_label_values(&["send_onchain", self.name])
575 .start_timer_ext();
576 let result = self.inner.send_onchain(payload).await;
577 timer.observe_duration();
578 self.record_call("send_onchain", &result);
579 result
580 }
581
582 async fn open_channel(
583 &self,
584 payload: OpenChannelRequest,
585 ) -> Result<OpenChannelResponse, LightningRpcError> {
586 let timer = metrics::LN_RPC_DURATION_SECONDS
587 .with_label_values(&["open_channel", self.name])
588 .start_timer_ext();
589 let result = self.inner.open_channel(payload).await;
590 timer.observe_duration();
591 self.record_call("open_channel", &result);
592 result
593 }
594
595 async fn close_channels_with_peer(
596 &self,
597 payload: CloseChannelsWithPeerRequest,
598 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
599 let timer = metrics::LN_RPC_DURATION_SECONDS
600 .with_label_values(&["close_channels_with_peer", self.name])
601 .start_timer_ext();
602 let result = self.inner.close_channels_with_peer(payload).await;
603 timer.observe_duration();
604 self.record_call("close_channels_with_peer", &result);
605 result
606 }
607
608 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
609 let timer = metrics::LN_RPC_DURATION_SECONDS
610 .with_label_values(&["list_channels", self.name])
611 .start_timer_ext();
612 let result = self.inner.list_channels().await;
613 timer.observe_duration();
614 self.record_call("list_channels", &result);
615 result
616 }
617
618 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
619 let timer = metrics::LN_RPC_DURATION_SECONDS
620 .with_label_values(&["get_balances", self.name])
621 .start_timer_ext();
622 let result = self.inner.get_balances().await;
623 timer.observe_duration();
624 self.record_call("get_balances", &result);
625 result
626 }
627
628 async fn get_invoice(
629 &self,
630 get_invoice_request: GetInvoiceRequest,
631 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
632 let timer = metrics::LN_RPC_DURATION_SECONDS
633 .with_label_values(&["get_invoice", self.name])
634 .start_timer_ext();
635 let result = self.inner.get_invoice(get_invoice_request).await;
636 timer.observe_duration();
637 self.record_call("get_invoice", &result);
638 result
639 }
640
641 async fn list_transactions(
642 &self,
643 start_secs: u64,
644 end_secs: u64,
645 ) -> Result<ListTransactionsResponse, LightningRpcError> {
646 let timer = metrics::LN_RPC_DURATION_SECONDS
647 .with_label_values(&["list_transactions", self.name])
648 .start_timer_ext();
649 let result = self.inner.list_transactions(start_secs, end_secs).await;
650 timer.observe_duration();
651 self.record_call("list_transactions", &result);
652 result
653 }
654
655 fn create_offer(
656 &self,
657 amount: Option<Amount>,
658 description: Option<String>,
659 expiry_secs: Option<u32>,
660 quantity: Option<u64>,
661 ) -> Result<String, LightningRpcError> {
662 let timer = metrics::LN_RPC_DURATION_SECONDS
663 .with_label_values(&["create_offer", self.name])
664 .start_timer_ext();
665 let result = self
666 .inner
667 .create_offer(amount, description, expiry_secs, quantity);
668 timer.observe_duration();
669 self.record_call("create_offer", &result);
670 result
671 }
672
673 async fn pay_offer(
674 &self,
675 offer: String,
676 quantity: Option<u64>,
677 amount: Option<Amount>,
678 payer_note: Option<String>,
679 ) -> Result<Preimage, LightningRpcError> {
680 let timer = metrics::LN_RPC_DURATION_SECONDS
681 .with_label_values(&["pay_offer", self.name])
682 .start_timer_ext();
683 let result = self
684 .inner
685 .pay_offer(offer, quantity, amount, payer_note)
686 .await;
687 timer.observe_duration();
688 self.record_call("pay_offer", &result);
689 result
690 }
691
692 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
693 let timer = metrics::LN_RPC_DURATION_SECONDS
694 .with_label_values(&["sync_wallet", self.name])
695 .start_timer_ext();
696 let result = self.inner.sync_wallet();
697 timer.observe_duration();
698 self.record_call("sync_wallet", &result);
699 result
700 }
701}