Skip to main content

fedimint_lightning/
lib.rs

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/// Represents an active connection to the lightning node.
82#[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/// A trait that the gateway uses to interact with a lightning node. This allows
91/// the gateway to be agnostic to the specific lightning node implementation
92/// being used.
93#[async_trait]
94pub trait ILnRpcClient: Debug + Send + Sync {
95    /// Returns high-level info about the lightning node.
96    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
97
98    /// Returns route hints to the lightning node.
99    ///
100    /// Note: This is only used for inbound LNv1 payments and will be removed
101    /// when we switch to LNv2.
102    async fn routehints(
103        &self,
104        num_route_hints: usize,
105    ) -> Result<GetRouteHintsResponse, LightningRpcError>;
106
107    /// Attempts to pay an invoice using the lightning node, waiting for the
108    /// payment to complete and returning the preimage.
109    ///
110    /// Caller restrictions:
111    /// May be called multiple times for the same invoice, but _should_ be done
112    /// with all the same parameters. This is because the payment may be
113    /// in-flight from a previous call, in which case fee or delay limits cannot
114    /// be changed and will be ignored.
115    ///
116    /// Implementor restrictions:
117    /// This _must_ be idempotent for a given invoice, since it is called by
118    /// state machines. In more detail, when called for a given invoice:
119    /// * If the payment is already in-flight, wait for that payment to complete
120    ///   as if it were the first call.
121    /// * If the payment has already been attempted and failed, return an error.
122    /// * If the payment has already succeeded, return a success response.
123    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    /// Attempts to pay an invoice using the lightning node, waiting for the
140    /// payment to complete and returning the preimage.
141    ///
142    /// This is more private than [`ILnRpcClient::pay`], as it does not require
143    /// the invoice description. If this is implemented,
144    /// [`ILnRpcClient::supports_private_payments`] must return true.
145    ///
146    /// Note: This is only used for outbound LNv1 payments and will be removed
147    /// when we switch to LNv2.
148    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    /// Returns true if the lightning backend supports payments without full
160    /// invoices. If this returns true, [`ILnRpcClient::pay_private`] must
161    /// be implemented.
162    fn supports_private_payments(&self) -> bool {
163        false
164    }
165
166    /// Consumes the current client and returns a stream of intercepted HTLCs
167    /// and a new client. `complete_htlc` must be called for all successfully
168    /// intercepted HTLCs sent to the returned stream.
169    ///
170    /// `route_htlcs` can only be called once for a given client, since the
171    /// returned stream grants exclusive routing decisions to the caller.
172    /// For this reason, `route_htlc` consumes the client and returns one
173    /// wrapped in an `Arc`. This lets the compiler enforce that `route_htlcs`
174    /// can only be called once for a given client, since the value inside
175    /// the `Arc` cannot be consumed.
176    async fn route_htlcs<'a>(
177        self: Box<Self>,
178        task_group: &TaskGroup,
179    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
180
181    /// Completes an HTLC that was intercepted by the gateway. Must be called
182    /// for all successfully intercepted HTLCs sent to the stream returned
183    /// by `route_htlcs`.
184    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
185
186    /// Requests the lightning node to create an invoice. The presence of a
187    /// payment hash in the `CreateInvoiceRequest` determines if the invoice is
188    /// intended to be an ecash payment or a direct payment to this lightning
189    /// node.
190    async fn create_invoice(
191        &self,
192        create_invoice_request: CreateInvoiceRequest,
193    ) -> Result<CreateInvoiceResponse, LightningRpcError>;
194
195    /// Gets a funding address belonging to the lightning node's on-chain
196    /// wallet.
197    async fn get_ln_onchain_address(
198        &self,
199    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
200
201    /// Executes an onchain transaction using the lightning node's on-chain
202    /// wallet.
203    async fn send_onchain(
204        &self,
205        payload: SendOnchainRequest,
206    ) -> Result<SendOnchainResponse, LightningRpcError>;
207
208    /// Opens a channel with a peer lightning node.
209    async fn open_channel(
210        &self,
211        payload: OpenChannelRequest,
212    ) -> Result<OpenChannelResponse, LightningRpcError>;
213
214    /// Closes all channels with a peer lightning node.
215    async fn close_channels_with_peer(
216        &self,
217        payload: CloseChannelsWithPeerRequest,
218    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
219
220    /// Lists the lightning node's active channels with all peers.
221    async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
222
223    /// Returns a summary of the lightning node's balance, including the onchain
224    /// wallet, outbound liquidity, and inbound liquidity.
225    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    /// Retrieve route hints from the Lightning node, capped at
259    /// `num_route_hints`. The route hints should be ordered based on liquidity
260    /// of incoming channels.
261    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    /// Retrieves the basic information about the Gateway's connected Lightning
276    /// node.
277    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    /// Waits for the Lightning node to be synced to the Bitcoin blockchain.
297    pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
298        // In devimint, we explicitly sync the onchain wallet to start the sync quicker
299        // than background sync would. In production, background sync is
300        // sufficient
301        if is_env_var_set(FM_IN_DEVIMINT_ENV) {
302            self.sync_wallet()?;
303        }
304
305        // Wait for the Lightning node to sync
306        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
420/// A wrapper around `Arc<dyn ILnRpcClient>` that tracks metrics for each RPC
421/// call.
422///
423/// This wrapper records the duration and success/error status of each
424/// Lightning RPC call to Prometheus metrics, allowing monitoring of
425/// Lightning node connectivity and performance.
426///
427/// Note: This wrapper is designed to wrap the `Arc<dyn ILnRpcClient>` returned
428/// from `route_htlcs`. Calling `route_htlcs` on this wrapper will panic, as
429/// `route_htlcs` should only be called once on the original client before
430/// wrapping.
431pub 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    /// Wraps an `Arc<dyn ILnRpcClient>` with metrics tracking.
447    ///
448    /// The `name` parameter is used to distinguish different uses of the
449    /// Lightning RPC client in metrics (e.g., "gateway").
450    #[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        // route_htlcs should only be called once on the original client before
547        // wrapping with LnRpcTracked. The Arc returned from route_htlcs should
548        // be wrapped with LnRpcTracked::new.
549        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}