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