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