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 fn sync_wallet(&self) -> Result<(), LightningRpcError>;
251}
252
253impl dyn ILnRpcClient {
254 pub async fn parsed_route_hints(&self, num_route_hints: u32) -> Vec<RouteHint> {
258 if num_route_hints == 0 {
259 return vec![];
260 }
261
262 let route_hints =
263 self.routehints(num_route_hints as usize)
264 .await
265 .unwrap_or(GetRouteHintsResponse {
266 route_hints: Vec::new(),
267 });
268 route_hints.route_hints
269 }
270
271 pub async fn parsed_node_info(&self) -> LightningInfo {
274 if let Ok(info) = self.info().await
275 && let Ok(network) =
276 Network::from_str(&info.network).map_err(|e| LightningRpcError::InvalidMetadata {
277 failure_reason: format!("Invalid network {}: {e}", info.network),
278 })
279 {
280 return LightningInfo::Connected {
281 public_key: info.pub_key,
282 alias: info.alias,
283 network,
284 block_height: info.block_height as u64,
285 synced_to_chain: info.synced_to_chain,
286 };
287 }
288
289 LightningInfo::NotConnected
290 }
291
292 pub async fn wait_for_chain_sync(&self) -> std::result::Result<(), LightningRpcError> {
294 self.sync_wallet()?;
295
296 retry(
298 "Wait for chain sync",
299 backoff_util::background_backoff(),
300 || async {
301 let info = self.info().await?;
302 let block_height = info.block_height;
303 if info.synced_to_chain {
304 Ok(())
305 } else {
306 warn!(target: LOG_LIGHTNING, block_height = %block_height, "Lightning node is not synced yet");
307 Err(anyhow::anyhow!("Not synced yet"))
308 }
309 },
310 )
311 .await
312 .map_err(|e| LightningRpcError::FailedToSyncToChain {
313 failure_reason: format!("Failed to sync to chain: {e:?}"),
314 })?;
315
316 info!(target: LOG_LIGHTNING, "Gateway successfully synced with the chain");
317 Ok(())
318 }
319}
320
321#[derive(Debug, Serialize, Deserialize, Clone)]
322pub struct GetNodeInfoResponse {
323 pub pub_key: PublicKey,
324 pub alias: String,
325 pub network: String,
326 pub block_height: u32,
327 pub synced_to_chain: bool,
328}
329
330#[derive(Debug, Serialize, Deserialize, Clone)]
331pub struct InterceptPaymentRequest {
332 pub payment_hash: sha256::Hash,
333 pub amount_msat: u64,
334 pub expiry: u32,
335 pub incoming_chan_id: u64,
336 pub short_channel_id: Option<u64>,
337 pub htlc_id: u64,
338}
339
340#[derive(Debug, Serialize, Deserialize, Clone)]
341pub struct InterceptPaymentResponse {
342 pub incoming_chan_id: u64,
343 pub htlc_id: u64,
344 pub payment_hash: sha256::Hash,
345 pub action: PaymentAction,
346}
347
348#[derive(Debug, Serialize, Deserialize, Clone)]
349pub enum PaymentAction {
350 Settle(Preimage),
351 Cancel,
352 Forward,
353}
354
355#[derive(Debug, Serialize, Deserialize, Clone)]
356pub struct GetRouteHintsResponse {
357 pub route_hints: Vec<RouteHint>,
358}
359
360#[derive(Debug, Serialize, Deserialize, Clone)]
361pub struct PayInvoiceResponse {
362 pub preimage: Preimage,
363}
364
365#[derive(Debug, Serialize, Deserialize, Clone)]
366pub struct CreateInvoiceRequest {
367 pub payment_hash: Option<sha256::Hash>,
368 pub amount_msat: u64,
369 pub expiry_secs: u32,
370 pub description: Option<InvoiceDescription>,
371}
372
373#[derive(Debug, Serialize, Deserialize, Clone)]
374pub enum InvoiceDescription {
375 Direct(String),
376 Hash(sha256::Hash),
377}
378
379#[derive(Debug, Serialize, Deserialize, Clone)]
380pub struct CreateInvoiceResponse {
381 pub invoice: String,
382}
383
384#[derive(Debug, Serialize, Deserialize, Clone)]
385pub struct GetLnOnchainAddressResponse {
386 pub address: String,
387}
388
389#[derive(Debug, Serialize, Deserialize, Clone)]
390pub struct SendOnchainResponse {
391 pub txid: String,
392}
393
394#[derive(Debug, Serialize, Deserialize, Clone)]
395pub struct OpenChannelResponse {
396 pub funding_txid: String,
397}
398
399#[derive(Debug, Serialize, Deserialize, Clone)]
400pub struct ListChannelsResponse {
401 pub channels: Vec<ChannelInfo>,
402}
403
404#[derive(Debug, Serialize, Deserialize, Clone)]
405pub struct GetBalancesResponse {
406 pub onchain_balance_sats: u64,
407 pub lightning_balance_msats: u64,
408 pub inbound_lightning_liquidity_msats: u64,
409}