Skip to main content

fedimint_wasm_tests/
tests.rs

1use std::time::Duration;
2
3use anyhow::{anyhow, bail};
4use fedimint_core::Amount;
5use fedimint_derive_secret::DerivableSecret;
6use fedimint_ln_client::{
7    LightningClientModule, LnPayState, LnReceiveState, OutgoingLightningPayment, PayType,
8};
9use fedimint_ln_common::LightningGateway;
10use fedimint_ln_common::lightning_invoice::{Bolt11InvoiceDescription, Description};
11use fedimint_mint_client::{
12    MintClientModule, ReissueExternalNotesState, SelectNotesWithAtleastAmount, SpendOOBState,
13};
14use futures::StreamExt;
15use wasm_bindgen_test::wasm_bindgen_test;
16
17use super::{Result, client, faucet};
18
19#[wasm_bindgen_test]
20async fn build_client() -> Result<()> {
21    let _client = client(&faucet::invite_code().await?.parse()?).await?;
22    Ok(())
23}
24
25async fn get_gateway(
26    client: &fedimint_client::ClientHandleArc,
27) -> anyhow::Result<LightningGateway> {
28    let lightning_module = client.get_first_module::<LightningClientModule>()?;
29    let gws = lightning_module.list_gateways().await;
30    let gw_api = faucet::gateway_api().await?;
31    let lnd_gw = gws
32        .into_iter()
33        .find(|x| x.info.api.to_string() == gw_api)
34        .expect("no gateway with api");
35
36    Ok(lnd_gw.info)
37}
38
39#[wasm_bindgen_test]
40async fn receive() -> Result<()> {
41    let client = client(&faucet::invite_code().await?.parse()?).await?;
42    client.start_executor();
43    let ln_gateway = get_gateway(&client).await?;
44    futures::future::try_join_all(
45        (0..10).map(|_| receive_once(client.clone(), Amount::from_sats(21), ln_gateway.clone())),
46    )
47    .await?;
48    Ok(())
49}
50
51async fn receive_once(
52    client: fedimint_client::ClientHandleArc,
53    amount: Amount,
54    gateway: LightningGateway,
55) -> Result<()> {
56    let lightning_module = client.get_first_module::<LightningClientModule>()?;
57    let desc = Description::new("test".to_string())?;
58    let (opid, invoice, _) = lightning_module
59        .create_bolt11_invoice(
60            amount,
61            Bolt11InvoiceDescription::Direct(desc),
62            None,
63            (),
64            Some(gateway),
65        )
66        .await?;
67    faucet::pay_invoice(&invoice.to_string()).await?;
68
69    let mut updates = lightning_module
70        .subscribe_ln_receive(opid)
71        .await?
72        .into_stream();
73    while let Some(update) = updates.next().await {
74        match update {
75            LnReceiveState::Claimed => return Ok(()),
76            LnReceiveState::Canceled { reason } => {
77                return Err(reason.into());
78            }
79            _ => {}
80        }
81    }
82    Err(anyhow!("Lightning receive failed"))
83}
84
85// Tests that ChaCha20 crypto functions used for backup and recovery are
86// available in WASM at runtime. Related issue: https://github.com/fedimint/fedimint/issues/2843
87#[wasm_bindgen_test]
88#[allow(clippy::unused_async)]
89async fn derive_chacha_key() {
90    let root_secret = DerivableSecret::new_root(&[0x42; 32], &[0x2a; 32]);
91    let key = root_secret.to_chacha20_poly1305_key();
92
93    // Prevent optimization
94    // FIXME: replace with `std::hint::black_box` once stabilized
95    assert!(format!("key: {key:?}").len() > 8);
96}
97
98async fn pay_once(
99    client: fedimint_client::ClientHandleArc,
100    ln_gateway: LightningGateway,
101) -> Result<(), anyhow::Error> {
102    let lightning_module = client.get_first_module::<LightningClientModule>()?;
103    let bolt11 = faucet::generate_invoice(11).await?;
104    let OutgoingLightningPayment {
105        payment_type,
106        contract_id: _,
107        fee: _,
108    } = lightning_module
109        .pay_bolt11_invoice(Some(ln_gateway), bolt11.parse()?, ())
110        .await?;
111    let PayType::Lightning(operation_id) = payment_type else {
112        unreachable!("paying invoice over lightning");
113    };
114    let lightning_module = client.get_first_module::<LightningClientModule>()?;
115    let mut updates = lightning_module
116        .subscribe_ln_pay(operation_id)
117        .await?
118        .into_stream();
119    loop {
120        match updates.next().await {
121            Some(LnPayState::Success { preimage: _ }) => {
122                break;
123            }
124            Some(LnPayState::Refunded { gateway_error }) => {
125                return Err(anyhow!("refunded {gateway_error}"));
126            }
127            None => return Err(anyhow!("Lightning send failed")),
128            _ => {}
129        }
130    }
131    Ok(())
132}
133
134#[wasm_bindgen_test]
135async fn receive_and_pay() -> Result<()> {
136    let client = client(&faucet::invite_code().await?.parse()?).await?;
137    client.start_executor();
138    let ln_gateway = get_gateway(&client).await?;
139
140    futures::future::try_join_all(
141        (0..10).map(|_| receive_once(client.clone(), Amount::from_sats(21), ln_gateway.clone())),
142    )
143    .await?;
144    futures::future::try_join_all((0..10).map(|_| pay_once(client.clone(), ln_gateway.clone())))
145        .await?;
146
147    Ok(())
148}
149
150async fn send_and_recv_ecash_once(
151    client: fedimint_client::ClientHandleArc,
152) -> Result<(), anyhow::Error> {
153    let mint = client.get_first_module::<MintClientModule>()?;
154    let (_, notes) = mint
155        .spend_notes_with_selector(
156            &SelectNotesWithAtleastAmount,
157            Amount::from_sats(11),
158            Some(Duration::from_secs(10000)),
159            false,
160            (),
161        )
162        .await?;
163    let operation_id = mint.reissue_external_notes(notes, ()).await?;
164    let mut updates = mint
165        .subscribe_reissue_external_notes(operation_id)
166        .await?
167        .into_stream();
168    loop {
169        match updates.next().await {
170            Some(ReissueExternalNotesState::Done) => {
171                break;
172            }
173            Some(ReissueExternalNotesState::Failed(error)) => {
174                return Err(anyhow!("reissue failed {error}"));
175            }
176            Some(_) => {}
177            None => return Err(anyhow!("reissue failed")),
178        }
179    }
180    Ok(())
181}
182
183async fn send_ecash_exact(
184    client: fedimint_client::ClientHandleArc,
185    amount: Amount,
186) -> Result<(), anyhow::Error> {
187    let mint = client.get_first_module::<MintClientModule>()?;
188    'retry: loop {
189        let (operation_id, notes) = mint
190            .spend_notes_with_selector(
191                &SelectNotesWithAtleastAmount,
192                amount,
193                Some(Duration::from_secs(10000)),
194                false,
195                (),
196            )
197            .await?;
198        if notes.total_amount() == amount {
199            return Ok(());
200        }
201        mint.try_cancel_spend_notes(operation_id).await;
202        let mut updates = mint
203            .subscribe_spend_notes(operation_id)
204            .await?
205            .into_stream();
206        while let Some(update) = updates.next().await {
207            if update == SpendOOBState::UserCanceledSuccess {
208                continue 'retry;
209            }
210        }
211        bail!("failed to cancel notes");
212    }
213}
214
215#[wasm_bindgen_test]
216async fn test_ecash() -> Result<()> {
217    let client = client(&faucet::invite_code().await?.parse()?).await?;
218    client.start_executor();
219    let ln_gateway = get_gateway(&client).await?;
220
221    futures::future::try_join_all(
222        (0..10)
223            .map(|_| receive_once(client.clone(), Amount::from_sats(100), ln_gateway.clone())), // Increased from 25 to 100 sats to account for mint fees
224    )
225    .await?;
226    futures::future::try_join_all((0..10).map(|_| send_and_recv_ecash_once(client.clone())))
227        .await?;
228    Ok(())
229}
230
231#[wasm_bindgen_test]
232async fn test_ecash_exact() -> Result<()> {
233    let client = client(&faucet::invite_code().await?.parse()?).await?;
234    client.start_executor();
235    let ln_gateway = get_gateway(&client).await?;
236
237    receive_once(client.clone(), Amount::from_sats(100), ln_gateway).await?;
238    futures::future::try_join_all(
239        (0..3).map(|_| send_ecash_exact(client.clone(), Amount::from_sats(1))),
240    )
241    .await?;
242    Ok(())
243}