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