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, ConnectPeerRequest,
15    GetInvoiceRequest, GetInvoiceResponse, ListTransactionsResponse, OpenChannelRequest,
16    SendOnchainRequest, 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 connect_peer(&self, _payload: ConnectPeerRequest) -> Result<(), LightningRpcError> {
279        Err(LightningRpcError::FailedToConnectToPeer {
280            failure_reason: "FakeLightningTest does not support connecting to peers".to_string(),
281        })
282    }
283
284    async fn close_channels_with_peer(
285        &self,
286        _payload: CloseChannelsWithPeerRequest,
287    ) -> Result<CloseChannelsWithPeerResponse, LightningRpcError> {
288        Err(LightningRpcError::FailedToCloseChannelsWithPeer {
289            failure_reason: "FakeLightningTest does not support closing channels by peer"
290                .to_string(),
291        })
292    }
293
294    async fn list_channels(&self) -> Result<ListChannelsResponse, LightningRpcError> {
295        Err(LightningRpcError::FailedToListChannels {
296            failure_reason: "FakeLightningTest does not support listing active channels"
297                .to_string(),
298        })
299    }
300
301    async fn set_channel_fees(
302        &self,
303        _payload: SetChannelFeesRequest,
304    ) -> Result<(), LightningRpcError> {
305        Err(LightningRpcError::FailedToSetChannelFees {
306            failure_reason: "FakeLightningTest does not support updating channel fees".to_string(),
307        })
308    }
309
310    async fn get_balances(&self) -> Result<GetBalancesResponse, LightningRpcError> {
311        Ok(GetBalancesResponse {
312            onchain_balance_sats: 0,
313            lightning_balance_msats: 0,
314            inbound_lightning_liquidity_msats: 0,
315        })
316    }
317
318    async fn get_invoice(
319        &self,
320        _get_invoice_request: GetInvoiceRequest,
321    ) -> Result<Option<GetInvoiceResponse>, LightningRpcError> {
322        Err(LightningRpcError::FailedToGetInvoice {
323            failure_reason: "FakeLightningTest does not support getting invoices".to_string(),
324        })
325    }
326
327    async fn list_transactions(
328        &self,
329        _start_secs: u64,
330        _end_secs: u64,
331    ) -> Result<ListTransactionsResponse, LightningRpcError> {
332        Err(LightningRpcError::FailedToListTransactions {
333            failure_reason: "FakeLightningTest does not support listing transactions".to_string(),
334        })
335    }
336
337    fn create_offer(
338        &self,
339        _amount_msat: Option<Amount>,
340        _description: Option<String>,
341        _expiry_secs: Option<u32>,
342        _quantity: Option<u64>,
343    ) -> Result<String, LightningRpcError> {
344        Err(LightningRpcError::Bolt12Error {
345            failure_reason: "FakeLightningTest does not support Bolt12".to_string(),
346        })
347    }
348
349    async fn pay_offer(
350        &self,
351        _offer: String,
352        _quantity: Option<u64>,
353        _amount: Option<Amount>,
354        _payer_note: Option<String>,
355    ) -> Result<Preimage, LightningRpcError> {
356        Err(LightningRpcError::Bolt12Error {
357            failure_reason: "FakeLightningTest does not support Bolt12".to_string(),
358        })
359    }
360
361    fn sync_wallet(&self) -> Result<(), LightningRpcError> {
362        Ok(())
363    }
364}