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::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 set channel fees: {failure_reason}")]
60 FailedToSetChannelFees { failure_reason: String },
61 #[error("Failed to get Invoice: {failure_reason}")]
62 FailedToGetInvoice { failure_reason: String },
63 #[error("Failed to list transactions: {failure_reason}")]
64 FailedToListTransactions { failure_reason: String },
65 #[error("Failed to get funding address: {failure_reason}")]
66 FailedToGetLnOnchainAddress { failure_reason: String },
67 #[error("Failed to withdraw funds on-chain: {failure_reason}")]
68 FailedToWithdrawOnchain { failure_reason: String },
69 #[error("Failed to connect to peer: {failure_reason}")]
70 FailedToConnectToPeer { failure_reason: String },
71 #[error("Failed to list active channels: {failure_reason}")]
72 FailedToListChannels { failure_reason: String },
73 #[error("Failed to get balances: {failure_reason}")]
74 FailedToGetBalances { failure_reason: String },
75 #[error("Failed to sync to chain: {failure_reason}")]
76 FailedToSyncToChain { failure_reason: String },
77 #[error("Invalid metadata: {failure_reason}")]
78 InvalidMetadata { failure_reason: String },
79 #[error("Bolt12 Error: {failure_reason}")]
80 Bolt12Error { failure_reason: String },
81}
82
83#[derive(Clone, Debug)]
85pub struct LightningContext {
86 pub lnrpc: Arc<dyn ILnRpcClient>,
87 pub lightning_public_key: PublicKey,
88 pub lightning_alias: String,
89 pub lightning_network: Network,
90}
91
92#[async_trait]
96pub trait ILnRpcClient: Debug + Send + Sync {
97 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
99
100 async fn routehints(
105 &self,
106 num_route_hints: usize,
107 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
108
109 async fn pay(
126 &self,
127 invoice: Bolt11Invoice,
128 max_delay: u64,
129 max_fee: Amount,
130 ) -> Result<PayInvoiceResponse, LightningRpcError> {
131 self.pay_private(
132 PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
133 failure_reason: "Invoice has no amount".to_string(),
134 })?,
135 max_delay,
136 max_fee,
137 )
138 .await
139 }
140
141 async fn pay_private(
151 &self,
152 _invoice: PrunedInvoice,
153 _max_delay: u64,
154 _max_fee: Amount,
155 ) -> Result<PayInvoiceResponse, LightningRpcError> {
156 Err(LightningRpcError::FailedPayment {
157 failure_reason: "Private payments not supported".to_string(),
158 })
159 }
160
161 fn supports_private_payments(&self) -> bool {
165 false
166 }
167
168 async fn route_htlcs<'a>(
179 self: Box<Self>,
180 task_group: &TaskGroup,
181 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
182
183 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
187
188 async fn create_invoice(
193 &self,
194 create_invoice_request: CreateInvoiceRequest,
195 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
196
197 async fn get_ln_onchain_address(
200 &self,
201 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
202
203 async fn send_onchain(
206 &self,
207 payload: SendOnchainRequest,
208 ) -> Result<SendOnchainResponse, LightningRpcError>;
209
210 async fn open_channel(
212 &self,
213 payload: OpenChannelRequest,
214 ) -> Result<OpenChannelResponse, LightningRpcError>;
215
216 async fn close_channels_with_peer(
218 &self,
219 payload: CloseChannelsWithPeerRequest,
220 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
221
222 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
224
225 async fn set_channel_fees(
229 &self,
230 payload: SetChannelFeesRequest,
231 ) -> Result<(), LightningRpcError>;
232
233 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
236
237 async fn get_invoice(
238 &self,
239 get_invoice_request: GetInvoiceRequest,
240 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
241
242 async fn list_transactions(
243 &self,
244 start_secs: u64,
245 end_secs: u64,
246 ) -> Result<ListTransactionsResponse, LightningRpcError>;
247
248 fn create_offer(
249 &self,
250 amount: Option<Amount>,
251 description: Option<String>,
252 expiry_secs: Option<u32>,
253 quantity: Option<u64>,
254 ) -> Result<String, LightningRpcError>;
255
256 async fn pay_offer(
257 &self,
258 offer: String,
259 quantity: Option<u64>,
260 amount: Option<Amount>,
261 payer_note: Option<String>,
262 ) -> Result<Preimage, LightningRpcError>;
263
264 fn sync_wallet(&self) -> Result<(), LightningRpcError>;
265}
266
267impl dyn ILnRpcClient {
268 pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
272 if num_route_hints == 0 {
273 return vec![];
274 }
275
276 let route_hints =
277 self.routehints(num_route_hints as usize)
278 .await
279 .unwrap_or(GetRouteHintsResponse {
280 route_hints: Vec::new(),
281 });
282 route_hints.route_hints
283 }
284
285 pub async fn parsed_node_info(&self) -> LightningInfo {
288 if let Ok(info) = self.info().await
289 && let Ok(network) =
290 Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
291 failure_reason: format!("Invalid network {}: {e}", info.network),
292 })
293 {
294 return LightningInfo::Connected {
295 public_key: info.pub_key,
296 alias: info.alias,
297 network,
298 block_height: info.block_height as u64,
299 synced_to_chain: info.synced_to_chain,
300 };
301 }
302
303 LightningInfo::NotConnected
304 }
305
306 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
308 if is_env_var_set(FM_IN_DEVIMINT_ENV) {
312 self.sync_wallet()?;
313 }
314
315 retry(
317 "Wait for chain sync",
318 backoff_util::background_backoff(),
319 || async {
320 let info = self.info().await?;
321 let block_height = info.block_height;
322 if info.synced_to_chain {
323 Ok(())
324 } else {
325 warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
326 Err(anyhow::anyhow!("Not synced yet"))
327 }
328 },
329 )
330 .await
331 .map_err(|e| LightningRpcError::FailedToSyncToChain {
332 failure_reason: format!("Failed to sync to chain: {e:?}"),
333 })?;
334
335 info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
336 Ok(())
337 }
338}
339
340#[derive(Debug, Serialize, Deserialize, Clone)]
341pub struct GetNodeInfoResponse {
342 pub pub_key: PublicKey,
343 pub alias: String,
344 pub network: String,
345 pub block_height: u32,
346 pub synced_to_chain: bool,
347}
348
349#[derive(Debug, Serialize, Deserialize, Clone)]
350pub struct InterceptPaymentRequest {
351 pub payment_hash: sha256::Hash,
352 pub amount_msat: u64,
353 pub expiry: u32,
354 pub incoming_chan_id: u64,
355 pub short_channel_id: Option<u64>,
356 pub htlc_id: u64,
357}
358
359#[derive(Debug, Serialize, Deserialize, Clone)]
360pub struct InterceptPaymentResponse {
361 pub incoming_chan_id: u64,
362 pub htlc_id: u64,
363 pub payment_hash: sha256::Hash,
364 pub action: PaymentAction,
365}
366
367#[derive(Debug, Serialize, Deserialize, Clone)]
368pub enum PaymentAction {
369 Settle(Preimage),
370 Cancel,
371 Forward,
372}
373
374#[derive(Debug, Serialize, Deserialize, Clone)]
375pub struct GetRouteHintsResponse {
376 pub route_hints: Vec<RouteHint>,
377}
378
379#[derive(Debug, Serialize, Deserialize, Clone)]
380pub struct PayInvoiceResponse {
381 pub preimage: Preimage,
382}
383
384#[derive(Debug, Serialize, Deserialize, Clone)]
385pub struct CreateInvoiceRequest {
386 pub payment_hash: Option<sha256::Hash>,
387 pub amount_msat: u64,
388 pub expiry_secs: u32,
389 pub description: Option<InvoiceDescription>,
390}
391
392#[derive(Debug, Serialize, Deserialize, Clone)]
393pub enum InvoiceDescription {
394 Direct(String),
395 Hash(sha256::Hash),
396}
397
398#[derive(Debug, Serialize, Deserialize, Clone)]
399pub struct CreateInvoiceResponse {
400 pub invoice: String,
401}
402
403#[derive(Debug, Serialize, Deserialize, Clone)]
404pub struct GetLnOnchainAddressResponse {
405 pub address: String,
406}
407
408#[derive(Debug, Serialize, Deserialize, Clone)]
409pub struct SendOnchainResponse {
410 pub txid: String,
411}
412
413#[derive(Debug, Serialize, Deserialize, Clone)]
414pub struct OpenChannelResponse {
415 pub funding_txid: String,
416}
417
418#[derive(Debug, Serialize, Deserialize, Clone)]
419pub struct ListChannelsResponse {
420 pub channels: Vec<ChannelInfo>,
421}
422
423#[derive(Debug, Serialize, Deserialize, Clone)]
424pub struct GetBalancesResponse {
425 pub onchain_balance_sats: u64,
426 pub lightning_balance_msats: u64,
427 pub inbound_lightning_liquidity_msats: u64,
428}
429
430pub struct LnRpcTracked {
442 inner: Arc<dyn ILnRpcClient>,
443 name: &'static str,
444}
445
446impl std::fmt::Debug for LnRpcTracked {
447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 f.debug_struct("LnRpcTracked")
449 .field("name", &self.name)
450 .field("inner", &self.inner)
451 .finish()
452 }
453}
454
455impl LnRpcTracked {
456 #[allow(clippy::new_ret_no_self)]
461 pub fn new(inner: Arc<dyn ILnRpcClient>, name: &'static str) -> Arc<dyn ILnRpcClient> {
462 Arc::new(Self { inner, name })
463 }
464
465 fn record_call<T, E>(&self, method: &str, result: &Result<T, E>) {
466 let result_label = if result.is_ok() { "success" } else { "error" };
467 metrics::LN_RPC_REQUESTS_TOTAL
468 .with_label_values(&[method, self.name, result_label])
469 .inc();
470 }
471}
472
473macro_rules! tracked_call {
474 ($self:ident, $method:expr, $call:expr) => {{
475 trace!(
476 target: LOG_LIGHTNING,
477 method = $method,
478 name = $self.name,
479 "starting lightning rpc"
480 );
481 let start = now();
482 let timer = metrics::LN_RPC_DURATION_SECONDS
483 .with_label_values(&[$method, $self.name])
484 .start_timer_ext();
485 let result = $call;
486 timer.observe_duration();
487 $self.record_call($method, &result);
488 let duration_ms = now()
489 .duration_since(start)
490 .unwrap_or_default()
491 .as_secs_f64()
492 * 1000.0;
493 trace!(
494 target: LOG_LIGHTNING,
495 method = $method,
496 name = $self.name,
497 duration_ms,
498 error = %result.fmt_compact_result(),
499 "completed lightning rpc"
500 );
501 result
502 }};
503}
504
505#[async_trait]
506impl ILnRpcClient for LnRpcTracked {
507 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
508 tracked_call!(self, "info", self.inner.info().await)
509 }
510
511 async fn routehints(
512 &self,
513 num_route_hints: usize,
514 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
515 tracked_call!(
516 self,
517 "routehints",
518 self.inner.routehints(num_route_hints).await
519 )
520 }
521
522 async fn pay(
523 &self,
524 invoice: Bolt11Invoice,
525 max_delay: u64,
526 max_fee: Amount,
527 ) -> Result<PayInvoiceResponse, LightningRpcError> {
528 tracked_call!(
529 self,
530 "pay",
531 self.inner.pay(invoice, max_delay, max_fee).await
532 )
533 }
534
535 async fn pay_private(
536 &self,
537 invoice: PrunedInvoice,
538 max_delay: u64,
539 max_fee: Amount,
540 ) -> Result<PayInvoiceResponse, LightningRpcError> {
541 tracked_call!(
542 self,
543 "pay_private",
544 self.inner.pay_private(invoice, max_delay, max_fee).await
545 )
546 }
547
548 fn supports_private_payments(&self) -> bool {
549 self.inner.supports_private_payments()
550 }
551
552 async fn route_htlcs<'a>(
553 self: Box<Self>,
554 _task_group: &TaskGroup,
555 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
556 panic!(
560 "route_htlcs should not be called on LnRpcTracked. \
561 Wrap the Arc returned from route_htlcs instead."
562 );
563 }
564
565 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError> {
566 tracked_call!(self, "complete_htlc", self.inner.complete_htlc(htlc).await)
567 }
568
569 async fn create_invoice(
570 &self,
571 create_invoice_request: CreateInvoiceRequest,
572 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
573 tracked_call!(
574 self,
575 "create_invoice",
576 self.inner.create_invoice(create_invoice_request).await
577 )
578 }
579
580 async fn get_ln_onchain_address(
581 &self,
582 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
583 tracked_call!(
584 self,
585 "get_ln_onchain_address",
586 self.inner.get_ln_onchain_address().await
587 )
588 }
589
590 async fn send_onchain(
591 &self,
592 payload: SendOnchainRequest,
593 ) -> Result<SendOnchainResponse, LightningRpcError> {
594 tracked_call!(self, "send_onchain", self.inner.send_onchain(payload).await)
595 }
596
597 async fn open_channel(
598 &self,
599 payload: OpenChannelRequest,
600 ) -> Result<OpenChannelResponse, LightningRpcError> {
601 tracked_call!(self, "open_channel", self.inner.open_channel(payload).await)
602 }
603
604 async fn close_channels_with_peer(
605 &self,
606 payload: CloseChannelsWithPeerRequest,
607 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
608 tracked_call!(
609 self,
610 "close_channels_with_peer",
611 self.inner.close_channels_with_peer(payload).await
612 )
613 }
614
615 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
616 tracked_call!(self, "list_channels", self.inner.list_channels().await)
617 }
618
619 async fn set_channel_fees(
620 &self,
621 payload: SetChannelFeesRequest,
622 ) -> Result<(), LightningRpcError> {
623 tracked_call!(
624 self,
625 "set_channel_fees",
626 self.inner.set_channel_fees(payload).await
627 )
628 }
629
630 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
631 tracked_call!(self, "get_balances", self.inner.get_balances().await)
632 }
633
634 async fn get_invoice(
635 &self,
636 get_invoice_request: GetInvoiceRequest,
637 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
638 tracked_call!(
639 self,
640 "get_invoice",
641 self.inner.get_invoice(get_invoice_request).await
642 )
643 }
644
645 async fn list_transactions(
646 &self,
647 start_secs: u64,
648 end_secs: u64,
649 ) -> Result<ListTransactionsResponse, LightningRpcError> {
650 tracked_call!(
651 self,
652 "list_transactions",
653 self.inner.list_transactions(start_secs, end_secs).await
654 )
655 }
656
657 fn create_offer(
658 &self,
659 amount: Option<Amount>,
660 description: Option<String>,
661 expiry_secs: Option<u32>,
662 quantity: Option<u64>,
663 ) -> Result<String, LightningRpcError> {
664 tracked_call!(
665 self,
666 "create_offer",
667 self.inner
668 .create_offer(amount, description, expiry_secs, quantity)
669 )
670 }
671
672 async fn pay_offer(
673 &self,
674 offer: String,
675 quantity: Option<u64>,
676 amount: Option<Amount>,
677 payer_note: Option<String>,
678 ) -> Result<Preimage, LightningRpcError> {
679 tracked_call!(
680 self,
681 "pay_offer",
682 self.inner
683 .pay_offer(offer, quantity, amount, payer_note)
684 .await
685 )
686 }
687
688 fn sync_wallet(&self) -> Result<(), LightningRpcError> {
689 tracked_call!(self, "sync_wallet", self.inner.sync_wallet())
690 }
691}