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_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, Database)> {
32 let mem_database = MemDatabase::default();
33 let mut builder = fedimint_client::Client::builder().await?;
34 builder.with_module(LightningClientInit::default());
35 builder.with_module(MintClientInit);
36 builder.with_module(WalletClientInit::default());
37
38 Ok((builder, mem_database.into()))
39}
40
41async fn client(invite_code: &InviteCode) -> Result<fedimint_client::ClientHandleArc> {
42 let (mut builder, db) = make_client_builder().await?;
43 let client_secret = load_or_generate_mnemonic(&db).await?;
44 builder.stopped();
45 let client = builder
46 .preview(invite_code)
47 .await?
48 .join(
49 db,
50 RootSecret::StandardDoubleDerive(PlainRootSecretStrategy::to_root_secret(
51 &client_secret,
52 )),
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 Some(_) => {}
292 None => return Err(anyhow!("reissue failed")),
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(100), ln_gateway.clone())), )
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}