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#[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#[async_trait]
82pub trait ILnRpcClient: Debug + Send + Sync {
83 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError>;
85
86 async fn routehints(
91 &self,
92 num_route_hints: usize,
93 ) -> Result<GetRouteHintsResponse, LightningRpcError>;
94
95 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 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 fn supports_private_payments(&self) -> bool {
151 false
152 }
153
154 async fn route_htlcs<'a>(
165 self: Box<Self>,
166 task_group: &TaskGroup,
167 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError>;
168
169 async fn complete_htlc(&self, htlc: InterceptPaymentResponse) -> Result<(), LightningRpcError>;
173
174 async fn create_invoice(
179 &self,
180 create_invoice_request: CreateInvoiceRequest,
181 ) -> Result<CreateInvoiceResponse, LightningRpcError>;
182
183 async fn get_ln_onchain_address(
186 &self,
187 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError>;
188
189 async fn send_onchain(
192 &self,
193 payload: SendOnchainRequest,
194 ) -> Result<SendOnchainResponse, LightningRpcError>;
195
196 async fn open_channel(
198 &self,
199 payload: OpenChannelRequest,
200 ) -> Result<OpenChannelResponse, LightningRpcError>;
201
202 async fn close_channels_with_peer(
204 &self,
205 payload: CloseChannelsWithPeerRequest,
206 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError>;
207
208 async fn list_active_channels(&self) -> Result<ListActiveChannelsResponse, LightningRpcError>;
210
211 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError>;
214}
215
216impl dyn ILnRpcClient {
217 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 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 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#[async_trait]
405pub trait LightningV2Manager: Debug + Send + Sync {
406 async fn contains_incoming_contract(&self, payment_image: PaymentImage) -> bool;
407}