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