fedimint_lightning/
lib.rs

1pub mod ldk;
2pub mod lnd;
3
4use std::fmt::Debug;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use async_trait::async_trait;
9use bitcoin::Network;
10use bitcoin::hashes::sha256;
11use fedimint_core::Amount;
12use fedimint_core::encoding::{Decodable, Encodable};
13use fedimint_core::envs::{FM_IN_DEVIMINT_ENV, is_env_var_set};
14use fedimint_core::secp256k1::PublicKey;
15use fedimint_core::task::TaskGroup;
16use fedimint_core::util::{backoff_util, retry};
17use fedimint_gateway_common::{
18    ChannelInfo, CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, GetInvoiceRequest,
19    GetInvoiceResponse, LightningInfo, ListTransactionsResponse, OpenChannelRequest,
20    SendOnchainRequest,
21};
22use fedimint_ln_common::PrunedInvoice;
23pub use fedimint_ln_common::contracts::Preimage;
24use fedimint_ln_common::route_hints::RouteHint;
25use fedimint_logging::LOG_LIGHTNING;
26use futures::stream::BoxStream;
27use lightning_invoice::Bolt11Invoice;
28use serde::{Deserialize, Serialize};
29use thiserror::Error;
30use tracing::{info, warn};
31
32pub const MAX_LIGHTNING_RETRIES: u32 = 10;
33
34pub type RouteHtlcStream<'a> = BoxStream<'a, InterceptPaymentRequest>;
35
36#[derive(
37    Error, Debug, Serialize, Deserialize, Encodable, Decodable, Clone, Eq, PartialEq, Hash,
38)]
39pub enum LightningRpcError {
40    #[error("Failed to connect to Lightning node")]
41    FailedToConnect,
42    #[error("Failed to retrieve node info: {failure_reason}")]
43    FailedToGetNodeInfo { failure_reason: String },
44    #[error("Failed to retrieve route hints: {failure_reason}")]
45    FailedToGetRouteHints { failure_reason: String },
46    #[error("Payment failed: {failure_reason}")]
47    FailedPayment { failure_reason: String },
48    #[error("Failed to route HTLCs: {failure_reason}")]
49    FailedToRouteHtlcs { failure_reason: String },
50    #[error("Failed to complete HTLC: {failure_reason}")]
51    FailedToCompleteHtlc { failure_reason: String },
52    #[error("Failed to open channel: {failure_reason}")]
53    FailedToOpenChannel { failure_reason: String },
54    #[error("Failed to close channel: {failure_reason}")]
55    FailedToCloseChannelsWithPeer { failure_reason: String },
56    #[error("Failed to get Invoice: {failure_reason}")]
57    FailedToGetInvoice { failure_reason: String },
58    #[error("Failed to list transactions: {failure_reason}")]
59    FailedToListTransactions { failure_reason: String },
60    #[error("Failed to get funding address: {failure_reason}")]
61    FailedToGetLnOnchainAddress { failure_reason: String },
62    #[error("Failed to withdraw funds on-chain: {failure_reason}")]
63    FailedToWithdrawOnchain { failure_reason: String },
64    #[error("Failed to connect to peer: {failure_reason}")]
65    FailedToConnectToPeer { failure_reason: String },
66    #[error("Failed to list active channels: {failure_reason}")]
67    FailedToListChannels { failure_reason: String },
68    #[error("Failed to get balances: {failure_reason}")]
69    FailedToGetBalances { failure_reason: String },
70    #[error("Failed to sync to chain: {failure_reason}")]
71    FailedToSyncToChain { failure_reason: String },
72    #[error("Invalid metadata: {failure_reason}")]
73    InvalidMetadata { failure_reason: String },
74    #[error("Bolt12 Error: {failure_reason}")]
75    Bolt12Error { failure_reason: String },
76}
77
78/// Represents an active connection to the lightning node.
79#[derive(Clone, Debug)]
80pub struct LightningContext {
81    pub lnrpc: Arc<dyn ILnRpcClient>,
82    pub lightning_public_key: PublicKey,
83    pub lightning_alias: String,
84    pub lightning_network: Network,
85}
86
87/// A trait that the gateway uses to interact with a lightning node. This allows
88/// the gateway to be agnostic to the specific lightning node implementation
89/// being used.
90#[async_trait]
91pub trait ILnRpcClient: Debug + Send + Sync {
92    /// Returns high-level info about the lightning node.
93    async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
94
95    /// Returns route hints to the lightning node.
96    ///
97    /// Note: This is only used for inbound LNv1 payments and will be removed
98    /// when we switch to LNv2.
99    async fn routehints(
100        &self,
101        num_route_hints: usize,
102    ) -> Result<GetRouteHintsResponse, LightningRpcError>;
103
104    /// Attempts to pay an invoice using the lightning node, waiting for the
105    /// payment to complete and returning the preimage.
106    ///
107    /// Caller restrictions:
108    /// May be called multiple times for the same invoice, but _should_ be done
109    /// with all the same parameters. This is because the payment may be
110    /// in-flight from a previous call, in which case fee or delay limits cannot
111    /// be changed and will be ignored.
112    ///
113    /// Implementor restrictions:
114    /// This _must_ be idempotent for a given invoice, since it is called by
115    /// state machines. In more detail, when called for a given invoice:
116    /// * If the payment is already in-flight, wait for that payment to complete
117    ///   as if it were the first call.
118    /// * If the payment has already been attempted and failed, return an error.
119    /// * If the payment has already succeeded, return a success response.
120    async fn pay(
121        &self,
122        invoice: Bolt11Invoice,
123        max_delay: u64,
124        max_fee: Amount,
125    ) -> Result<PayInvoiceResponse, LightningRpcError> {
126        self.pay_private(
127            PrunedInvoice::try_from(invoice).map_err(|_| LightningRpcError::FailedPayment {
128                failure_reason: "Invoice has no amount".to_string(),
129            })?,
130            max_delay,
131            max_fee,
132        )
133        .await
134    }
135
136    /// Attempts to pay an invoice using the lightning node, waiting for the
137    /// payment to complete and returning the preimage.
138    ///
139    /// This is more private than [`ILnRpcClient::pay`], as it does not require
140    /// the invoice description. If this is implemented,
141    /// [`ILnRpcClient::supports_private_payments`] must return true.
142    ///
143    /// Note: This is only used for outbound LNv1 payments and will be removed
144    /// when we switch to LNv2.
145    async fn pay_private(
146        &self,
147        _invoice: PrunedInvoice,
148        _max_delay: u64,
149        _max_fee: Amount,
150    ) -> Result<PayInvoiceResponse, LightningRpcError> {
151        Err(LightningRpcError::FailedPayment {
152            failure_reason: "Private payments not supported".to_string(),
153        })
154    }
155
156    /// Returns true if the lightning backend supports payments without full
157    /// invoices. If this returns true, [`ILnRpcClient::pay_private`] must
158    /// be implemented.
159    fn supports_private_payments(&self) -> bool {
160        false
161    }
162
163    /// Consumes the current client and returns a stream of intercepted HTLCs
164    /// and a new client. `complete_htlc` must be called for all successfully
165    /// intercepted HTLCs sent to the returned stream.
166    ///
167    /// `route_htlcs` can only be called once for a given client, since the
168    /// returned stream grants exclusive routing decisions to the caller.
169    /// For this reason, `route_htlc` consumes the client and returns one
170    /// wrapped in an `Arc`. This lets the compiler enforce that `route_htlcs`
171    /// can only be called once for a given client, since the value inside
172    /// the `Arc` cannot be consumed.
173    async fn route_htlcs<'a>(
174        self: Box<Self>,
175        task_group: &TaskGroup,
176    ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
177
178    /// Completes an HTLC that was intercepted by the gateway. Must be called
179    /// for all successfully intercepted HTLCs sent to the stream returned
180    /// by `route_htlcs`.
181    async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
182
183    /// Requests the lightning node to create an invoice. The presence of a
184    /// payment hash in the `CreateInvoiceRequest` determines if the invoice is
185    /// intended to be an ecash payment or a direct payment to this lightning
186    /// node.
187    async fn create_invoice(
188        &self,
189        create_invoice_request: CreateInvoiceRequest,
190    ) -> Result<CreateInvoiceResponse, LightningRpcError>;
191
192    /// Gets a funding address belonging to the lightning node's on-chain
193    /// wallet.
194    async fn get_ln_onchain_address(
195        &self,
196    ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
197
198    /// Executes an onchain transaction using the lightning node's on-chain
199    /// wallet.
200    async fn send_onchain(
201        &self,
202        payload: SendOnchainRequest,
203    ) -> Result<SendOnchainResponse, LightningRpcError>;
204
205    /// Opens a channel with a peer lightning node.
206    async fn open_channel(
207        &self,
208        payload: OpenChannelRequest,
209    ) -> Result<OpenChannelResponse, LightningRpcError>;
210
211    /// Closes all channels with a peer lightning node.
212    async fn close_channels_with_peer(
213        &self,
214        payload: CloseChannelsWithPeerRequest,
215    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
216
217    /// Lists the lightning node's active channels with all peers.
218    async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
219
220    /// Returns a summary of the lightning node's balance, including the onchain
221    /// wallet, outbound liquidity, and inbound liquidity.
222    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
223
224    async fn get_invoice(
225        &self,
226        get_invoice_request: GetInvoiceRequest,
227    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError>;
228
229    async fn list_transactions(
230        &self,
231        start_secs: u64,
232        end_secs: u64,
233    ) -> Result<ListTransactionsResponse, LightningRpcError>;
234
235    fn create_offer(
236        &self,
237        amount: Option<Amount>,
238        description: Option<String>,
239        expiry_secs: Option<u32>,
240        quantity: Option<u64>,
241    ) -> Result<String, LightningRpcError>;
242
243    async fn pay_offer(
244        &self,
245        offer: String,
246        quantity: Option<u64>,
247        amount: Option<Amount>,
248        payer_note: Option<String>,
249    ) -> Result<Preimage, LightningRpcError>;
250
251    fn sync_wallet(&self) -> Result<(), LightningRpcError>;
252}
253
254impl dyn ILnRpcClient {
255    /// Retrieve route hints from the Lightning node, capped at
256    /// `num_route_hints`. The route hints should be ordered based on liquidity
257    /// of incoming channels.
258    pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
259        if num_route_hints == 0 {
260            return vec![];
261        }
262
263        let route_hints =
264            self.routehints(num_route_hints as usize)
265                .await
266                .unwrap_or(GetRouteHintsResponse {
267                    route_hints: Vec::new(),
268                });
269        route_hints.route_hints
270    }
271
272    /// Retrieves the basic information about the Gateway's connected Lightning
273    /// node.
274    pub async fn parsed_node_info(&self) -> LightningInfo {
275        if let Ok(info) = self.info().await
276            && let Ok(network) =
277                Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
278                    failure_reason: format!("Invalid network {}: {e}", info.network),
279                })
280        {
281            return LightningInfo::Connected {
282                public_key: info.pub_key,
283                alias: info.alias,
284                network,
285                block_height: info.block_height as u64,
286                synced_to_chain: info.synced_to_chain,
287            };
288        }
289
290        LightningInfo::NotConnected
291    }
292
293    /// Waits for the Lightning node to be synced to the Bitcoin blockchain.
294    pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
295        // In devimint, we explicitly sync the onchain wallet to start the sync quicker
296        // than background sync would. In production, background sync is
297        // sufficient
298        if is_env_var_set(FM_IN_DEVIMINT_ENV) {
299            self.sync_wallet()?;
300        }
301
302        // Wait for the Lightning node to sync
303        retry(
304            "Wait for chain sync",
305            backoff_util::background_backoff(),
306            || async {
307                let info = self.info().await?;
308                let block_height = info.block_height;
309                if info.synced_to_chain {
310                    Ok(())
311                } else {
312                    warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
313                    Err(anyhow::anyhow!("Not synced yet"))
314                }
315            },
316        )
317        .await
318        .map_err(|e| LightningRpcError::FailedToSyncToChain {
319            failure_reason: format!("Failed to sync to chain: {e:?}"),
320        })?;
321
322        info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
323        Ok(())
324    }
325}
326
327#[derive(Debug, Serialize, Deserialize, Clone)]
328pub struct GetNodeInfoResponse {
329    pub pub_key: PublicKey,
330    pub alias: String,
331    pub network: String,
332    pub block_height: u32,
333    pub synced_to_chain: bool,
334}
335
336#[derive(Debug, Serialize, Deserialize, Clone)]
337pub struct InterceptPaymentRequest {
338    pub payment_hash: sha256::Hash,
339    pub amount_msat: u64,
340    pub expiry: u32,
341    pub incoming_chan_id: u64,
342    pub short_channel_id: Option<u64>,
343    pub htlc_id: u64,
344}
345
346#[derive(Debug, Serialize, Deserialize, Clone)]
347pub struct InterceptPaymentResponse {
348    pub incoming_chan_id: u64,
349    pub htlc_id: u64,
350    pub payment_hash: sha256::Hash,
351    pub action: PaymentAction,
352}
353
354#[derive(Debug, Serialize, Deserialize, Clone)]
355pub enum PaymentAction {
356    Settle(Preimage),
357    Cancel,
358    Forward,
359}
360
361#[derive(Debug, Serialize, Deserialize, Clone)]
362pub struct GetRouteHintsResponse {
363    pub route_hints: Vec<RouteHint>,
364}
365
366#[derive(Debug, Serialize, Deserialize, Clone)]
367pub struct PayInvoiceResponse {
368    pub preimage: Preimage,
369}
370
371#[derive(Debug, Serialize, Deserialize, Clone)]
372pub struct CreateInvoiceRequest {
373    pub payment_hash: Option<sha256::Hash>,
374    pub amount_msat: u64,
375    pub expiry_secs: u32,
376    pub description: Option<InvoiceDescription>,
377}
378
379#[derive(Debug, Serialize, Deserialize, Clone)]
380pub enum InvoiceDescription {
381    Direct(String),
382    Hash(sha256::Hash),
383}
384
385#[derive(Debug, Serialize, Deserialize, Clone)]
386pub struct CreateInvoiceResponse {
387    pub invoice: String,
388}
389
390#[derive(Debug, Serialize, Deserialize, Clone)]
391pub struct GetLnOnchainAddressResponse {
392    pub address: String,
393}
394
395#[derive(Debug, Serialize, Deserialize, Clone)]
396pub struct SendOnchainResponse {
397    pub txid: String,
398}
399
400#[derive(Debug, Serialize, Deserialize, Clone)]
401pub struct OpenChannelResponse {
402    pub funding_txid: String,
403}
404
405#[derive(Debug, Serialize, Deserialize, Clone)]
406pub struct ListChannelsResponse {
407    pub channels: Vec<ChannelInfo>,
408}
409
410#[derive(Debug, Serialize, Deserialize, Clone)]
411pub struct GetBalancesResponse {
412    pub onchain_balance_sats: u64,
413    pub lightning_balance_msats: u64,
414    pub inbound_lightning_liquidity_msats: u64,
415}