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#[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#[async_trait]
90pub trait ILnRpcClient: Debug + Send + Sync {
91 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
93
94 async fn routehints(
99 &self,
100 num_route_hints: usize,
101 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
102
103 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 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 fn supports_private_payments(&self) -> bool {
159 false
160 }
161
162 async fn route_htlcs<'a>(
173 self: Box<Self>,
174 task_group: &TaskGroup,
175 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
176
177 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
181
182 async fn create_invoice(
187 &self,
188 create_invoice_request: CreateInvoiceRequest,
189 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
190
191 async fn get_ln_onchain_address(
194 &self,
195 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
196
197 async fn send_onchain(
200 &self,
201 payload: SendOnchainRequest,
202 ) -> Result<SendOnchainResponse, LightningRpcError>;
203
204 async fn open_channel(
206 &self,
207 payload: OpenChannelRequest,
208 ) -> Result<OpenChannelResponse, LightningRpcError>;
209
210 async fn close_channels_with_peer(
212 &self,
213 payload: CloseChannelsWithPeerRequest,
214 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
215
216 async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError>;
218
219 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 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 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 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}