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