fedimint_testing/
ln.rs

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    /// Creates an invoice that is not payable
90    ///
91    /// * Mocks use hard-coded invoice description to fail the payment
92    /// * Real fixtures won't be able to route to randomly generated node pubkey
93    pub fn unpayable_invoice(&self, amount: Amount, expiry_time: Option<u64>) -> Bolt11Invoice {
94        let ctx = secp256k1::Secp256k1::new();
95        // Generate fake node keypair
96        let kp = Keypair::new(&ctx, &mut OsRng);
97        let payment_hash = sha256::Hash::hash(&MOCK_INVOICE_PREIMAGE);
98
99        // `FakeLightningTest` will fail to pay any invoice with
100        // `INVALID_INVOICE_DESCRIPTION` in the description of the invoice.
101        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        // `FakeLightningTest` will never intercept any HTLCs because there is no
198        // lightning connection, so instead we just create a stream that blocks
199        // until the task group is shutdown.
200        let (_, mut receiver) = mpsc::channel::<InterceptPaymentRequest>(0);
201        let stream: BoxStream<'a, InterceptPaymentRequest> = Box::pin(stream! {
202            shutdown_receiver.await;
203            // This block, and `receiver`, exist solely to satisfy the type checker.
204            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}