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