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