fedimint_wasm_tests/
lib.rs

1#![deny(clippy::pedantic)]
2#![allow(clippy::large_futures)]
3
4use std::sync::Arc;
5
6use anyhow::Result;
7use fedimint_client::Client;
8use fedimint_client::secret::{PlainRootSecretStrategy, RootSecretStrategy};
9use fedimint_core::db::Database;
10use fedimint_core::db::mem_impl::MemDatabase;
11use fedimint_core::invite_code::InviteCode;
12use fedimint_ln_client::{LightningClientInit, LightningClientModule};
13use fedimint_mint_client::MintClientInit;
14use fedimint_wallet_client::WalletClientInit;
15use rand::thread_rng;
16
17async fn load_or_generate_mnemonic(db: &Database) -> anyhow::Result<[u8; 64]> {
18    Ok(
19        if let Ok(s) = Client::load_decodable_client_secret(db).await {
20            s
21        } else {
22            let secret = PlainRootSecretStrategy::random(&mut thread_rng());
23            Client::store_encodable_client_secret(db, secret).await?;
24            secret
25        },
26    )
27}
28
29async fn make_client_builder() -> Result<fedimint_client::ClientBuilder> {
30    let mem_database = MemDatabase::default();
31    let mut builder = fedimint_client::Client::builder(mem_database.into()).await?;
32    builder.with_module(LightningClientInit::default());
33    builder.with_module(MintClientInit);
34    builder.with_module(WalletClientInit::default());
35    builder.with_primary_module_kind(fedimint_mint_client::KIND);
36
37    Ok(builder)
38}
39
40async fn client(invite_code: &InviteCode) -> Result<fedimint_client::ClientHandleArc> {
41    let client_config = fedimint_api_client::api::net::Connector::default()
42        .download_from_invite_code(invite_code)
43        .await?;
44    let mut builder = make_client_builder().await?;
45    let client_secret = load_or_generate_mnemonic(builder.db_no_decoders()).await?;
46    builder.stopped();
47    let client = builder
48        .join(
49            PlainRootSecretStrategy::to_root_secret(&client_secret),
50            client_config.clone(),
51            None,
52        )
53        .await
54        .map(Arc::new)?;
55    if let Ok(ln_client) = client.get_first_module::<LightningClientModule>() {
56        let _ = ln_client.update_gateway_cache().await;
57    }
58    Ok(client)
59}
60
61mod faucet {
62    pub async fn invite_code() -> anyhow::Result<String> {
63        let resp = gloo_net::http::Request::get("http://localhost:15243/connect-string")
64            .send()
65            .await?;
66        if resp.ok() {
67            Ok(resp.text().await?)
68        } else {
69            anyhow::bail!(resp.text().await?);
70        }
71    }
72
73    pub async fn pay_invoice(invoice: &str) -> anyhow::Result<()> {
74        let resp = gloo_net::http::Request::post("http://localhost:15243/pay")
75            .body(invoice)?
76            .send()
77            .await?;
78        if resp.ok() {
79            Ok(())
80        } else {
81            anyhow::bail!(resp.text().await?);
82        }
83    }
84
85    pub async fn gateway_api() -> anyhow::Result<String> {
86        let resp = gloo_net::http::Request::get("http://localhost:15243/gateway-api")
87            .send()
88            .await?;
89        if resp.ok() {
90            Ok(resp.text().await?)
91        } else {
92            anyhow::bail!(resp.text().await?);
93        }
94    }
95
96    pub async fn generate_invoice(amt: u64) -> anyhow::Result<String> {
97        let resp = gloo_net::http::Request::post("http://localhost:15243/invoice")
98            .body(amt)?
99            .send()
100            .await?;
101        if resp.ok() {
102            Ok(resp.text().await?)
103        } else {
104            anyhow::bail!(resp.text().await?);
105        }
106    }
107}
108
109wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
110mod tests {
111    use std::time::Duration;
112
113    use anyhow::{anyhow, bail};
114    use fedimint_core::Amount;
115    use fedimint_derive_secret::DerivableSecret;
116    use fedimint_ln_client::{
117        LightningClientModule, LnPayState, LnReceiveState, OutgoingLightningPayment, PayType,
118    };
119    use fedimint_ln_common::LightningGateway;
120    use fedimint_ln_common::lightning_invoice::{Bolt11InvoiceDescription, Description};
121    use fedimint_mint_client::{
122        MintClientModule, ReissueExternalNotesState, SelectNotesWithAtleastAmount, SpendOOBState,
123    };
124    use futures::StreamExt;
125    use wasm_bindgen_test::wasm_bindgen_test;
126
127    use super::{Result, client, faucet};
128
129    #[wasm_bindgen_test]
130    async fn build_client() -> Result<()> {
131        let _client = client(&faucet::invite_code().await?.parse()?).await?;
132        Ok(())
133    }
134
135    async fn get_gateway(
136        client: &fedimint_client::ClientHandleArc,
137    ) -> anyhow::Result<LightningGateway> {
138        let lightning_module = client.get_first_module::<LightningClientModule>()?;
139        let gws = lightning_module.list_gateways().await;
140        let gw_api = faucet::gateway_api().await?;
141        let lnd_gw = gws
142            .into_iter()
143            .find(|x| x.info.api.to_string() == gw_api)
144            .expect("no gateway with api");
145
146        Ok(lnd_gw.info)
147    }
148
149    #[wasm_bindgen_test]
150    async fn receive() -> Result<()> {
151        let client = client(&faucet::invite_code().await?.parse()?).await?;
152        client.start_executor();
153        let ln_gateway = get_gateway(&client).await?;
154        futures::future::try_join_all(
155            (0..10)
156                .map(|_| receive_once(client.clone(), Amount::from_sats(21), ln_gateway.clone())),
157        )
158        .await?;
159        Ok(())
160    }
161
162    async fn receive_once(
163        client: fedimint_client::ClientHandleArc,
164        amount: Amount,
165        gateway: LightningGateway,
166    ) -> Result<()> {
167        let lightning_module = client.get_first_module::<LightningClientModule>()?;
168        let desc = Description::new("test".to_string())?;
169        let (opid, invoice, _) = lightning_module
170            .create_bolt11_invoice(
171                amount,
172                Bolt11InvoiceDescription::Direct(&desc),
173                None,
174                (),
175                Some(gateway),
176            )
177            .await?;
178        faucet::pay_invoice(&invoice.to_string()).await?;
179
180        let mut updates = lightning_module
181            .subscribe_ln_receive(opid)
182            .await?
183            .into_stream();
184        while let Some(update) = updates.next().await {
185            match update {
186                LnReceiveState::Claimed => return Ok(()),
187                LnReceiveState::Canceled { reason } => {
188                    return Err(reason.into());
189                }
190                _ => {}
191            }
192        }
193        Err(anyhow!("Lightning receive failed"))
194    }
195
196    // Tests that ChaCha20 crypto functions used for backup and recovery are
197    // available in WASM at runtime. Related issue: https://github.com/fedimint/fedimint/issues/2843
198    #[wasm_bindgen_test]
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(21), 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}