ln_gateway/lightning/
mod.rs

1pub mod cln;
2pub mod extension;
3pub mod ldk;
4pub mod lnd;
5
6use std::fmt::Debug;
7use std::path::PathBuf;
8use std::str::FromStr;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use bitcoin::Network;
13use clap::Subcommand;
14use fedimint_bip39::Mnemonic;
15use fedimint_core::db::Database;
16use fedimint_core::encoding::{Decodable, Encodable};
17use fedimint_core::envs::is_env_var_set;
18use fedimint_core::secp256k1::PublicKey;
19use fedimint_core::task::TaskGroup;
20use fedimint_core::util::{backoff_util, retry, SafeUrl};
21use fedimint_core::{secp256k1, Amount};
22use fedimint_ln_common::route_hints::RouteHint;
23use fedimint_ln_common::PrunedInvoice;
24use futures::stream::BoxStream;
25use lightning_invoice::Bolt11Invoice;
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28use tracing::{debug, info, warn};
29
30use self::cln::NetworkLnRpcClient;
31use self::lnd::GatewayLndClient;
32use crate::envs::{
33    FM_GATEWAY_LIGHTNING_ADDR_ENV, FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV, FM_LDK_ESPLORA_SERVER_URL,
34    FM_LDK_NETWORK, FM_LND_MACAROON_ENV, FM_LND_RPC_ADDR_ENV, FM_LND_TLS_CERT_ENV, FM_PORT_LDK,
35};
36use crate::rpc::{CloseChannelsWithPeerPayload, SendOnchainPayload};
37use crate::{OpenChannelPayload, Preimage};
38
39pub const MAX_LIGHTNING_RETRIES: u32 = 10;
40
41pub type RouteHtlcStream<'a> = BoxStream<'a, InterceptPaymentRequest>;
42
43#[derive(
44    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
45)]
46pub enum LightningRpcError {
47    #[error("Failed to connect to Lightning node")]
48    FailedToConnect,
49    #[error("Failed to retrieve node info: {failure_reason}")]
50    FailedToGetNodeInfo { failure_reason: String },
51    #[error("Failed to retrieve route hints: {failure_reason}")]
52    FailedToGetRouteHints { failure_reason: String },
53    #[error("Payment failed: {failure_reason}")]
54    FailedPayment { failure_reason: String },
55    #[error("Failed to route HTLCs: {failure_reason}")]
56    FailedToRouteHtlcs { failure_reason: String },
57    #[error("Failed to complete HTLC: {failure_reason}")]
58    FailedToCompleteHtlc { failure_reason: String },
59    #[error("Failed to open channel: {failure_reason}")]
60    FailedToOpenChannel { failure_reason: String },
61    #[error("Failed to close channel: {failure_reason}")]
62    FailedToCloseChannelsWithPeer { failure_reason: String },
63    #[error("Failed to get Invoice: {failure_reason}")]
64    FailedToGetInvoice { 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    FailedToListActiveChannels { failure_reason: String },
73    #[error("Failed to get balances: {failure_reason}")]
74    FailedToGetBalances { failure_reason: String },
75    #[error("Failed to subscribe to invoice updates: {failure_reason}")]
76    FailedToSubscribeToInvoiceUpdates { failure_reason: String },
77    #[error("Failed to sync to chain: {failure_reason}")]
78    FailedToSyncToChain { failure_reason: String },
79    #[error("Invalid metadata: {failure_reason}")]
80    InvalidMetadata { failure_reason: String },
81}
82
83/// Represents an active connection to the lightning node.
84#[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/// A trait that the gateway uses to interact with a lightning node. This allows
93/// the gateway to be agnostic to the specific lightning node implementation
94/// being used.
95#[async_trait]
96pub trait ILnRpcClient: Debug + Send + Sync {
97    /// Returns high-level info about the lightning node.
98    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
99
100    /// Returns route hints to the lightning node.
101    ///
102    /// Note: This is only used for inbound LNv1 payments and will be removed
103    /// when we switch to LNv2.
104    async fn routehints(
105        &self,
106        num_route_hints: usize,
107    ) -> Result<GetRouteHintsResponse, LightningRpcError>;
108
109    /// Attempts to pay an invoice using the lightning node, waiting for the
110    /// payment to complete and returning the preimage.
111    ///
112    /// Caller restrictions:
113    /// May be called multiple times for the same invoice, but _should_ be done
114    /// with all the same parameters. This is because the payment may be
115    /// in-flight from a previous call, in which case fee or delay limits cannot
116    /// be changed and will be ignored.
117    ///
118    /// Implementor restrictions:
119    /// This _must_ be idempotent for a given invoice, since it is called by
120    /// state machines. In more detail, when called for a given invoice:
121    /// * If the payment is already in-flight, wait for that payment to complete
122    ///   as if it were the first call.
123    /// * If the payment has already been attempted and failed, return an error.
124    /// * If the payment has already succeeded, return a success response.
125    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    /// Attempts to pay an invoice using the lightning node, waiting for the
142    /// payment to complete and returning the preimage.
143    ///
144    /// This is more private than [`ILnRpcClient::pay`], as it does not require
145    /// the invoice description. If this is implemented,
146    /// [`ILnRpcClient::supports_private_payments`] must return true.
147    ///
148    /// Note: This is only used for outbound LNv1 payments and will be removed
149    /// when we switch to LNv2.
150    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    /// Returns true if the lightning backend supports payments without full
162    /// invoices. If this returns true, [`ILnRpcClient::pay_private`] must
163    /// be implemented.
164    fn supports_private_payments(&self) -> bool {
165        false
166    }
167
168    /// Consumes the current client and returns a stream of intercepted HTLCs
169    /// and a new client. `complete_htlc` must be called for all successfully
170    /// intercepted HTLCs sent to the returned stream.
171    ///
172    /// `route_htlcs` can only be called once for a given client, since the
173    /// returned stream grants exclusive routing decisions to the caller.
174    /// For this reason, `route_htlc` consumes the client and returns one
175    /// wrapped in an `Arc`. This lets the compiler enforce that `route_htlcs`
176    /// can only be called once for a given client, since the value inside
177    /// the `Arc` cannot be consumed.
178    async fn route_htlcs<'a>(
179        self: Box<Self>,
180        task_group: &TaskGroup,
181    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
182
183    /// Completes an HTLC that was intercepted by the gateway. Must be called
184    /// for all successfully intercepted HTLCs sent to the stream returned
185    /// by `route_htlcs`.
186    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
187
188    /// Requests the lightning node to create an invoice. The presence of a
189    /// payment hash in the `CreateInvoiceRequest` determines if the invoice is
190    /// intended to be an ecash payment or a direct payment to this lightning
191    /// node.
192    async fn create_invoice(
193        &self,
194        create_invoice_request: CreateInvoiceRequest,
195    ) -> Result<CreateInvoiceResponse, LightningRpcError>;
196
197    /// Gets a funding address belonging to the lightning node's on-chain
198    /// wallet.
199    async fn get_ln_onchain_address(
200        &self,
201    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
202
203    /// Executes an onchain transaction using the lightning node's on-chain
204    /// wallet.
205    async fn send_onchain(
206        &self,
207        payload: SendOnchainPayload,
208    ) -> Result<SendOnchainResponse, LightningRpcError>;
209
210    /// Opens a channel with a peer lightning node.
211    async fn open_channel(
212        &self,
213        payload: OpenChannelPayload,
214    ) -> Result<OpenChannelResponse, LightningRpcError>;
215
216    /// Closes all channels with a peer lightning node.
217    async fn close_channels_with_peer(
218        &self,
219        payload: CloseChannelsWithPeerPayload,
220    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
221
222    /// Lists the lightning node's active channels with all peers.
223    async fn list_active_channels(&self) -> Result<Vec<ChannelInfo>, LightningRpcError>;
224
225    /// Returns a summary of the lightning node's balance, including the onchain
226    /// wallet, outbound liquidity, and inbound liquidity.
227    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
228}
229
230impl dyn ILnRpcClient {
231    /// Retrieve route hints from the Lightning node, capped at
232    /// `num_route_hints`. The route hints should be ordered based on liquidity
233    /// of incoming channels.
234    pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
235        if num_route_hints == 0 {
236            return vec![];
237        }
238
239        let route_hints =
240            self.routehints(num_route_hints as usize)
241                .await
242                .unwrap_or(GetRouteHintsResponse {
243                    route_hints: Vec::new(),
244                });
245        route_hints.route_hints
246    }
247
248    /// Retrieves the basic information about the Gateway's connected Lightning
249    /// node.
250    pub async fn parsed_node_info(
251        &self,
252    ) -> std::result::Result<(PublicKey, String, Network, u32, bool), LightningRpcError> {
253        let GetNodeInfoResponse {
254            pub_key,
255            alias,
256            network,
257            block_height,
258            synced_to_chain,
259        } = self.info().await?;
260        let network =
261            Network::from_str(&network).map_err(|e| LightningRpcError::InvalidMetadata {
262                failure_reason: format!("Invalid network {network}: {e}"),
263            })?;
264        Ok((pub_key, alias, network, block_height, synced_to_chain))
265    }
266
267    /// Waits for the Lightning node to be synced to the Bitcoin blockchain.
268    pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
269        if is_env_var_set(FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV) {
270            debug!("Skip waiting for gateway to sync to chain");
271            return Ok(());
272        }
273
274        retry(
275            "Wait for chain sync",
276            backoff_util::background_backoff(),
277            || async {
278                let info = self.info().await?;
279                let block_height = info.block_height;
280                if info.synced_to_chain {
281                    Ok(())
282                } else {
283                    warn!(?block_height, "Lightning node is not synced yet");
284                    Err(anyhow::anyhow!("Not synced yet"))
285                }
286            },
287        )
288        .await
289        .map_err(|e| LightningRpcError::FailedToSyncToChain {
290            failure_reason: format!("Failed to sync to chain: {e:?}"),
291        })?;
292
293        info!("Gateway successfully synced with the chain");
294        Ok(())
295    }
296}
297
298#[derive(Serialize, Deserialize, Debug, Clone)]
299pub struct ChannelInfo {
300    pub remote_pubkey: secp256k1::PublicKey,
301    pub channel_size_sats: u64,
302    pub outbound_liquidity_sats: u64,
303    pub inbound_liquidity_sats: u64,
304    pub short_channel_id: u64,
305}
306
307#[derive(Debug, Clone, Subcommand, Serialize, Deserialize, Eq, PartialEq)]
308pub enum LightningMode {
309    #[clap(name = "lnd")]
310    Lnd {
311        /// LND RPC address
312        #[arg(long = "lnd-rpc-host", env = FM_LND_RPC_ADDR_ENV)]
313        lnd_rpc_addr: String,
314
315        /// LND TLS cert file path
316        #[arg(long = "lnd-tls-cert", env = FM_LND_TLS_CERT_ENV)]
317        lnd_tls_cert: String,
318
319        /// LND macaroon file path
320        #[arg(long = "lnd-macaroon", env = FM_LND_MACAROON_ENV)]
321        lnd_macaroon: String,
322    },
323    #[clap(name = "cln")]
324    Cln {
325        #[arg(long = "cln-extension-addr", env = FM_GATEWAY_LIGHTNING_ADDR_ENV)]
326        cln_extension_addr: SafeUrl,
327    },
328    #[clap(name = "ldk")]
329    Ldk {
330        /// LDK esplora server URL
331        #[arg(long = "ldk-esplora-server-url", env = FM_LDK_ESPLORA_SERVER_URL)]
332        esplora_server_url: String,
333
334        /// LDK network (defaults to regtest if not provided)
335        #[arg(long = "ldk-network", env = FM_LDK_NETWORK, default_value = "regtest")]
336        network: Network,
337
338        /// LDK lightning server port
339        #[arg(long = "ldk-lightning-port", env = FM_PORT_LDK)]
340        lightning_port: u16,
341    },
342}
343
344#[async_trait]
345pub trait LightningBuilder {
346    async fn build(&self) -> Box<dyn ILnRpcClient>;
347    fn lightning_mode(&self) -> Option<LightningMode> {
348        None
349    }
350}
351
352#[derive(Clone)]
353pub struct GatewayLightningBuilder {
354    pub lightning_mode: LightningMode,
355    pub gateway_db: Database,
356    pub ldk_data_dir: PathBuf,
357    pub mnemonic: Mnemonic,
358}
359
360#[async_trait]
361impl LightningBuilder for GatewayLightningBuilder {
362    async fn build(&self) -> Box<dyn ILnRpcClient> {
363        match self.lightning_mode.clone() {
364            LightningMode::Cln { cln_extension_addr } => {
365                Box::new(NetworkLnRpcClient::new(cln_extension_addr))
366            }
367            LightningMode::Lnd {
368                lnd_rpc_addr,
369                lnd_tls_cert,
370                lnd_macaroon,
371            } => Box::new(GatewayLndClient::new(
372                lnd_rpc_addr,
373                lnd_tls_cert,
374                lnd_macaroon,
375                None,
376                self.gateway_db.clone(),
377            )),
378            LightningMode::Ldk {
379                esplora_server_url,
380                network,
381                lightning_port,
382            } => Box::new(
383                ldk::GatewayLdkClient::new(
384                    &self.ldk_data_dir,
385                    &esplora_server_url,
386                    network,
387                    lightning_port,
388                    self.mnemonic.clone(),
389                )
390                .unwrap(),
391            ),
392        }
393    }
394
395    fn lightning_mode(&self) -> Option<LightningMode> {
396        Some(self.lightning_mode.clone())
397    }
398}
399
400#[derive(Debug, Serialize, Deserialize, Clone)]
401pub struct GetNodeInfoResponse {
402    pub pub_key: PublicKey,
403    pub alias: String,
404    pub network: String,
405    pub block_height: u32,
406    pub synced_to_chain: bool,
407}
408
409#[derive(Debug, Serialize, Deserialize, Clone)]
410pub struct InterceptPaymentRequest {
411    pub payment_hash: crate::sha256::Hash,
412    pub amount_msat: u64,
413    pub expiry: u32,
414    pub incoming_chan_id: u64,
415    pub short_channel_id: Option<u64>,
416    pub htlc_id: u64,
417}
418
419#[derive(Debug, Serialize, Deserialize, Clone)]
420pub struct InterceptPaymentResponse {
421    pub incoming_chan_id: u64,
422    pub htlc_id: u64,
423    pub payment_hash: crate::sha256::Hash,
424    pub action: PaymentAction,
425}
426
427#[derive(Debug, Serialize, Deserialize, Clone)]
428pub enum PaymentAction {
429    Settle(Preimage),
430    Cancel,
431    Forward,
432}
433
434#[derive(Debug, Serialize, Deserialize, Clone)]
435pub struct GetRouteHintsRequest {
436    pub num_route_hints: u64,
437}
438
439#[derive(Debug, Serialize, Deserialize, Clone)]
440pub struct GetRouteHintsResponse {
441    pub route_hints: Vec<RouteHint>,
442}
443
444#[derive(Debug, Serialize, Deserialize, Clone)]
445pub struct PayInvoiceRequest {
446    pub invoice: String,
447    pub max_delay: u64,
448    pub max_fee_msat: u64,
449    pub payment_hash: Vec<u8>,
450}
451
452#[derive(Debug, Serialize, Deserialize, Clone)]
453pub struct PayInvoiceResponse {
454    pub preimage: Preimage,
455}
456
457#[derive(Debug, Serialize, Deserialize, Clone)]
458pub struct PayPrunedInvoiceRequest {
459    pub pruned_invoice: Option<PrunedInvoice>,
460    pub max_delay: u64,
461    pub max_fee_msat: Amount,
462}
463
464#[derive(Debug, Serialize, Deserialize, Clone)]
465pub struct CreateInvoiceRequest {
466    pub payment_hash: Option<crate::sha256::Hash>,
467    pub amount_msat: u64,
468    pub expiry_secs: u32,
469    pub description: Option<InvoiceDescription>,
470}
471
472#[derive(Debug, Serialize, Deserialize, Clone)]
473pub enum InvoiceDescription {
474    Direct(String),
475    Hash(crate::sha256::Hash),
476}
477
478#[derive(Debug, Serialize, Deserialize, Clone)]
479pub struct CreateInvoiceResponse {
480    pub invoice: String,
481}
482
483#[derive(Debug, Serialize, Deserialize, Clone)]
484pub struct GetLnOnchainAddressResponse {
485    pub address: String,
486}
487
488#[derive(Debug, Serialize, Deserialize, Clone)]
489pub struct SendOnchainResponse {
490    pub txid: String,
491}
492
493#[derive(Debug, Serialize, Deserialize, Clone)]
494pub struct OpenChannelResponse {
495    pub funding_txid: String,
496}
497
498#[derive(Debug, Serialize, Deserialize, Clone)]
499pub struct CloseChannelsWithPeerResponse {
500    pub num_channels_closed: u32,
501}
502
503#[derive(Debug, Serialize, Deserialize, Clone)]
504pub struct ListActiveChannelsResponse {
505    pub channels: Vec<ChannelInfo>,
506}
507
508#[derive(Debug, Serialize, Deserialize, Clone)]
509pub struct GetBalancesResponse {
510    pub onchain_balance_sats: u64,
511    pub lightning_balance_msats: u64,
512    pub inbound_lightning_liquidity_msats: u64,
513}