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 }