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