status.rs (8444B)
1 use std::collections::BTreeMap; 2 3 use anyhow::Result; 4 5 use crate::{ 6 adapters::config_store::{MAX_POW_MINING_WORKERS, clamp_pow_mining_workers}, 7 domain::{Amount, MINE_FINALIZER_FEE, Transaction, VDF_TARGET_BLOCK_MS}, 8 }; 9 10 use super::{ 11 LaunchProfileStatus, MiningStatus, NodeCore, NodeStatus, StratumStatus, 12 helpers::{transaction_input_total_from_outputs, transaction_output_total_for_address}, 13 now_ms, 14 }; 15 16 impl NodeCore { 17 pub fn status(&self) -> NodeStatus { 18 let chain = self.ledger.light_status(); 19 let launch_profile = self.ledger.launch_profile(); 20 let current_leader = self.ledger.expected_leader_for_next_block(); 21 let wallet_is_current_leader = current_leader 22 .as_deref() 23 .is_none_or(|leader| leader == self.wallet.address()); 24 25 NodeStatus { 26 app_version: env!("CARGO_PKG_VERSION").to_string(), 27 wallet_address: self.wallet.address().to_string(), 28 wallet_balance: self.wallet_projected_balance(), 29 wallet_locked: self.wallet.is_locked(), 30 launch_profile: LaunchProfileStatus { 31 profile_id: launch_profile.profile_id.clone(), 32 profile_hash: chain.launch_profile_hash.clone(), 33 ticket_maturity_delay_heights: launch_profile.ticket_maturity_delay_heights, 34 ticket_expiry_window_heights: launch_profile.ticket_expiry_window_heights, 35 mine_difficulty_bits: launch_profile.mine_difficulty_bits, 36 }, 37 mining: MiningStatus { 38 automatic: self.automatic_mining_enabled, 39 pow_mining_enabled: self.pow_mining_enabled, 40 pow_mining_workers: self.pow_mining_workers, 41 max_pow_mining_workers: MAX_POW_MINING_WORKERS, 42 burn_per_block: self.burn_per_block, 43 automatic_burn_fee: self.burn_fee, 44 automatic_pow_mine_fee: MINE_FINALIZER_FEE, 45 last_auto_pow_mine_anchor: self.last_auto_pow_mine_anchor.clone(), 46 last_auto_pow_mine_status: if self.pow_mining_enabled && !self.has_real_chain() { 47 Some("waiting for a real chain before PoW mining can start".to_string()) 48 } else { 49 self.last_auto_pow_mine_status.clone() 50 }, 51 vdf_rounds: self.ledger.vdf_rounds(), 52 vdf_target_block_ms: VDF_TARGET_BLOCK_MS, 53 current_leader, 54 wallet_is_current_leader, 55 last_auto_burn_height: self.last_auto_burn_height, 56 recovery_vdf_top_rank_percent: self.recovery_vdf_top_rank_percent, 57 }, 58 stratum: StratumStatus { 59 enabled: false, 60 listen_addr: None, 61 }, 62 chain, 63 } 64 } 65 66 fn wallet_projected_balance(&self) -> Amount { 67 let address = self.wallet.address(); 68 let mut balance = self.ledger.balance_of(address); 69 let confirmed_outputs = self 70 .ledger 71 .utxos_for_address(address) 72 .into_iter() 73 .map(|(outpoint, output)| (outpoint, output.amount)) 74 .collect::<BTreeMap<_, _>>(); 75 76 for (commitment, payload) in &self.owned_blinded_payloads { 77 if !self.ledger.has_unrevealed_blinded_transaction(commitment) { 78 continue; 79 } 80 let output_total = transaction_output_total_for_address(payload, address); 81 if self.ledger.has_active_blinded_transaction(commitment) { 82 balance = balance.saturating_add(output_total); 83 } else { 84 let input_total = 85 transaction_input_total_from_outputs(payload, address, &confirmed_outputs); 86 balance = balance 87 .saturating_sub(input_total) 88 .saturating_add(output_total); 89 } 90 } 91 92 if let Some((height, burn)) = &self.local_block_anchor_burn { 93 if *height == self.ledger.height() && !self.ledger.has_transaction(burn.signature()) { 94 let output_total = transaction_output_total_for_address(burn, address); 95 let input_total = 96 transaction_input_total_from_outputs(burn, address, &confirmed_outputs); 97 balance = balance 98 .saturating_sub(input_total) 99 .saturating_add(output_total); 100 } 101 } 102 103 balance 104 } 105 106 pub fn set_burn_per_block(&mut self, amount: Amount) -> Result<Option<Transaction>> { 107 self.set_automatic_burn(amount, self.burn_fee) 108 } 109 110 pub fn set_automatic_burn( 111 &mut self, 112 amount: Amount, 113 fee: Amount, 114 ) -> Result<Option<Transaction>> { 115 self.set_automatic_burn_settings(amount > 0, amount, fee) 116 } 117 118 pub fn set_automatic_burn_settings( 119 &mut self, 120 enabled: bool, 121 amount: Amount, 122 fee: Amount, 123 ) -> Result<Option<Transaction>> { 124 let was_disabled = !self.automatic_mining_enabled || self.burn_per_block == 0; 125 self.automatic_mining_enabled = enabled; 126 self.burn_per_block = amount; 127 self.burn_fee = fee; 128 if was_disabled && enabled && amount > 0 { 129 self.last_auto_burn_height = None; 130 self.last_auto_anchor_burn_height = None; 131 } 132 self.prepare_automatic_burn(now_ms()) 133 } 134 135 pub fn set_pow_mining_enabled(&mut self, enabled: bool) { 136 self.pow_mining_enabled = enabled; 137 self.auto_pow_mine_cursor = None; 138 if !enabled { 139 self.last_auto_pow_mine_anchor = None; 140 self.last_auto_pow_mine_status = None; 141 } else { 142 self.last_auto_pow_mine_status = 143 Some("waiting for next automatic PoW mining tick".to_string()); 144 } 145 } 146 147 pub fn pow_mining_enabled(&self) -> bool { 148 self.pow_mining_enabled 149 } 150 151 pub fn set_pow_mining_workers(&mut self, workers: u8) { 152 let workers = clamp_pow_mining_workers(workers); 153 if self.pow_mining_workers != workers { 154 self.pow_mining_workers = workers; 155 self.auto_pow_mine_cursor = None; 156 if self.pow_mining_enabled { 157 self.last_auto_pow_mine_status = 158 Some("waiting for next automatic PoW mining tick".to_string()); 159 } 160 } 161 } 162 163 pub fn pow_mining_workers(&self) -> u8 { 164 self.pow_mining_workers 165 } 166 167 pub fn set_recovery_vdf_top_rank_percent(&mut self, percent: u8) { 168 self.recovery_vdf_top_rank_percent = percent.min(100); 169 } 170 } 171 172 #[cfg(test)] 173 mod tests { 174 use std::collections::BTreeMap; 175 176 use crate::{ 177 app::{NodeConfig, NodeCore}, 178 domain::{Ledger, Wallet}, 179 }; 180 181 #[test] 182 fn status_reports_package_version() { 183 let wallet = Wallet::from_seed("status-version-wallet"); 184 let node = NodeCore::new(NodeConfig { 185 wallet, 186 genesis_allocations: BTreeMap::new(), 187 vdf_rounds: 1, 188 burn_per_block: 0, 189 burn_fee: 0, 190 pow_mining_workers: 1, 191 recovery_vdf_top_rank_percent: 100, 192 }); 193 194 assert_eq!(node.status().app_version, env!("CARGO_PKG_VERSION")); 195 } 196 197 #[test] 198 fn status_omits_full_balance_map_for_ui_polling() { 199 let wallet = Wallet::from_seed("status-light-wallet"); 200 let mut allocations = BTreeMap::new(); 201 allocations.insert(wallet.address().to_string(), 10); 202 allocations.insert( 203 Wallet::from_seed("status-light-peer").address().to_string(), 204 5, 205 ); 206 let ledger = Ledger::new(allocations, 1); 207 let node = NodeCore::from_ledger(wallet, ledger, 0); 208 209 let status = node.status(); 210 211 assert_eq!(status.wallet_balance, 10); 212 assert!(status.chain.balances.is_empty()); 213 } 214 215 #[test] 216 fn automatic_pow_status_reports_setup_placeholder_wait() { 217 let wallet = Wallet::from_seed("automatic-pow-setup-placeholder-wallet"); 218 let ledger = Ledger::new(BTreeMap::new(), 1); 219 let mut node = NodeCore::from_ledger(wallet, ledger, 0); 220 221 node.set_pow_mining_enabled(true); 222 223 assert_eq!( 224 node.status().mining.last_auto_pow_mine_status.as_deref(), 225 Some("waiting for a real chain before PoW mining can start") 226 ); 227 } 228 }