1use std::fmt::{Display, Formatter};
2use std::str::FromStr;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::Duration;
6
7use async_stream::stream;
8use async_trait::async_trait;
9use bitcoin::hashes::{Hash, sha256};
10use bitcoin::key::Keypair;
11use bitcoin::secp256k1::{self, PublicKey, SecretKey};
12use fedimint_core::Amount;
13use fedimint_core::task::TaskGroup;
14use fedimint_core::util::BoxStream;
15use fedimint_gateway_common::{
16 CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, GetInvoiceRequest,
17 GetInvoiceResponse, OpenChannelRequest, SendOnchainRequest,
18};
19use fedimint_lightning::{
20 CreateInvoiceRequest, CreateInvoiceResponse, GetBalancesResponse, GetLnOnchainAddressResponse,
21 GetNodeInfoResponse, GetRouteHintsResponse, ILnRpcClient, InterceptPaymentRequest,
22 InterceptPaymentResponse, LightningRpcError, ListActiveChannelsResponse, OpenChannelResponse,
23 PayInvoiceResponse, RouteHtlcStream, SendOnchainResponse,
24};
25use fedimint_ln_common::PrunedInvoice;
26use fedimint_ln_common::contracts::Preimage;
27use fedimint_ln_common::route_hints::RouteHint;
28use fedimint_logging::LOG_TEST;
29use lightning_invoice::{
30 Bolt11Invoice, Currency, DEFAULT_EXPIRY_TIME, InvoiceBuilder, PaymentSecret,
31};
32use rand::rngs::OsRng;
33use tokio::sync::mpsc;
34use tracing::info;
35
36pub const INVALID_INVOICE_PAYMENT_SECRET: [u8; 32] = [212; 32];
37
38pub const MOCK_INVOICE_PREIMAGE: [u8; 32] = [1; 32];
39
40#[derive(Debug)]
41pub struct FakeLightningTest {
42 pub gateway_node_pub_key: secp256k1::PublicKey,
43 gateway_node_sec_key: secp256k1::SecretKey,
44 amount_sent: AtomicU64,
45}
46
47impl FakeLightningTest {
48 pub fn new() -> Self {
49 info!(target: LOG_TEST, "Setting up fake lightning test fixture");
50 let ctx = bitcoin::secp256k1::Secp256k1::new();
51 let kp = Keypair::new(&ctx, &mut OsRng);
52 let amount_sent = AtomicU64::new(0);
53
54 FakeLightningTest {
55 gateway_node_sec_key: SecretKey::from_keypair(&kp),
56 gateway_node_pub_key: PublicKey::from_keypair(&kp),
57 amount_sent,
58 }
59 }
60}
61
62impl Default for FakeLightningTest {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68impl FakeLightningTest {
69 pub fn invoice(
70 &self,
71 amount: Amount,
72 expiry_time: Option<u64>,
73 ) -> fedimint_gateway_server::Result<Bolt11Invoice> {
74 let ctx = bitcoin::secp256k1::Secp256k1::new();
75 let payment_hash = sha256::Hash::hash(&MOCK_INVOICE_PREIMAGE);
76
77 Ok(InvoiceBuilder::new(Currency::Regtest)
78 .description(String::new())
79 .payment_hash(payment_hash)
80 .current_timestamp()
81 .min_final_cltv_expiry_delta(0)
82 .payment_secret(PaymentSecret([0; 32]))
83 .amount_milli_satoshis(amount.msats)
84 .expiry_time(Duration::from_secs(
85 expiry_time.unwrap_or(DEFAULT_EXPIRY_TIME),
86 ))
87 .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &self.gateway_node_sec_key))
88 .unwrap())
89 }
90
91 pub fn unpayable_invoice(&self, amount: Amount, expiry_time: Option<u64>) -> Bolt11Invoice {
96 let ctx = secp256k1::Secp256k1::new();
97 let kp = Keypair::new(&ctx, &mut OsRng);
99 let payment_hash = sha256::Hash::hash(&MOCK_INVOICE_PREIMAGE);
100
101 InvoiceBuilder::new(Currency::Regtest)
104 .payee_pub_key(kp.public_key())
105 .description("INVALID INVOICE DESCRIPTION".to_string())
106 .payment_hash(payment_hash)
107 .current_timestamp()
108 .min_final_cltv_expiry_delta(0)
109 .payment_secret(PaymentSecret(INVALID_INVOICE_PAYMENT_SECRET))
110 .amount_milli_satoshis(amount.msats)
111 .expiry_time(Duration::from_secs(
112 expiry_time.unwrap_or(DEFAULT_EXPIRY_TIME),
113 ))
114 .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &SecretKey::from_keypair(&kp)))
115 .expect("Invoice creation failed")
116 }
117
118 pub fn listening_address(&self) -> String {
119 "FakeListeningAddress".to_string()
120 }
121}
122
123#[async_trait]
124impl ILnRpcClient for FakeLightningTest {
125 async fn info(&self) -> Result<GetNodeInfoResponse, LightningRpcError> {
126 Ok(GetNodeInfoResponse {
127 pub_key: self.gateway_node_pub_key,
128 alias: "FakeLightningNode".to_string(),
129 network: "regtest".to_string(),
130 block_height: 0,
131 synced_to_chain: false,
132 })
133 }
134
135 async fn routehints(
136 &self,
137 _num_route_hints: usize,
138 ) -> Result<GetRouteHintsResponse, LightningRpcError> {
139 Ok(GetRouteHintsResponse {
140 route_hints: vec![RouteHint(vec![])],
141 })
142 }
143
144 async fn pay(
145 &self,
146 invoice: Bolt11Invoice,
147 _max_delay: u64,
148 _max_fee: Amount,
149 ) -> Result<PayInvoiceResponse, LightningRpcError> {
150 self.amount_sent.fetch_add(
151 invoice
152 .amount_milli_satoshis()
153 .expect("Invoice missing amount"),
154 Ordering::Relaxed,
155 );
156
157 if *invoice.payment_secret() == PaymentSecret(INVALID_INVOICE_PAYMENT_SECRET) {
158 return Err(LightningRpcError::FailedPayment {
159 failure_reason: "Invoice was invalid".to_string(),
160 });
161 }
162
163 Ok(PayInvoiceResponse {
164 preimage: Preimage(MOCK_INVOICE_PREIMAGE),
165 })
166 }
167
168 fn supports_private_payments(&self) -> bool {
169 true
170 }
171
172 async fn pay_private(
173 &self,
174 invoice: PrunedInvoice,
175 _max_delay: u64,
176 _max_fee: Amount,
177 ) -> Result<PayInvoiceResponse, LightningRpcError> {
178 self.amount_sent
179 .fetch_add(invoice.amount.msats, Ordering::Relaxed);
180
181 if invoice.payment_secret == INVALID_INVOICE_PAYMENT_SECRET {
182 return Err(LightningRpcError::FailedPayment {
183 failure_reason: "Invoice was invalid".to_string(),
184 });
185 }
186
187 Ok(PayInvoiceResponse {
188 preimage: Preimage(MOCK_INVOICE_PREIMAGE),
189 })
190 }
191
192 async fn route_htlcs<'a>(
193 self: Box<Self>,
194 task_group: &TaskGroup,
195 ) -> Result<(RouteHtlcStream<'a>, Arc<dyn ILnRpcClient>), LightningRpcError> {
196 let handle = task_group.make_handle();
197 let shutdown_receiver = handle.make_shutdown_rx();
198
199 let (_, mut receiver) = mpsc::channel::<InterceptPaymentRequest>(0);
203 let stream: BoxStream<'a, InterceptPaymentRequest> = Box::pin(stream! {
204 shutdown_receiver.await;
205 if let Some(htlc_result) = receiver.recv().await {
207 yield htlc_result;
208 }
209 });
210 Ok((stream, Arc::new(Self::new())))
211 }
212
213 async fn complete_htlc(
214 &self,
215 _htlc: InterceptPaymentResponse,
216 ) -> Result<(), LightningRpcError> {
217 Ok(())
218 }
219
220 async fn create_invoice(
221 &self,
222 create_invoice_request: CreateInvoiceRequest,
223 ) -> Result<CreateInvoiceResponse, LightningRpcError> {
224 let ctx = secp256k1::Secp256k1::new();
225
226 let invoice = match create_invoice_request.payment_hash {
227 Some(payment_hash) => InvoiceBuilder::new(Currency::Regtest)
228 .description(String::new())
229 .payment_hash(payment_hash)
230 .current_timestamp()
231 .min_final_cltv_expiry_delta(0)
232 .payment_secret(PaymentSecret([0; 32]))
233 .amount_milli_satoshis(create_invoice_request.amount_msat)
234 .expiry_time(Duration::from_secs(u64::from(
235 create_invoice_request.expiry_secs,
236 )))
237 .build_signed(|m| ctx.sign_ecdsa_recoverable(m, &self.gateway_node_sec_key))
238 .unwrap(),
239 None => {
240 return Err(LightningRpcError::FailedToGetInvoice {
241 failure_reason: "FakeLightningTest does not support creating invoices without a payment hash".to_string(),
242 });
243 }
244 };
245
246 Ok(CreateInvoiceResponse {
247 invoice: invoice.to_string(),
248 })
249 }
250
251 async fn get_ln_onchain_address(
252 &self,
253 ) -> Result<GetLnOnchainAddressResponse, LightningRpcError> {
254 Err(LightningRpcError::FailedToGetLnOnchainAddress {
255 failure_reason: "FakeLightningTest does not support getting a funding address"
256 .to_string(),
257 })
258 }
259
260 async fn send_onchain(
261 &self,
262 _payload: SendOnchainRequest,
263 ) -> Result<SendOnchainResponse, LightningRpcError> {
264 Err(LightningRpcError::FailedToWithdrawOnchain {
265 failure_reason: "FakeLightningTest does not support withdrawing funds on-chain"
266 .to_string(),
267 })
268 }
269
270 async fn open_channel(
271 &self,
272 _payload: OpenChannelRequest,
273 ) -> Result<OpenChannelResponse, LightningRpcError> {
274 Err(LightningRpcError::FailedToOpenChannel {
275 failure_reason: "FakeLightningTest does not support opening channels".to_string(),
276 })
277 }
278
279 async fn close_channels_with_peer(
280 &self,
281 _payload: CloseChannelsWithPeerRequest,
282 ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
283 Err(LightningRpcError::FailedToCloseChannelsWithPeer {
284 failure_reason: "FakeLightningTest does not support closing channels by peer"
285 .to_string(),
286 })
287 }
288
289 async fn list_active_channels(&self) -> Result<ListActiveChannelsResponse, LightningRpcError> {
290 Err(LightningRpcError::FailedToListActiveChannels {
291 failure_reason: "FakeLightningTest does not support listing active channels"
292 .to_string(),
293 })
294 }
295
296 async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
297 Ok(GetBalancesResponse {
298 onchain_balance_sats: 0,
299 lightning_balance_msats: 0,
300 inbound_lightning_liquidity_msats: 0,
301 })
302 }
303
304 async fn get_invoice(
305 &self,
306 _get_invoice_request: GetInvoiceRequest,
307 ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
308 Err(LightningRpcError::FailedToGetInvoice {
309 failure_reason: "FakeLightningTest does not support getting invoices".to_string(),
310 })
311 }
312}
313
314#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
315pub enum LightningNodeType {
316 Lnd,
317 Ldk,
318}
319
320impl Display for LightningNodeType {
321 fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
322 match self {
323 LightningNodeType::Lnd => write!(f, "lnd"),
324 LightningNodeType::Ldk => write!(f, "ldk"),
325 }
326 }
327}
328
329impl FromStr for LightningNodeType {
330 type Err = String;
331
332 fn from_str(s: &str) -> Result<Self, Self::Err> {
333 match s.to_lowercase().as_str() {
334 "lnd" => Ok(LightningNodeType::Lnd),
335 "ldk" => Ok(LightningNodeType::Ldk),
336 _ => Err(format!("Invalid value for LightningNodeType: {s}")),
337 }
338 }
339}