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