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