main.rs (25343B)
1 use std::{ 2 collections::BTreeMap, 3 net::SocketAddr, 4 path::{Path, PathBuf}, 5 sync::Arc, 6 time::{Duration, Instant}, 7 }; 8 9 use anyhow::{Context, Result, bail}; 10 use iuna::{ 11 adapters::{ 12 chain_store::SqliteChainStore, config_store, http, p2p, stratum, 13 ui_data_store::SqliteUiDataStore, wallet_store, 14 }, 15 app::{ 16 NodeCore, PeerBook, SharedNode, SharedPeerBook, StratumStatus, debug_logging_enabled, 17 now_ms, set_debug_logging, 18 }, 19 domain::{ 20 Amount, ChainSnapshot, GenesisBurn, Ledger, MAX_VDF_ROUNDS, MICRO_IUNA, 21 VDF_TARGET_BLOCK_MS, run_vdf, 22 }, 23 }; 24 use tokio::sync::Mutex; 25 26 mod cli; 27 use cli::{ 28 ChainMode, CliOptions, apply_cli_p2p_config_overrides, configured_p2p_announce_addr, 29 configured_p2p_bind_addr, initial_burn_fee, initial_burn_per_block, validate_wallet_for_mode, 30 }; 31 #[cfg(test)] 32 use cli::{default_data_dir, help_text}; 33 34 const GENESIS_BOOTSTRAP_BURN_AMOUNT: Amount = MICRO_IUNA; 35 const GENESIS_INITIAL_BURN_PER_BLOCK: Amount = config_store::DEFAULT_BURN_AMOUNT; 36 const GENESIS_INITIAL_BURN_FEE: Amount = config_store::DEFAULT_BURN_FEE; 37 const VDF_MEASUREMENT_INITIAL_ROUNDS: u64 = 1_000; 38 const VDF_MEASUREMENT_MAX_ROUNDS: u64 = 10_000_000; 39 const VDF_MEASUREMENT_MIN_ELAPSED: Duration = Duration::from_millis(150); 40 41 #[tokio::main] 42 async fn main() -> Result<()> { 43 let Some(opts) = CliOptions::parse()? else { 44 return Ok(()); 45 }; 46 set_debug_logging(opts.debug); 47 let debug_logging = opts.debug; 48 let wallet_path = opts.wallet_path(); 49 let config_path = opts.config_path(); 50 let wallet_file_exists = wallet_path.exists(); 51 validate_wallet_for_mode(&opts, &wallet_path, wallet_file_exists)?; 52 let chain_db_path = opts.chain_db_path(); 53 let ui_data_db_path = ui_data_db_path(&chain_db_path); 54 let chain_store = SqliteChainStore::open(&chain_db_path)?; 55 let ui_data_store = SqliteUiDataStore::open(&ui_data_db_path)?; 56 let persisted_chain_exists = chain_store.load()?.is_some(); 57 if opts.chain_mode == ChainMode::Genesis && persisted_chain_exists { 58 bail!( 59 "--genesis refuses to run because chain database already contains a blockchain at {}; start without --genesis to resume it", 60 chain_store.path().display() 61 ); 62 } 63 let mut ui_config = config_store::load_or_create(&config_path)?; 64 let ui_config_dirty = apply_cli_p2p_config_overrides(&opts, &mut ui_config); 65 let p2p_announce_addr = configured_p2p_announce_addr(&opts, &ui_config)?; 66 let configured_p2p_addr = configured_p2p_bind_addr(&opts, &ui_config); 67 let p2p_accept_inbound = ui_config.p2p_accept_inbound; 68 let advertised_p2p_addr = p2p_announce_addr.unwrap_or(configured_p2p_addr); 69 let wallet_load = load_startup_wallet(&wallet_path)?; 70 let wallet_address = wallet_load.address().to_string(); 71 if opts.chain_mode == ChainMode::Genesis { 72 ui_config.setup_complete = false; 73 ui_config.mining_enabled = true; 74 ui_config.pow_mining_enabled = false; 75 ui_config.burn_per_block = GENESIS_INITIAL_BURN_PER_BLOCK; 76 ui_config.burn_fee = GENESIS_INITIAL_BURN_FEE; 77 config_store::save(&config_path, &ui_config)?; 78 } else if ui_config_dirty { 79 config_store::save(&config_path, &ui_config)?; 80 } 81 let ledger = 82 initialize_ledger(&opts, &wallet_address, &chain_store, advertised_p2p_addr).await?; 83 let has_chain = opts.has_chain() || persisted_chain_exists; 84 let initial_burn_per_block = initial_burn_per_block(&opts, &ui_config); 85 let initial_burn_fee = initial_burn_fee(&opts, &ui_config); 86 87 let mut node_core = match wallet_load { 88 StartupWallet::Unlocked { 89 wallet, 90 owned_blinded_transactions, 91 } => { 92 let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( 93 wallet, 94 ledger, 95 ui_config.mining_enabled, 96 initial_burn_per_block, 97 initial_burn_fee, 98 ); 99 node.restore_owned_blinded_transactions(owned_blinded_transactions)?; 100 node 101 } 102 StartupWallet::Locked { address } => NodeCore::from_locked_wallet_address( 103 address, 104 ledger, 105 ui_config.mining_enabled, 106 initial_burn_per_block, 107 initial_burn_fee, 108 ), 109 }; 110 node_core.set_pow_mining_workers(ui_config.pow_mining_workers); 111 node_core.set_pow_mining_enabled(ui_config.pow_mining_enabled); 112 node_core.set_recovery_vdf_top_rank_percent(ui_config.recovery_vdf_top_rank_percent); 113 let node: SharedNode = Arc::new(Mutex::new(node_core)); 114 let ui_config = Arc::new(Mutex::new(ui_config)); 115 let mut peers = ui_config.lock().await.peers.clone(); 116 peers.extend(opts.peers); 117 let peers: SharedPeerBook = Arc::new(Mutex::new(PeerBook::from_addresses(peers))); 118 if has_chain { 119 let initial_snapshot = { node.lock().await.chain_snapshot() }; 120 let keep_metrics = ui_config.lock().await.keep_track_of_metrics; 121 persist_chain_snapshot(&chain_store, initial_snapshot.clone()).await?; 122 warm_ui_data_store(&ui_data_store, initial_snapshot, keep_metrics).await?; 123 } else { 124 clear_ui_data_store(&ui_data_store).await?; 125 } 126 127 println!("iuna wallet: {}", node.lock().await.wallet_address()); 128 if node.lock().await.wallet_is_locked() { 129 println!("wallet locked: unlock it in the management UI"); 130 } 131 println!("wallet file: {}", wallet_path.display()); 132 println!("config file: {}", config_path.display()); 133 println!("chain database: {}", chain_store.path().display()); 134 println!("UI data database: {}", ui_data_store.path().display()); 135 println!("management UI: http://{}", opts.http_addr); 136 if p2p_accept_inbound { 137 println!("p2p listener: {}", configured_p2p_addr); 138 } else { 139 println!("p2p listener: disabled (outbound-only)"); 140 } 141 if p2p_accept_inbound { 142 if let Some(addr) = p2p_announce_addr { 143 println!("p2p announce address: {addr}"); 144 } 145 } 146 println!( 147 "automatic finalization: VDF-driven, burning {} IUNA per block with {} IUNA per byte fee rate", 148 format_iuna(initial_burn_per_block), 149 format_iuna(initial_burn_fee) 150 ); 151 152 let gossip = p2p::GossipNetwork::start( 153 Arc::clone(&node), 154 Arc::clone(&peers), 155 configured_p2p_addr, 156 p2p_announce_addr, 157 p2p_accept_inbound, 158 ) 159 .await?; 160 let mut stratum_status = StratumStatus { 161 enabled: false, 162 listen_addr: None, 163 }; 164 if let Some(stratum_addr) = opts.stratum_addr { 165 let stratum = 166 stratum::StratumServer::start(Arc::clone(&node), gossip.clone(), stratum_addr).await?; 167 println!("stratum listener: {}", stratum.listen_addr()); 168 stratum_status = StratumStatus { 169 enabled: true, 170 listen_addr: Some(stratum.listen_addr().to_string()), 171 }; 172 } 173 174 let persistence_node = Arc::clone(&node); 175 let persistence_store = chain_store.clone(); 176 let persistence_ui_data_store = ui_data_store.clone(); 177 let persistence_config = Arc::clone(&ui_config); 178 let persistence_initial_tip = { 179 let node = node.lock().await; 180 if node.has_real_chain() { 181 Some(node.chain_tip_hash()) 182 } else { 183 None 184 } 185 }; 186 tokio::spawn(async move { 187 run_chain_persistence( 188 persistence_node, 189 persistence_store, 190 persistence_ui_data_store, 191 persistence_config, 192 persistence_initial_tip, 193 ) 194 .await; 195 }); 196 197 let finalizer_node = Arc::clone(&node); 198 let finalizer_gossip = gossip.clone(); 199 tokio::spawn(async move { 200 run_automatic_finalizer(finalizer_node, finalizer_gossip, debug_logging).await; 201 }); 202 203 let pow_miner_node = Arc::clone(&node); 204 let pow_miner_gossip = gossip.clone(); 205 tokio::spawn(async move { 206 run_automatic_pow_miner(pow_miner_node, pow_miner_gossip, debug_logging).await; 207 }); 208 209 let sync_node = Arc::clone(&node); 210 let sync_gossip = gossip.clone(); 211 tokio::spawn(async move { 212 run_peer_sync(sync_node, sync_gossip, debug_logging).await; 213 }); 214 215 if !has_chain { 216 println!("setup mode: waiting to join or create a chain"); 217 } 218 219 http::serve( 220 node, 221 peers, 222 gossip, 223 ui_config, 224 http::ServeOptions { 225 config_path, 226 chain_store, 227 ui_data_store, 228 wallet_path, 229 stratum: stratum_status, 230 addr: opts.http_addr, 231 }, 232 ) 233 .await 234 } 235 236 enum StartupWallet { 237 Unlocked { 238 wallet: iuna::domain::Wallet, 239 owned_blinded_transactions: Vec<iuna::domain::OwnedBlindedTransaction>, 240 }, 241 Locked { 242 address: String, 243 }, 244 } 245 246 impl StartupWallet { 247 fn address(&self) -> &str { 248 match self { 249 Self::Unlocked { wallet, .. } => wallet.address(), 250 Self::Locked { address } => address, 251 } 252 } 253 } 254 255 fn load_startup_wallet(wallet_path: &Path) -> Result<StartupWallet> { 256 match wallet_store::load_or_create(wallet_path) { 257 Ok(wallet) => { 258 let owned_blinded_transactions = 259 wallet_store::load_owned_blinded_transactions(wallet_path, None)?; 260 Ok(StartupWallet::Unlocked { 261 wallet, 262 owned_blinded_transactions, 263 }) 264 } 265 Err(error) => { 266 let Some(metadata) = wallet_store::metadata(wallet_path)? else { 267 return Err(error); 268 }; 269 if metadata.encrypted { 270 Ok(StartupWallet::Locked { 271 address: metadata.address, 272 }) 273 } else { 274 Err(error) 275 } 276 } 277 } 278 } 279 280 fn format_iuna(amount: Amount) -> String { 281 let whole = amount / MICRO_IUNA; 282 let fractional = amount % MICRO_IUNA; 283 if fractional == 0 { 284 whole.to_string() 285 } else { 286 let mut fractional = format!("{fractional:06}"); 287 while fractional.ends_with('0') { 288 fractional.pop(); 289 } 290 format!("{whole}.{fractional}") 291 } 292 } 293 294 fn ui_data_db_path(chain_db_path: &Path) -> PathBuf { 295 chain_db_path.with_file_name("ui_data.sqlite3") 296 } 297 298 async fn initialize_ledger( 299 opts: &CliOptions, 300 wallet_address: &str, 301 chain_store: &SqliteChainStore, 302 advertised_p2p_addr: SocketAddr, 303 ) -> Result<Ledger> { 304 if let Some(snapshot) = chain_store.load()? { 305 if opts.chain_mode == ChainMode::Genesis { 306 bail!( 307 "--genesis refuses to run because chain database already contains a blockchain at {}; start without --genesis to resume it", 308 chain_store.path().display() 309 ); 310 } 311 let height = snapshot_height(&snapshot); 312 let ledger = Ledger::from_persisted_snapshot(snapshot).with_context(|| { 313 format!( 314 "failed to load chain database {}", 315 chain_store.path().display() 316 ) 317 })?; 318 println!( 319 "resumed chain from {} at height {height}", 320 chain_store.path().display() 321 ); 322 Ok(ledger) 323 } else { 324 match opts.chain_mode { 325 ChainMode::Setup => Ok(setup_ledger()), 326 ChainMode::Genesis => start_genesis_ledger(wallet_address), 327 ChainMode::Join => join_chain_ledger(&opts.join_peers, advertised_p2p_addr).await, 328 } 329 } 330 } 331 332 fn snapshot_height(snapshot: &ChainSnapshot) -> u64 { 333 snapshot 334 .blocks 335 .last() 336 .map(|block| block.height) 337 .unwrap_or(0) 338 } 339 340 fn setup_ledger() -> Ledger { 341 Ledger::new(BTreeMap::new(), 1) 342 } 343 344 fn start_genesis_ledger(wallet_address: &str) -> Result<Ledger> { 345 let vdf_rounds = measure_initial_vdf_rounds(); 346 let mut genesis = BTreeMap::new(); 347 genesis.insert(wallet_address.to_string(), GENESIS_BOOTSTRAP_BURN_AMOUNT); 348 Ledger::new_with_genesis_burns( 349 genesis, 350 vec![GenesisBurn::new( 351 wallet_address, 352 GENESIS_BOOTSTRAP_BURN_AMOUNT, 353 )], 354 vdf_rounds, 355 ) 356 } 357 358 fn measure_initial_vdf_rounds() -> u64 { 359 let seed = "iuna-vdf-calibration"; 360 let (measured_rounds, elapsed) = measure_vdf_rounds( 361 seed, 362 VDF_MEASUREMENT_INITIAL_ROUNDS, 363 VDF_MEASUREMENT_MIN_ELAPSED, 364 VDF_MEASUREMENT_MAX_ROUNDS, 365 ); 366 let rounds = extrapolate_vdf_rounds( 367 measured_rounds, 368 elapsed, 369 Duration::from_millis(VDF_TARGET_BLOCK_MS), 370 ); 371 println!( 372 "measured {measured_rounds} VDF rounds in {:.3}ms; initial VDF rounds: {rounds}", 373 elapsed.as_secs_f64() * 1000.0 374 ); 375 rounds 376 } 377 378 fn measure_vdf_rounds( 379 seed: &str, 380 initial_rounds: u64, 381 min_elapsed: Duration, 382 max_rounds_per_attempt: u64, 383 ) -> (u64, Duration) { 384 let mut rounds = initial_rounds.max(1).min(max_rounds_per_attempt.max(1)); 385 let mut measured_rounds = 0_u64; 386 let mut measured_elapsed = Duration::ZERO; 387 388 loop { 389 let started = Instant::now(); 390 let _ = run_vdf(seed, rounds); 391 measured_elapsed += started.elapsed(); 392 measured_rounds = measured_rounds.saturating_add(rounds); 393 394 if measured_elapsed >= min_elapsed || rounds >= max_rounds_per_attempt { 395 return (measured_rounds, measured_elapsed); 396 } 397 rounds = rounds.saturating_mul(2).min(max_rounds_per_attempt); 398 } 399 } 400 401 fn extrapolate_vdf_rounds(measured_rounds: u64, elapsed: Duration, target: Duration) -> u64 { 402 let elapsed_ns = elapsed.as_nanos().max(1); 403 let target_ns = target.as_nanos().max(1); 404 let rounds = u128::from(measured_rounds) 405 .saturating_mul(target_ns) 406 .saturating_div(elapsed_ns) 407 .max(1); 408 rounds.min(u128::from(MAX_VDF_ROUNDS)) as u64 409 } 410 411 async fn join_chain_ledger(join_peers: &[String], advertised_addr: SocketAddr) -> Result<Ledger> { 412 let mut errors = Vec::new(); 413 for peer in join_peers { 414 match p2p::fetch_snapshot_with_announcement(peer, Some(advertised_addr)).await { 415 Ok(snapshot) => { 416 let height = snapshot 417 .blocks 418 .last() 419 .map(|block| block.height) 420 .unwrap_or(0); 421 println!("joined chain from {peer} at height {height}"); 422 return Ledger::from_snapshot(snapshot); 423 } 424 Err(error) => { 425 errors.push(format!("{peer}: {error:#}")); 426 } 427 } 428 } 429 430 bail!( 431 "could not join any requested peer; refusing to start a separate chain: {}", 432 errors.join("; ") 433 ) 434 } 435 436 async fn run_automatic_finalizer(node: SharedNode, gossip: p2p::GossipNetwork, debug: bool) { 437 let mut last_logged_skip: Option<(u64, String)> = None; 438 loop { 439 if !node.lock().await.has_real_chain() { 440 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 441 continue; 442 } 443 let (height, plan, outbox) = { 444 let mut node = node.lock().await; 445 let height = node.chain_height(); 446 let plan = node.prepare_automatic_finalization(now_ms()); 447 let outbox = node.drain_outbox(); 448 (height, plan, outbox) 449 }; 450 451 if let Err(error) = gossip.broadcast(outbox).await { 452 if debug { 453 eprintln!("p2p broadcast failed after automatic burn: {error:#}"); 454 } 455 } 456 457 let Some(work) = plan.work else { 458 if let Some(reason) = &plan.skipped_reason { 459 let skip = (height, reason.clone()); 460 if debug && last_logged_skip.as_ref() != Some(&skip) { 461 println!("auto-finalization skipped at height {height}: {reason}"); 462 last_logged_skip = Some(skip); 463 } 464 } 465 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 466 continue; 467 }; 468 469 last_logged_skip = None; 470 if debug { 471 println!( 472 "leader selected locally for candidate block {}; running VDF for {} rounds", 473 work.height(), 474 work.vdf_rounds() 475 ); 476 } 477 478 let seed = work.vdf_seed().to_string(); 479 let rounds = work.vdf_rounds(); 480 let publish_at_ms = work.timestamp_ms(); 481 let vdf_output = match tokio::task::spawn_blocking(move || run_vdf(&seed, rounds)).await { 482 Ok(output) => output, 483 Err(error) => { 484 if debug { 485 eprintln!("VDF worker failed: {error:#}"); 486 } 487 continue; 488 } 489 }; 490 491 let completed_at_ms = now_ms(); 492 let publish_timestamp_ms = completed_at_ms.max(publish_at_ms); 493 if completed_at_ms < publish_at_ms { 494 let wait_ms = publish_at_ms - completed_at_ms; 495 if debug { 496 println!( 497 "VDF completed early for candidate block {}; waiting {:.3}s for rank time slot", 498 work.height(), 499 wait_ms as f64 / 1000.0 500 ); 501 } 502 tokio::time::sleep(std::time::Duration::from_millis(wait_ms)).await; 503 } 504 505 let (finalized, outbox) = { 506 let mut node = node.lock().await; 507 let finalized = node.complete_prepared_block_at(work, vdf_output, publish_timestamp_ms); 508 match &finalized { 509 Ok(block) => node.record_automatic_finalization_status(format!( 510 "finalized block {} ({})", 511 block.height, block.hash 512 )), 513 Err(error) => node 514 .record_automatic_finalization_status(format!("skipped after VDF: {error:#}")), 515 } 516 let outbox = node.drain_outbox(); 517 (finalized, outbox) 518 }; 519 520 match finalized { 521 Ok(block) if debug => { 522 println!("auto-finalized block {} ({})", block.height, block.hash); 523 } 524 Ok(_) => {} 525 Err(error) if debug => println!("auto-finalization skipped after VDF: {error:#}"), 526 Err(_) => {} 527 } 528 529 if let Err(error) = gossip.broadcast(outbox).await { 530 if debug { 531 eprintln!("p2p broadcast failed after automatic block: {error:#}"); 532 } 533 } 534 535 tokio::task::yield_now().await; 536 } 537 } 538 539 async fn run_automatic_pow_miner(node: SharedNode, gossip: p2p::GossipNetwork, debug: bool) { 540 loop { 541 tokio::time::sleep(std::time::Duration::from_secs(1)).await; 542 let (height, job) = { 543 let mut node = node.lock().await; 544 if !node.pow_mining_enabled() { 545 continue; 546 } 547 if !node.has_real_chain() { 548 continue; 549 } 550 let height = node.chain_height(); 551 let job = match node.prepare_automatic_pow_mining_job() { 552 Ok(job) => job, 553 Err(error) => { 554 node.record_automatic_pow_mining_error(format!( 555 "automatic PoW mining failed: {error:#}" 556 )); 557 None 558 } 559 }; 560 (height, job) 561 }; 562 let Some(job) = job else { 563 continue; 564 }; 565 566 let search = tokio::task::spawn_blocking(move || job.search()).await; 567 let (pow_mined, outbox) = { 568 let mut node = node.lock().await; 569 let pow_mined = match search { 570 Ok(Ok((job, outcome))) => { 571 match node.finish_automatic_pow_mining_job(job, outcome) { 572 Ok(tx) => tx, 573 Err(error) => { 574 node.record_automatic_pow_mining_error(format!( 575 "automatic PoW mining failed: {error:#}" 576 )); 577 None 578 } 579 } 580 } 581 Ok(Err(error)) => { 582 node.record_automatic_pow_mining_error(format!( 583 "automatic PoW mining failed: {error:#}" 584 )); 585 None 586 } 587 Err(error) => { 588 node.record_automatic_pow_mining_error(format!( 589 "automatic PoW mining task failed: {error:#}" 590 )); 591 None 592 } 593 }; 594 let outbox = node.drain_outbox(); 595 (pow_mined, outbox) 596 }; 597 598 if let Err(error) = gossip.broadcast(outbox).await { 599 if debug { 600 eprintln!("p2p broadcast failed after automatic PoW mining: {error:#}"); 601 } 602 } 603 604 if debug { 605 if let Some(tx) = &pow_mined { 606 println!( 607 "auto-pow queued mine action for height {} ({})", 608 height, 609 tx.signature() 610 ); 611 } 612 } 613 } 614 } 615 616 async fn run_peer_sync(node: SharedNode, gossip: p2p::GossipNetwork, debug: bool) { 617 loop { 618 tokio::time::sleep(std::time::Duration::from_secs(5)).await; 619 let envelopes = { 620 let mut node = node.lock().await; 621 let mut envelopes = vec![node.peer_status()]; 622 envelopes.extend(node.drain_outbox()); 623 envelopes.extend(node.mempool_gossip()); 624 envelopes 625 }; 626 let mut envelopes = envelopes; 627 envelopes.push(gossip.peer_exchange().await); 628 if let Err(error) = gossip.broadcast(envelopes).await { 629 if debug { 630 eprintln!("p2p sync gossip failed: {error:#}"); 631 } 632 } 633 } 634 } 635 636 async fn run_chain_persistence( 637 node: SharedNode, 638 store: SqliteChainStore, 639 ui_data_store: SqliteUiDataStore, 640 ui_config: Arc<Mutex<config_store::UiConfig>>, 641 initial_saved_tip: Option<String>, 642 ) { 643 run_chain_persistence_with_interval( 644 node, 645 store, 646 ui_data_store, 647 ui_config, 648 Duration::from_secs(2), 649 initial_saved_tip, 650 ) 651 .await; 652 } 653 654 async fn run_chain_persistence_with_interval( 655 node: SharedNode, 656 store: SqliteChainStore, 657 ui_data_store: SqliteUiDataStore, 658 ui_config: Arc<Mutex<config_store::UiConfig>>, 659 interval: Duration, 660 initial_saved_tip: Option<String>, 661 ) { 662 let mut last_saved_tip = initial_saved_tip; 663 loop { 664 tokio::time::sleep(interval).await; 665 let snapshot = { 666 let node = node.lock().await; 667 if !node.has_real_chain() { 668 continue; 669 } 670 node.chain_snapshot() 671 }; 672 let Some(tip_hash) = snapshot.blocks.last().map(|block| block.hash.clone()) else { 673 continue; 674 }; 675 if last_saved_tip.as_deref() == Some(tip_hash.as_str()) { 676 continue; 677 } 678 679 let keep_metrics = ui_config.lock().await.keep_track_of_metrics; 680 match persist_chain_and_project_ui_data(&store, &ui_data_store, snapshot, keep_metrics) 681 .await 682 { 683 Ok(()) => last_saved_tip = Some(tip_hash), 684 Err(error) if debug_logging_enabled() => { 685 eprintln!("chain persistence failed: {error:#}") 686 } 687 Err(_) => {} 688 } 689 } 690 } 691 692 async fn persist_chain_and_project_ui_data( 693 store: &SqliteChainStore, 694 ui_data_store: &SqliteUiDataStore, 695 snapshot: ChainSnapshot, 696 keep_metrics: bool, 697 ) -> Result<()> { 698 persist_chain_snapshot(store, snapshot.clone()).await?; 699 project_ui_data_store(ui_data_store, snapshot, keep_metrics).await 700 } 701 702 async fn persist_chain_snapshot(store: &SqliteChainStore, snapshot: ChainSnapshot) -> Result<()> { 703 let store = store.clone(); 704 tokio::task::spawn_blocking(move || store.save(&snapshot)) 705 .await 706 .context("chain persistence worker failed")??; 707 Ok(()) 708 } 709 710 async fn warm_ui_data_store( 711 store: &SqliteUiDataStore, 712 snapshot: ChainSnapshot, 713 keep_metrics: bool, 714 ) -> Result<()> { 715 println!("warming UI data database..."); 716 let started = Instant::now(); 717 project_ui_data_store(store, snapshot, keep_metrics).await?; 718 println!( 719 "UI data database ready in {:.2}s", 720 started.elapsed().as_secs_f64() 721 ); 722 Ok(()) 723 } 724 725 async fn project_ui_data_store( 726 store: &SqliteUiDataStore, 727 snapshot: ChainSnapshot, 728 keep_metrics: bool, 729 ) -> Result<()> { 730 let store = store.clone(); 731 tokio::task::spawn_blocking(move || store.project_snapshot(&snapshot, keep_metrics)) 732 .await 733 .context("UI data projection worker failed")??; 734 Ok(()) 735 } 736 737 async fn clear_ui_data_store(store: &SqliteUiDataStore) -> Result<()> { 738 println!("clearing UI data database..."); 739 let started = Instant::now(); 740 let store = store.clone(); 741 tokio::task::spawn_blocking(move || store.clear_all()) 742 .await 743 .context("UI data cleanup worker failed")??; 744 println!( 745 "UI data database ready in {:.2}s", 746 started.elapsed().as_secs_f64() 747 ); 748 Ok(()) 749 } 750 751 #[cfg(test)] 752 #[path = "main_tests.rs"] 753 mod tests;