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::secret::{PlainRootSecretStrategy, RootSecretStrategy};
10use fedimint_client::{Client, RootSecret};
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 mut builder = make_client_builder().await?;
44 let client_secret = load_or_generate_mnemonic(builder.db_no_decoders()).await?;
45 builder.stopped();
46 let client = builder
47 .preview(invite_code)
48 .await?
49 .join(RootSecret::StandardDoubleDerive(
50 PlainRootSecretStrategy::to_root_secret(&client_secret),
51 ))
52 .await
53 .map(Arc::new)?;
54 if let Ok(ln_client) = client.get_first_module::<LightningClientModule>() {
55 let _ = ln_client.update_gateway_cache().await;
56 }
57 Ok(client)
58}
59
60mod faucet {
61 pub async fn invite_code() -> anyhow::Result<String> {
62 let resp = gloo_net::http::Request::get("http://localhost:15243/connect-string")
63 .send()
64 .await?;
65 if resp.ok() {
66 Ok(resp.text().await?)
67 } else {
68 anyhow::bail!(resp.text().await?);
69 }
70 }
71
72 pub async fn pay_invoice(invoice: &str) -> anyhow::Result<()> {
73 let resp = gloo_net::http::Request::post("http://localhost:15243/pay")
74 .body(invoice)?
75 .send()
76 .await?;
77 if resp.ok() {
78 Ok(())
79 } else {
80 anyhow::bail!(resp.text().await?);
81 }
82 }
83
84 pub async fn gateway_api() -> anyhow::Result<String> {
85 let resp = gloo_net::http::Request::get("http://localhost:15243/gateway-api")
86 .send()
87 .await?;
88 if resp.ok() {
89 Ok(resp.text().await?)
90 } else {
91 anyhow::bail!(resp.text().await?);
92 }
93 }
94
95 pub async fn generate_invoice(amt: u64) -> anyhow::Result<String> {
96 let resp = gloo_net::http::Request::post("http://localhost:15243/invoice")
97 .body(amt)?
98 .send()
99 .await?;
100 if resp.ok() {
101 Ok(resp.text().await?)
102 } else {
103 anyhow::bail!(resp.text().await?);
104 }
105 }
106}
107
108wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
109mod tests {
110 use std::time::Duration;
111
112 use anyhow::{anyhow, bail};
113 use fedimint_core::Amount;
114 use fedimint_derive_secret::DerivableSecret;
115 use fedimint_ln_client::{
116 LightningClientModule, LnPayState, LnReceiveState, OutgoingLightningPayment, PayType,
117 };
118 use fedimint_ln_common::LightningGateway;
119 use fedimint_ln_common::lightning_invoice::{Bolt11InvoiceDescription, Description};
120 use fedimint_mint_client::{
121 MintClientModule, ReissueExternalNotesState, SelectNotesWithAtleastAmount, SpendOOBState,
122 };
123 use futures::StreamExt;
124 use wasm_bindgen_test::wasm_bindgen_test;
125
126 use super::{Result, client, faucet};
127
128 #[wasm_bindgen_test]
129 async fn build_client() -> Result<()> {
130 let _client = client(&faucet::invite_code().await?.parse()?).await?;
131 Ok(())
132 }
133
134 async fn get_gateway(
135 client: &fedimint_client::ClientHandleArc,
136 ) -> anyhow::Result<LightningGateway> {
137 let lightning_module = client.get_first_module::<LightningClientModule>()?;
138 let gws = lightning_module.list_gateways().await;
139 let gw_api = faucet::gateway_api().await?;
140 let lnd_gw = gws
141 .into_iter()
142 .find(|x| x.info.api.to_string() == gw_api)
143 .expect("no gateway with api");
144
145 Ok(lnd_gw.info)
146 }
147
148 #[wasm_bindgen_test]
149 async fn receive() -> Result<()> {
150 let client = client(&faucet::invite_code().await?.parse()?).await?;
151 client.start_executor();
152 let ln_gateway = get_gateway(&client).await?;
153 futures::future::try_join_all(
154 (0..10)
155 .map(|_| receive_once(client.clone(), Amount::from_sats(21), ln_gateway.clone())),
156 )
157 .await?;
158 Ok(())
159 }
160
161 async fn receive_once(
162 client: fedimint_client::ClientHandleArc,
163 amount: Amount,
164 gateway: LightningGateway,
165 ) -> Result<()> {
166 let lightning_module = client.get_first_module::<LightningClientModule>()?;
167 let desc = Description::new("test".to_string())?;
168 let (opid, invoice, _) = lightning_module
169 .create_bolt11_invoice(
170 amount,
171 Bolt11InvoiceDescription::Direct(desc),
172 None,
173 (),
174 Some(gateway),
175 )
176 .await?;
177 faucet::pay_invoice(&invoice.to_string()).await?;
178
179 let mut updates = lightning_module
180 .subscribe_ln_receive(opid)
181 .await?
182 .into_stream();
183 while let Some(update) = updates.next().await {
184 match update {
185 LnReceiveState::Claimed => return Ok(()),
186 LnReceiveState::Canceled { reason } => {
187 return Err(reason.into());
188 }
189 _ => {}
190 }
191 }
192 Err(anyhow!("Lightning receive failed"))
193 }
194
195 #[wasm_bindgen_test]
198 #[allow(clippy::unused_async)]
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 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(25), 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}