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