fedimint_wasm_tests/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::large_futures)]
3#![allow(dead_code)]
4#![allow(clippy::literal_string_with_formatting_args)]
5
6use std::sync::Arc;
7
8use anyhow::Result;
9use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy};
10use fedimint_client::{Client, RootSecret};
11use fedimint_connectors::ConnectorRegistry;
12use fedimint_core::db::Database;
13use fedimint_core::db::mem_impl::MemDatabase;
14use fedimint_core::invite_code::InviteCode;
15use fedimint_ln_client::{LightningClientInit, LightningClientModule};
16use fedimint_mint_client::MintClientInit;
17use fedimint_wallet_client::WalletClientInit;
18use rand::thread_rng;
19
20async fn load_or_generate_mnemonic(db: &Database) -> anyhow::Result<[u8; 64]> {
21    Ok(
22        if let Ok(s) = Client::load_decodable_client_secret(db).await {
23            s
24        } else {
25            let secret = PlainRootSecretStrategy::random(&mut thread_rng());
26            Client::store_encodable_client_secret(db, secret).await?;
27            secret
28        },
29    )
30}
31
32async fn make_client_builder() -> Result<(fedimint_client::ClientBuilder, Database)> {
33    let mem_database = MemDatabase::default();
34    let mut builder = fedimint_client::Client::builder().await?;
35    builder.with_module(LightningClientInit::default());
36    builder.with_module(MintClientInit);
37    builder.with_module(WalletClientInit::default());
38
39    Ok((builder, mem_database.into()))
40}
41
42async fn client(invite_code: &InviteCode) -> Result<fedimint_client::ClientHandleArc> {
43    let (mut builder, db) = make_client_builder().await?;
44    let client_secret = load_or_generate_mnemonic(&db).await?;
45    let connectors = ConnectorRegistry::build_from_testing_defaults()
46        .bind()
47        .await?;
48    builder.stopped();
49    let client = builder
50        .preview(connectors, invite_code)
51        .await?
52        .join(
53            db,
54            RootSecret::StandardDoubleDerive(PlainRootSecretStrategy::to_root_secret(
55                &client_secret,
56            )),
57        )
58        .await
59        .map(Arc::new)?;
60    if let Ok(ln_client) = client.get_first_module::<LightningClientModule>() {
61        let _ = ln_client.update_gateway_cache().await;
62    }
63    Ok(client)
64}
65
66mod faucet {
67    pub async fn invite_code() -> anyhow::Result<String> {
68        let resp = gloo_net::http::Request::get("http://localhost:15243/connect-string")
69            .send()
70            .await?;
71        if resp.ok() {
72            Ok(resp.text().await?)
73        } else {
74            anyhow::bail!(resp.text().await?);
75        }
76    }
77
78    pub async fn pay_invoice(invoice: &str) -> anyhow::Result<()> {
79        let resp = gloo_net::http::Request::post("http://localhost:15243/pay")
80            .body(invoice)?
81            .send()
82            .await?;
83        if resp.ok() {
84            Ok(())
85        } else {
86            anyhow::bail!(resp.text().await?);
87        }
88    }
89
90    pub async fn gateway_api() -> anyhow::Result<String> {
91        let resp = gloo_net::http::Request::get("http://localhost:15243/gateway-api")
92            .send()
93            .await?;
94        if resp.ok() {
95            Ok(resp.text().await?)
96        } else {
97            anyhow::bail!(resp.text().await?);
98        }
99    }
100
101    pub async fn generate_invoice(amt: u64) -> anyhow::Result<String> {
102        let resp = gloo_net::http::Request::post("http://localhost:15243/invoice")
103            .body(amt)?
104            .send()
105            .await?;
106        if resp.ok() {
107            Ok(resp.text().await?)
108        } else {
109            anyhow::bail!(resp.text().await?);
110        }
111    }
112}
113
114wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
115mod tests {
116    use std::time::Duration;
117
118    use anyhow::{anyhow, bail};
119    use fedimint_core::Amount;
120    use fedimint_derive_secret::DerivableSecret;
121    use fedimint_ln_client::{
122        LightningClientModule, LnPayState, LnReceiveState, OutgoingLightningPayment, PayType,
123    };
124    use fedimint_ln_common::LightningGateway;
125    use fedimint_ln_common::lightning_invoice::{Bolt11InvoiceDescription, Description};
126    use fedimint_mint_client::{
127        MintClientModule, ReissueExternalNotesState, SelectNotesWithAtleastAmount, SpendOOBState,
128    };
129    use futures::StreamExt;
130    use wasm_bindgen_test::wasm_bindgen_test;
131
132    use super::{Result, client, faucet};
133
134    #[wasm_bindgen_test]
135    async fn build_client() -> Result<()> {
136        let _client = client(&faucet::invite_code().await?.parse()?).await?;
137        Ok(())
138    }
139
140    async fn get_gateway(
141        client: &fedimint_client::ClientHandleArc,
142    ) -> anyhow::Result<LightningGateway> {
143        let lightning_module = client.get_first_module::<LightningClientModule>()?;
144        let gws = lightning_module.list_gateways().await;
145        let gw_api = faucet::gateway_api().await?;
146        let lnd_gw = gws
147            .into_iter()
148            .find(|x| x.info.api.to_string() == gw_api)
149            .expect("no gateway with api");
150
151        Ok(lnd_gw.info)
152    }
153
154    #[wasm_bindgen_test]
155    async fn receive() -> Result<()> {
156        let client = client(&faucet::invite_code().await?.parse()?).await?;
157        client.start_executor();
158        let ln_gateway = get_gateway(&client).await?;
159        futures::future::try_join_all(
160            (0..10)
161                .map(|_| receive_once(client.clone(), Amount::from_sats(21), ln_gateway.clone())),
162        )
163        .await?;
164        Ok(())
165    }
166
167    async fn receive_once(
168        client: fedimint_client::ClientHandleArc,
169        amount: Amount,
170        gateway: LightningGateway,
171    ) -> Result<()> {
172        let lightning_module = client.get_first_module::<LightningClientModule>()?;
173        let desc = Description::new("test".to_string())?;
174        let (opid, invoice, _) = lightning_module
175            .create_bolt11_invoice(
176                amount,
177                Bolt11InvoiceDescription::Direct(desc),
178                None,
179                (),
180                Some(gateway),
181            )
182            .await?;
183        faucet::pay_invoice(&invoice.to_string()).await?;
184
185        let mut updates = lightning_module
186            .subscribe_ln_receive(opid)
187            .await?
188            .into_stream();
189        while let Some(update) = updates.next().await {
190            match update {
191                LnReceiveState::Claimed => return Ok(()),
192                LnReceiveState::Canceled { reason } => {
193                    return Err(reason.into());
194                }
195                _ => {}
196            }
197        }
198        Err(anyhow!("Lightning receive failed"))
199    }
200
201    // Tests that ChaCha20 crypto functions used for backup and recovery are
202    // available in WASM at runtime. Related issue: https://github.com/fedimint/fedimint/issues/2843
203    #[wasm_bindgen_test]
204    #[allow(clippy::unused_async)]
205    async fn derive_chacha_key() {
206        let root_secret = DerivableSecret::new_root(&[0x42; 32], &[0x2a; 32]);
207        let key = root_secret.to_chacha20_poly1305_key();
208
209        // Prevent optimization
210        // FIXME: replace with `std::hint::black_box` once stabilized
211        assert!(format!("key: {key:?}").len() > 8);
212    }
213
214    async fn pay_once(
215        client: fedimint_client::ClientHandleArc,
216        ln_gateway: LightningGateway,
217    ) -> Result<(), anyhow::Error> {
218        let lightning_module = client.get_first_module::<LightningClientModule>()?;
219        let bolt11 = faucet::generate_invoice(11).await?;
220        let OutgoingLightningPayment {
221            payment_type,
222            contract_id: _,
223            fee: _,
224        } = lightning_module
225            .pay_bolt11_invoice(Some(ln_gateway), bolt11.parse()?, ())
226            .await?;
227        let PayType::Lightning(operation_id) = payment_type else {
228            unreachable!("paying invoice over lightning");
229        };
230        let lightning_module = client.get_first_module::<LightningClientModule>()?;
231        let mut updates = lightning_module
232            .subscribe_ln_pay(operation_id)
233            .await?
234            .into_stream();
235        loop {
236            match updates.next().await {
237                Some(LnPayState::Success { preimage: _ }) => {
238                    break;
239                }
240                Some(LnPayState::Refunded { gateway_error }) => {
241                    return Err(anyhow!("refunded {gateway_error}"));
242                }
243                None => return Err(anyhow!("Lightning send failed")),
244                _ => {}
245            }
246        }
247        Ok(())
248    }
249
250    #[wasm_bindgen_test]
251    async fn receive_and_pay() -> Result<()> {
252        let client = client(&faucet::invite_code().await?.parse()?).await?;
253        client.start_executor();
254        let ln_gateway = get_gateway(&client).await?;
255
256        futures::future::try_join_all(
257            (0..10)
258                .map(|_| receive_once(client.clone(), Amount::from_sats(21), ln_gateway.clone())),
259        )
260        .await?;
261        futures::future::try_join_all(
262            (0..10).map(|_| pay_once(client.clone(), ln_gateway.clone())),
263        )
264        .await?;
265
266        Ok(())
267    }
268
269    async fn send_and_recv_ecash_once(
270        client: fedimint_client::ClientHandleArc,
271    ) -> Result<(), anyhow::Error> {
272        let mint = client.get_first_module::<MintClientModule>()?;
273        let (_, notes) = mint
274            .spend_notes_with_selector(
275                &SelectNotesWithAtleastAmount,
276                Amount::from_sats(11),
277                Duration::from_secs(10000),
278                false,
279                (),
280            )
281            .await?;
282        let operation_id = mint.reissue_external_notes(notes, ()).await?;
283        let mut updates = mint
284            .subscribe_reissue_external_notes(operation_id)
285            .await?
286            .into_stream();
287        loop {
288            match updates.next().await {
289                Some(ReissueExternalNotesState::Done) => {
290                    break;
291                }
292                Some(ReissueExternalNotesState::Failed(error)) => {
293                    return Err(anyhow!("reissue failed {error}"));
294                }
295                Some(_) => {}
296                None => return Err(anyhow!("reissue failed")),
297            }
298        }
299        Ok(())
300    }
301
302    async fn send_ecash_exact(
303        client: fedimint_client::ClientHandleArc,
304        amount: Amount,
305    ) -> Result<(), anyhow::Error> {
306        let mint = client.get_first_module::<MintClientModule>()?;
307        'retry: loop {
308            let (operation_id, notes) = mint
309                .spend_notes_with_selector(
310                    &SelectNotesWithAtleastAmount,
311                    amount,
312                    Duration::from_secs(10000),
313                    false,
314                    (),
315                )
316                .await?;
317            if notes.total_amount() == amount {
318                return Ok(());
319            }
320            mint.try_cancel_spend_notes(operation_id).await;
321            let mut updates = mint
322                .subscribe_spend_notes(operation_id)
323                .await?
324                .into_stream();
325            while let Some(update) = updates.next().await {
326                if update == SpendOOBState::UserCanceledSuccess {
327                    continue 'retry;
328                }
329            }
330            bail!("failed to cancel notes");
331        }
332    }
333
334    #[wasm_bindgen_test]
335    async fn test_ecash() -> Result<()> {
336        let client = client(&faucet::invite_code().await?.parse()?).await?;
337        client.start_executor();
338        let ln_gateway = get_gateway(&client).await?;
339
340        futures::future::try_join_all(
341            (0..10)
342                .map(|_| receive_once(client.clone(), Amount::from_sats(100), ln_gateway.clone())), // Increased from 25 to 100 sats to account for mint fees
343        )
344        .await?;
345        futures::future::try_join_all((0..10).map(|_| send_and_recv_ecash_once(client.clone())))
346            .await?;
347        Ok(())
348    }
349
350    #[wasm_bindgen_test]
351    async fn test_ecash_exact() -> Result<()> {
352        let client = client(&faucet::invite_code().await?.parse()?).await?;
353        client.start_executor();
354        let ln_gateway = get_gateway(&client).await?;
355
356        receive_once(client.clone(), Amount::from_sats(100), ln_gateway).await?;
357        futures::future::try_join_all(
358            (0..3).map(|_| send_ecash_exact(client.clone(), Amount::from_sats(1))),
359        )
360        .await?;
361        Ok(())
362    }
363}