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 }