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#[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#[async_trait]
89pub trait ILnRpcClient: Debug + Send + Sync {
90 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
92
93 async fn routehints(
98 &self,
99 num_route_hints: usize,
100 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
101
102 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 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 fn supports_private_payments(&self) -> bool {
158 false
159 }
160
161 async fn route_htlcs<'a>(
172 self: Box<Self>,
173 task_group: &TaskGroup,
174 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
175
176 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
180
181 async fn create_invoice(
186 &self,
187 create_invoice_request: CreateInvoiceRequest,
188 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
189
190 async fn get_ln_onchain_address(
193 &self,
194 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
195
196 async fn send_onchain(
199 &self,
200 payload: SendOnchainRequest,
201 ) -> Result<SendOnchainResponse, LightningRpcError>;
202
203 async fn open_channel(
205 &self,
206 payload: OpenChannelRequest,
207 ) -> Result<OpenChannelResponse, LightningRpcError>;
208
209 async fn close_channels_with_peer(
211 &self,
212 payload: CloseChannelsWithPeerRequest,
213 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
214
215 async fn list_active_channels(&self) -> Result<ListActiveChannelsResponse, LightningRpcError>;
217
218 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 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 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 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}