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