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