fedimint_wasm_tests/
lib.rs

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