actions.rs (24363B)
1 use std::{net::SocketAddr, path::Path, sync::Arc}; 2 3 use anyhow::{Context, Result, bail}; 4 use axum::{ 5 Form, Json, 6 extract::State, 7 http::HeaderMap, 8 response::{IntoResponse, Redirect, Response}, 9 }; 10 use secrecy::ExposeSecret; 11 use tokio::sync::Mutex; 12 13 use super::types::{ 14 ActionResponse, AddressBookDeleteForm, AddressBookForm, BurnSettingsForm, ChainResetForm, 15 ConfigForm, FeeEstimateResponse, MetricsSettingsForm, P2pAnnounceForm, P2pInboundForm, 16 PeerForm, PowMiningForm, RecoveryVdfSettingsForm, SeedPhraseForm, StratumSettingsForm, 17 TransferForm, WalletEndpointSettingsForm, WalletSetupResponse, 18 }; 19 use super::{ 20 HttpState, action_json, api_error, config_store, estimate_burn_fee, estimate_mine_fee, 21 estimate_transfer_fee, fee_estimate_json, required_fee_per_byte_burn, transfer, 22 wallet_setup_json, 23 }; 24 use crate::{ 25 adapters::{ 26 chain_store::SqliteChainStore, config_store::UiConfig, ui_data_store::SqliteUiDataStore, 27 }, 28 app::{GossipEnvelope, NodeCore}, 29 domain::Amount, 30 }; 31 32 const CHAIN_RESET_CONFIRMATION: &str = "RESET"; 33 34 pub(super) async fn apply_config_form(state: &HttpState, form: ConfigForm) -> Result<()> { 35 let peer = form.peer.trim(); 36 if !peer.is_empty() { 37 add_peer(state, peer.to_string()).await?; 38 } 39 if form.setup_complete && super::setup_requires_peer(state).await { 40 let has_peer = !state.peers.lock().await.addresses().is_empty(); 41 if !has_peer { 42 bail!("add a bootstrap peer before completing setup"); 43 } 44 } 45 let mut config = state.ui_config.lock().await; 46 config.setup_complete = form.setup_complete; 47 config_store::save(&state.config_path, &config) 48 } 49 50 pub(super) async fn api_wallet_generate_form( 51 State(state): State<HttpState>, 52 headers: HeaderMap, 53 ) -> Json<WalletSetupResponse> { 54 wallet_setup_json(super::replace_setup_wallet_with_generated_seed(&state, &headers).await) 55 } 56 57 pub(super) async fn api_wallet_import_form( 58 State(state): State<HttpState>, 59 headers: HeaderMap, 60 Form(form): Form<SeedPhraseForm>, 61 ) -> Json<WalletSetupResponse> { 62 wallet_setup_json( 63 super::import_setup_wallet_seed(&state, &headers, form.seed_phrase.expose_secret()).await, 64 ) 65 } 66 67 pub(super) async fn api_transfer_fee_estimate_form( 68 State(state): State<HttpState>, 69 Form(form): Form<TransferForm>, 70 ) -> Json<FeeEstimateResponse> { 71 fee_estimate_json(estimate_transfer_fee(&state, form).await) 72 } 73 74 pub(super) async fn api_burn_fee_estimate_form( 75 State(state): State<HttpState>, 76 Form(form): Form<BurnSettingsForm>, 77 ) -> Json<FeeEstimateResponse> { 78 fee_estimate_json(estimate_burn_fee(&state, form).await) 79 } 80 81 pub(super) async fn api_mine_fee_estimate_form( 82 State(state): State<HttpState>, 83 Form(_form): Form<std::collections::BTreeMap<String, String>>, 84 ) -> Json<FeeEstimateResponse> { 85 fee_estimate_json(estimate_mine_fee(&state).await) 86 } 87 88 pub(super) async fn api_burn_per_block_form( 89 State(state): State<HttpState>, 90 Form(form): Form<BurnSettingsForm>, 91 ) -> Json<ActionResponse> { 92 let enabled = form.enabled.unwrap_or(form.amount > 0); 93 let result = match required_fee_per_byte_burn(&form) { 94 Ok(fee_per_byte) => set_burn_settings(&state, enabled, form.amount, fee_per_byte).await, 95 Err(error) => Err(error), 96 }; 97 action_json(result) 98 } 99 100 pub(super) async fn api_pow_mining_form( 101 State(state): State<HttpState>, 102 Form(form): Form<PowMiningForm>, 103 ) -> Json<ActionResponse> { 104 action_json(set_pow_mining(&state, form.enabled, form.workers).await) 105 } 106 107 pub(super) async fn api_metrics_settings_form( 108 State(state): State<HttpState>, 109 Form(form): Form<MetricsSettingsForm>, 110 ) -> Json<ActionResponse> { 111 action_json(set_keep_track_of_metrics(&state, form.enabled).await) 112 } 113 114 pub(super) async fn api_recovery_vdf_settings_form( 115 State(state): State<HttpState>, 116 Form(form): Form<RecoveryVdfSettingsForm>, 117 ) -> Json<ActionResponse> { 118 action_json(set_recovery_vdf_top_rank_percent(&state, form.top_rank_percent).await) 119 } 120 121 pub(super) async fn api_chain_reset_form( 122 State(state): State<HttpState>, 123 Form(form): Form<ChainResetForm>, 124 ) -> Json<ActionResponse> { 125 action_json(reset_local_chain(&state, &form.confirm).await) 126 } 127 128 pub(super) async fn api_p2p_announce_form( 129 State(state): State<HttpState>, 130 Form(form): Form<P2pAnnounceForm>, 131 ) -> Json<ActionResponse> { 132 action_json(set_p2p_announce_addr(&state, form.addr).await) 133 } 134 135 pub(super) async fn api_p2p_inbound_form( 136 State(state): State<HttpState>, 137 Form(form): Form<P2pInboundForm>, 138 ) -> Json<ActionResponse> { 139 action_json(set_p2p_accept_inbound(&state, form.enabled, form.bind_port).await) 140 } 141 142 pub(super) async fn api_stratum_settings_form( 143 State(state): State<HttpState>, 144 Form(form): Form<StratumSettingsForm>, 145 ) -> Json<ActionResponse> { 146 action_json(set_stratum_settings(&state, form.enabled, form.bind_port).await) 147 } 148 149 pub(super) async fn api_wallet_endpoint_settings_form( 150 State(state): State<HttpState>, 151 Form(form): Form<WalletEndpointSettingsForm>, 152 ) -> Json<ActionResponse> { 153 action_json(set_wallet_endpoint_settings(&state, form.enabled, form.bind_port).await) 154 } 155 156 pub(super) async fn burn_per_block_form( 157 State(state): State<HttpState>, 158 Form(form): Form<BurnSettingsForm>, 159 ) -> Response { 160 let enabled = form.enabled.unwrap_or(form.amount > 0); 161 let result = match required_fee_per_byte_burn(&form) { 162 Ok(fee_per_byte) => set_burn_settings(&state, enabled, form.amount, fee_per_byte).await, 163 Err(error) => Err(error), 164 }; 165 match result { 166 Ok(_) => Redirect::to("/").into_response(), 167 Err(error) => api_error(error).into_response(), 168 } 169 } 170 171 pub(super) async fn api_transfer_form( 172 State(state): State<HttpState>, 173 Form(form): Form<TransferForm>, 174 ) -> Json<ActionResponse> { 175 let result = transfer(&state, form).await; 176 action_json(result) 177 } 178 179 pub(super) async fn transfer_form( 180 State(state): State<HttpState>, 181 Form(form): Form<TransferForm>, 182 ) -> Response { 183 match transfer(&state, form).await { 184 Ok(_) => Redirect::to("/").into_response(), 185 Err(error) => api_error(error).into_response(), 186 } 187 } 188 189 pub(super) async fn api_peer_form( 190 State(state): State<HttpState>, 191 Form(form): Form<PeerForm>, 192 ) -> Json<ActionResponse> { 193 let result = add_peer(&state, form.peer).await; 194 action_json(result) 195 } 196 197 pub(super) async fn api_peer_delete_form( 198 State(state): State<HttpState>, 199 Form(form): Form<PeerForm>, 200 ) -> Json<ActionResponse> { 201 let result = remove_peer(&state, form.peer).await; 202 action_json(result) 203 } 204 205 pub(super) async fn api_address_book_form( 206 State(state): State<HttpState>, 207 Form(form): Form<AddressBookForm>, 208 ) -> Json<ActionResponse> { 209 action_json(upsert_address_book_entry(&state, form.address, form.name, form.old_address).await) 210 } 211 212 pub(super) async fn api_address_book_delete_form( 213 State(state): State<HttpState>, 214 Form(form): Form<AddressBookDeleteForm>, 215 ) -> Json<ActionResponse> { 216 action_json(remove_address_book_entry(&state, form.address).await) 217 } 218 219 pub(super) async fn peer_form( 220 State(state): State<HttpState>, 221 Form(form): Form<PeerForm>, 222 ) -> Response { 223 match add_peer(&state, form.peer).await { 224 Ok(()) => Redirect::to("/").into_response(), 225 Err(error) => api_error(error).into_response(), 226 } 227 } 228 229 pub(super) async fn set_burn_settings( 230 state: &HttpState, 231 enabled: bool, 232 amount: Amount, 233 fee: Amount, 234 ) -> Result<()> { 235 if enabled && amount == 0 { 236 bail!("IUNA per block must be greater than zero when finalization burns are on"); 237 } 238 let result = { 239 let mut node = state.node.lock().await; 240 let result = node.set_automatic_burn_settings(enabled, amount, fee); 241 let outbox = node.drain_outbox(); 242 (result, outbox) 243 }; 244 245 match result.0 { 246 Ok(_) => { 247 persist_burn_settings_config( 248 &state.ui_config, 249 &state.config_path, 250 enabled, 251 amount, 252 fee, 253 ) 254 .await?; 255 state.gossip.broadcast(result.1).await 256 } 257 Err(error) => Err(error), 258 } 259 } 260 261 pub(super) async fn persist_burn_settings_config( 262 ui_config: &Arc<Mutex<UiConfig>>, 263 config_path: &Path, 264 enabled: bool, 265 amount: Amount, 266 fee: Amount, 267 ) -> Result<()> { 268 let mut config = ui_config.lock().await; 269 config.mining_enabled = enabled; 270 config.burn_per_block = amount; 271 config.burn_fee = fee; 272 config_store::save(config_path, &config) 273 } 274 275 pub(super) async fn set_pow_mining( 276 state: &HttpState, 277 enabled: bool, 278 workers: Option<u8>, 279 ) -> Result<()> { 280 let workers = match workers { 281 Some(workers) => workers, 282 None => state.ui_config.lock().await.pow_mining_workers, 283 }; 284 { 285 let mut node = state.node.lock().await; 286 node.set_pow_mining_workers(workers); 287 node.set_pow_mining_enabled(enabled); 288 } 289 persist_pow_mining_config(&state.ui_config, &state.config_path, enabled, workers).await 290 } 291 292 pub(super) async fn persist_pow_mining_config( 293 ui_config: &Arc<Mutex<UiConfig>>, 294 config_path: &Path, 295 enabled: bool, 296 workers: u8, 297 ) -> Result<()> { 298 let mut config = ui_config.lock().await; 299 config.pow_mining_enabled = enabled; 300 config.pow_mining_workers = config_store::clamp_pow_mining_workers(workers); 301 config_store::save(config_path, &config) 302 } 303 304 pub(super) async fn set_recovery_vdf_top_rank_percent( 305 state: &HttpState, 306 percent: u8, 307 ) -> Result<()> { 308 let percent = percent.min(100); 309 { 310 let mut node = state.node.lock().await; 311 node.set_recovery_vdf_top_rank_percent(percent); 312 } 313 let mut config = state.ui_config.lock().await; 314 config.recovery_vdf_top_rank_percent = percent; 315 config_store::save(&state.config_path, &config) 316 } 317 318 pub(super) async fn set_keep_track_of_metrics(state: &HttpState, enabled: bool) -> Result<()> { 319 let mut config = state.ui_config.lock().await; 320 let mut next_config = config.clone(); 321 next_config.keep_track_of_metrics = enabled; 322 config_store::save(&state.config_path, &next_config)?; 323 *config = next_config; 324 Ok(()) 325 } 326 327 pub(super) async fn reset_local_chain(state: &HttpState, confirmation: &str) -> Result<()> { 328 if confirmation.trim() != CHAIN_RESET_CONFIRMATION { 329 bail!("type RESET to confirm deleting the local chain"); 330 } 331 332 { 333 let mut node = state.node.lock().await; 334 state.gossip.invalidate_sync_progress(); 335 node.reset_chain_to_setup_placeholder(); 336 } 337 clear_chain(&state.chain_store).await?; 338 clear_ui_data(&state.ui_data_store).await?; 339 state.node.lock().await.complete_network_migration(); 340 state 341 .gossip 342 .broadcast(vec![GossipEnvelope::ChainBootstrapRequest]) 343 .await?; 344 Ok(()) 345 } 346 347 pub(super) async fn set_p2p_announce_addr(state: &HttpState, addr: String) -> Result<()> { 348 let trimmed = addr.trim(); 349 let parsed = if trimmed.is_empty() { 350 None 351 } else { 352 Some( 353 trimmed 354 .parse::<SocketAddr>() 355 .with_context(|| format!("invalid P2P announce address {trimmed}"))?, 356 ) 357 }; 358 359 let mut config = state.ui_config.lock().await; 360 let mut next_config = config.clone(); 361 next_config.p2p_announce_addr = parsed.map(|addr| addr.to_string()); 362 config_store::save(&state.config_path, &next_config)?; 363 *config = next_config; 364 drop(config); 365 state.gossip.set_p2p_announce_addr(parsed).await; 366 Ok(()) 367 } 368 369 pub(super) async fn set_p2p_accept_inbound( 370 state: &HttpState, 371 enabled: bool, 372 bind_port: Option<u16>, 373 ) -> Result<()> { 374 let bind_port = bind_port.unwrap_or(config_store::DEFAULT_P2P_BIND_PORT); 375 if bind_port == 0 { 376 bail!("P2P bind port must be between 1 and 65535"); 377 } 378 let previous = state.gossip.accepts_inbound().await; 379 if enabled && previous { 380 state.gossip.set_accept_inbound(true).await?; 381 } 382 383 let mut config = state.ui_config.lock().await; 384 let mut next_config = config.clone(); 385 next_config.p2p_accept_inbound = enabled; 386 next_config.p2p_bind_port = bind_port; 387 if !enabled { 388 next_config.wallet_endpoint_enabled = false; 389 } 390 if let Err(error) = config_store::save(&state.config_path, &next_config) { 391 let _ = state.gossip.set_accept_inbound(previous).await; 392 return Err(error); 393 } 394 *config = next_config; 395 drop(config); 396 397 if !enabled { 398 state.gossip.set_accept_inbound(false).await?; 399 } 400 401 Ok(()) 402 } 403 404 pub(super) async fn set_stratum_settings( 405 state: &HttpState, 406 enabled: bool, 407 bind_port: Option<u16>, 408 ) -> Result<()> { 409 let bind_port = bind_port.unwrap_or(config_store::DEFAULT_STRATUM_BIND_PORT); 410 if bind_port == 0 { 411 bail!("Stratum bind port must be between 1 and 65535"); 412 } 413 414 let mut config = state.ui_config.lock().await; 415 let mut next_config = config.clone(); 416 next_config.stratum_enabled = enabled; 417 next_config.stratum_bind_port = bind_port; 418 config_store::save(&state.config_path, &next_config)?; 419 *config = next_config; 420 Ok(()) 421 } 422 423 pub(super) async fn set_wallet_endpoint_settings( 424 state: &HttpState, 425 enabled: bool, 426 bind_port: Option<u16>, 427 ) -> Result<()> { 428 let bind_port = bind_port.unwrap_or(config_store::DEFAULT_WALLET_ENDPOINT_BIND_PORT); 429 if bind_port == 0 { 430 bail!("Wallet endpoint bind port must be between 1 and 65535"); 431 } 432 if enabled && bind_port == state.management_port { 433 bail!("Wallet endpoint port must differ from the management UI port"); 434 } 435 436 let mut config = state.ui_config.lock().await; 437 let mut next_config = config.clone(); 438 validate_wallet_endpoint_public_node(&next_config, enabled)?; 439 if enabled && next_config.p2p_accept_inbound && bind_port == next_config.p2p_bind_port { 440 bail!("Wallet endpoint port must differ from the P2P listener port"); 441 } 442 if enabled && next_config.stratum_enabled && bind_port == next_config.stratum_bind_port { 443 bail!("Wallet endpoint port must differ from the Stratum listener port"); 444 } 445 next_config.wallet_endpoint_enabled = enabled; 446 next_config.wallet_endpoint_bind_port = bind_port; 447 config_store::save(&state.config_path, &next_config)?; 448 *config = next_config; 449 Ok(()) 450 } 451 452 fn validate_wallet_endpoint_public_node( 453 config: &config_store::UiConfig, 454 enabled: bool, 455 ) -> Result<()> { 456 if enabled && !config.p2p_accept_inbound { 457 bail!("Enable Public node before enabling the wallet endpoint"); 458 } 459 Ok(()) 460 } 461 462 async fn clear_ui_data(store: &SqliteUiDataStore) -> Result<()> { 463 let store = store.clone(); 464 tokio::task::spawn_blocking(move || store.clear_all()) 465 .await 466 .context("UI data cleanup worker failed")??; 467 Ok(()) 468 } 469 470 async fn clear_chain(store: &SqliteChainStore) -> Result<()> { 471 let store = store.clone(); 472 tokio::task::spawn_blocking(move || store.clear_chain()) 473 .await 474 .context("chain reset worker failed")??; 475 Ok(()) 476 } 477 478 pub(super) async fn add_peer(state: &HttpState, peer: String) -> Result<()> { 479 let peer = validate_peer_address(peer)?; 480 let addresses = { 481 let mut peers = state.peers.lock().await; 482 peers.add_peer(peer); 483 peers.addresses() 484 }; 485 let mut config = state.ui_config.lock().await; 486 config.peers = addresses; 487 config_store::save(&state.config_path, &config) 488 } 489 490 pub(super) async fn remove_peer(state: &HttpState, peer: String) -> Result<()> { 491 let peer = validate_peer_address(peer)?; 492 let addresses = { 493 let mut peers = state.peers.lock().await; 494 if !peers.remove_peer(&peer) { 495 bail!("peer is not configured as an outbound peer"); 496 } 497 peers.addresses() 498 }; 499 let mut config = state.ui_config.lock().await; 500 config.peers = addresses; 501 config_store::save(&state.config_path, &config) 502 } 503 504 pub(super) async fn upsert_address_book_entry( 505 state: &HttpState, 506 address: String, 507 name: String, 508 old_address: Option<String>, 509 ) -> Result<()> { 510 let address = validate_address_book_address(state, address).await?; 511 let name = validate_address_book_name(name)?; 512 let old_address = match old_address { 513 Some(old_address) => { 514 Some(validate_existing_address_book_address(state, old_address).await?) 515 } 516 None => None, 517 }; 518 let mut config = state.ui_config.lock().await; 519 if let Some(old_address) = old_address.as_deref() { 520 if old_address != address { 521 if config.address_book.contains_key(&address) { 522 bail!("address is already saved"); 523 } 524 config.address_book.remove(old_address); 525 } 526 } else if config.address_book.contains_key(&address) { 527 bail!("address is already saved"); 528 } 529 config.address_book.insert(address, name); 530 config_store::save(&state.config_path, &config) 531 } 532 533 pub(super) async fn remove_address_book_entry(state: &HttpState, address: String) -> Result<()> { 534 let address = validate_existing_address_book_address(state, address).await?; 535 let mut config = state.ui_config.lock().await; 536 config.address_book.remove(&address); 537 config_store::save(&state.config_path, &config) 538 } 539 540 fn validate_peer_address(peer: String) -> Result<String> { 541 let peer = peer.trim().to_string(); 542 if peer.is_empty() { 543 bail!("peer address is required"); 544 } 545 Ok(peer) 546 } 547 548 async fn validate_address_book_address(state: &HttpState, address: String) -> Result<String> { 549 let node = state.node.lock().await; 550 normalize_address_book_address(&node, address) 551 } 552 553 fn normalize_address_book_address(node: &NodeCore, address: String) -> Result<String> { 554 let address = address.trim().to_string(); 555 if address.is_empty() { 556 bail!("address is required"); 557 } 558 node.decode_user_address(&address) 559 .context("invalid address book address")?; 560 Ok(address.to_ascii_lowercase()) 561 } 562 563 async fn validate_existing_address_book_address( 564 state: &HttpState, 565 address: String, 566 ) -> Result<String> { 567 let address = address.trim().to_string(); 568 if address.is_empty() { 569 bail!("address is required"); 570 } 571 if state 572 .node 573 .lock() 574 .await 575 .decode_user_address(&address) 576 .is_err() 577 { 578 crate::domain::validate_address(&address, "legacy address book") 579 .context("invalid existing address book address")?; 580 } 581 Ok(address.to_ascii_lowercase()) 582 } 583 584 fn validate_address_book_name(name: String) -> Result<String> { 585 let name = name.trim().to_string(); 586 if name.is_empty() { 587 bail!("name is required"); 588 } 589 Ok(name) 590 } 591 592 #[cfg(test)] 593 mod tests { 594 use std::{ 595 collections::BTreeMap, 596 net::{IpAddr, Ipv4Addr, SocketAddr}, 597 sync::Arc, 598 }; 599 600 use tempfile::tempdir; 601 use tokio::sync::Mutex; 602 603 use crate::{ 604 adapters::{ 605 chain_store::SqliteChainStore, config_store, config_store::UiConfig, 606 p2p::GossipNetwork, ui_data_store::SqliteUiDataStore, wallet_store, 607 }, 608 app::{NodeCore, PeerBook, StratumStatus}, 609 domain::{AddressNetwork, GenesisBurn, Ledger, MICRO_IUNA, Wallet, encode_address}, 610 }; 611 612 use super::super::{AuthSession, HttpState, state::AuthBackoff}; 613 use super::{ 614 CHAIN_RESET_CONFIRMATION, normalize_address_book_address, reset_local_chain, 615 validate_wallet_endpoint_public_node, 616 }; 617 618 fn socket() -> SocketAddr { 619 SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9444) 620 } 621 622 #[test] 623 fn wallet_endpoint_requires_a_public_node() { 624 let mut config = UiConfig::default(); 625 let error = validate_wallet_endpoint_public_node(&config, true).unwrap_err(); 626 assert!(error.to_string().contains("Public node")); 627 628 config.p2p_accept_inbound = true; 629 validate_wallet_endpoint_public_node(&config, true).unwrap(); 630 validate_wallet_endpoint_public_node(&UiConfig::default(), false).unwrap(); 631 } 632 633 #[test] 634 fn address_book_accepts_legacy_and_hybrid_addresses_for_the_active_network() { 635 let wallet = Wallet::from_seed("hybrid-address-book-wallet"); 636 let ledger = Ledger::new(BTreeMap::new(), 1); 637 let node = NodeCore::from_ledger(wallet.clone(), ledger, 0); 638 let legacy = encode_address(wallet.address(), AddressNetwork::Mainnet).unwrap(); 639 let hybrid = wallet.hybrid_address(AddressNetwork::Mainnet); 640 641 assert_eq!( 642 normalize_address_book_address(&node, legacy.to_uppercase()).unwrap(), 643 legacy 644 ); 645 assert_eq!( 646 normalize_address_book_address(&node, hybrid.to_uppercase()).unwrap(), 647 hybrid 648 ); 649 assert!( 650 normalize_address_book_address(&node, wallet.hybrid_address(AddressNetwork::Testnet)) 651 .is_err() 652 ); 653 } 654 655 #[tokio::test] 656 async fn local_chain_reset_clears_chain_and_ui_without_touching_wallet_or_config() { 657 let dir = tempdir().unwrap(); 658 let wallet_path = dir.path().join("wallet.json"); 659 let config_path = dir.path().join("config.json"); 660 let chain_store = SqliteChainStore::open(dir.path().join("chain.sqlite3")).unwrap(); 661 let ui_data_store = SqliteUiDataStore::open(dir.path().join("ui.sqlite3")).unwrap(); 662 let wallet = wallet_store::replace_with_imported_seed_phrase( 663 &wallet_path, 664 "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", 665 ) 666 .unwrap(); 667 let config = UiConfig { 668 setup_complete: true, 669 peers: vec!["127.0.0.1:9445".to_string()], 670 ..UiConfig::default() 671 }; 672 config_store::save(&config_path, &config).unwrap(); 673 let wallet_before = std::fs::read(&wallet_path).unwrap(); 674 let config_before = std::fs::read(&config_path).unwrap(); 675 676 let mut allocations = BTreeMap::new(); 677 allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA); 678 let ledger = Ledger::new_with_genesis_burns( 679 allocations, 680 vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)], 681 1, 682 ) 683 .unwrap(); 684 let snapshot = ledger.snapshot(); 685 chain_store.save(&snapshot).unwrap(); 686 ui_data_store.project_snapshot(&snapshot, true).unwrap(); 687 688 let mut node_core = NodeCore::from_ledger(wallet, ledger, 0); 689 node_core.require_network_migration("iuna-devnet-v5"); 690 let node = Arc::new(Mutex::new(node_core)); 691 let peers = Arc::new(Mutex::new(PeerBook::default())); 692 let gossip = GossipNetwork::start(node.clone(), peers.clone(), socket(), None, false) 693 .await 694 .unwrap(); 695 let state = HttpState { 696 node, 697 peers, 698 gossip, 699 ui_config: Arc::new(Mutex::new(config)), 700 config_path: config_path.clone(), 701 chain_store: chain_store.clone(), 702 ui_data_store: ui_data_store.clone(), 703 wallet_path: wallet_path.clone(), 704 stratum: StratumStatus { 705 enabled: false, 706 listen_addr: None, 707 }, 708 auth_sessions: Arc::new(Mutex::new(BTreeMap::<String, AuthSession>::new())), 709 auth_backoff: Arc::new(Mutex::new(BTreeMap::<String, AuthBackoff>::new())), 710 setup_capability: Arc::new(Mutex::new(Some("test-setup-capability".into()))), 711 management_port: 9444, 712 wallet_endpoint_addr: None, 713 }; 714 715 reset_local_chain(&state, CHAIN_RESET_CONFIRMATION) 716 .await 717 .unwrap(); 718 719 assert!(!state.node.lock().await.has_real_chain()); 720 assert!(state.node.lock().await.network_migration_from().is_none()); 721 assert!(chain_store.load().unwrap().is_none()); 722 assert!(ui_data_store.load_metrics().unwrap().is_empty()); 723 assert_eq!(std::fs::read(&wallet_path).unwrap(), wallet_before); 724 assert_eq!(std::fs::read(&config_path).unwrap(), config_before); 725 } 726 }