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