Skip to main content

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