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 #[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 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}