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#[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#[async_trait]
91pub trait ILnRpcClient: Debug + Send + Sync {
92 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
94
95 async fn routehints(
100 &self,
101 num_route_hints: usize,
102 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
103
104 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 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 fn supports_private_payments(&self) -> bool {
160 false
161 }
162
163 async fn route_htlcs<'a>(
174 self: Box<Self>,
175 task_group: &TaskGroup,
176 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
177
178 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
182
183 async fn create_invoice(
188 &self,
189 create_invoice_request: CreateInvoiceRequest,
190 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
191
192 async fn get_ln_onchain_address(
195 &self,
196 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
197
198 async fn send_onchain(
201 &self,
202 payload: SendOnchainRequest,
203 ) -> Result<SendOnchainResponse, LightningRpcError>;
204
205 async fn open_channel(
207 &self,
208 payload: OpenChannelRequest,
209 ) -> Result<OpenChannelResponse, LightningRpcError>;
210
211 async fn close_channels_with_peer(
213 &self,
214 payload: CloseChannelsWithPeerRequest,
215 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
216
217 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
219
220 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 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 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 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
295 if is_env_var_set(FM_IN_DEVIMINT_ENV) {
299 self.sync_wallet()?;
300 }
301
302 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}