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