iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

commit cbbfb4152d54dccc4f4d75fb6fcce85f5d0e42c9
parent 3b5fdf77e336525351313e465a0ae27af2478d86
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Sat,  8 Aug 2026 22:32:26 +0200

Add configurable PoW mining workers

Diffstat:
Msrc/adapters/config_store.rs | 20++++++++++++++++++++
Msrc/adapters/http.rs | 28+++++++++++++++++++++++-----
Msrc/app.rs | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/main.rs | 1+
Mtests/iuna.rs | 13+++++++++----
Mwww/assets/iuna-ui.js | 35+++++++++++++++++++++++++++++++----
6 files changed, 188 insertions(+), 31 deletions(-)

diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs @@ -17,6 +17,8 @@ const LEGACY_AMOUNT_UNIT_PRE_RENAME: &str = concat!("micro", "l", "uun"); pub const DEFAULT_BURN_AMOUNT: Amount = MICRO_IUNA / 10_000; pub const DEFAULT_BURN_FEE: Amount = DEFAULT_BURN_AMOUNT; pub const DEFAULT_RECOVERY_VDF_TOP_RANK_PERCENT: u8 = 50; +pub const DEFAULT_POW_MINING_WORKERS: u8 = 1; +pub const MAX_POW_MINING_WORKERS: u8 = 32; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct UiConfig { @@ -25,6 +27,7 @@ pub struct UiConfig { pub auth_password_hash: Option<String>, pub mining_enabled: bool, pub pow_mining_enabled: bool, + pub pow_mining_workers: u8, pub burn_per_block: Amount, pub burn_fee: Amount, pub recovery_vdf_top_rank_percent: u8, @@ -41,6 +44,7 @@ impl Default for UiConfig { auth_password_hash: None, mining_enabled: false, pow_mining_enabled: false, + pow_mining_workers: DEFAULT_POW_MINING_WORKERS, burn_per_block: DEFAULT_BURN_AMOUNT, burn_fee: DEFAULT_BURN_FEE, recovery_vdf_top_rank_percent: DEFAULT_RECOVERY_VDF_TOP_RANK_PERCENT, @@ -64,6 +68,8 @@ struct ConfigFile { mining_enabled: Option<bool>, #[serde(default)] pow_mining_enabled: bool, + #[serde(default = "default_pow_mining_workers")] + pow_mining_workers: u8, #[serde(default)] burn_per_block: Amount, #[serde(default)] @@ -103,6 +109,7 @@ pub fn save(path: &Path, config: &UiConfig) -> Result<()> { auth_password_hash: config.auth_password_hash.clone(), mining_enabled: Some(config.mining_enabled), pow_mining_enabled: config.pow_mining_enabled, + pow_mining_workers: clamp_pow_mining_workers(config.pow_mining_workers), burn_per_block: config.burn_per_block, burn_fee: Some(config.burn_fee), recovery_vdf_top_rank_percent: Some(config.recovery_vdf_top_rank_percent), @@ -152,6 +159,7 @@ fn load(path: &Path) -> Result<UiConfig> { auth_password_hash: stored.auth_password_hash, mining_enabled: stored.mining_enabled.unwrap_or(stored.burn_per_block > 0), pow_mining_enabled: stored.pow_mining_enabled, + pow_mining_workers: clamp_pow_mining_workers(stored.pow_mining_workers), burn_per_block: stored.burn_per_block.saturating_mul(scale), burn_fee: stored .burn_fee @@ -168,6 +176,14 @@ fn load(path: &Path) -> Result<UiConfig> { }) } +pub fn clamp_pow_mining_workers(workers: u8) -> u8 { + workers.clamp(1, MAX_POW_MINING_WORKERS) +} + +fn default_pow_mining_workers() -> u8 { + DEFAULT_POW_MINING_WORKERS +} + fn create_config_file(path: &Path) -> Result<File> { let mut options = OpenOptions::new(); options.write(true).create(true).truncate(true); @@ -203,6 +219,7 @@ mod tests { assert!(stored.contains("\"auth_password_hash\": null")); assert!(stored.contains("\"mining_enabled\": false")); assert!(stored.contains("\"pow_mining_enabled\": false")); + assert!(stored.contains("\"pow_mining_workers\": 1")); assert!(stored.contains("\"burn_per_block\": 100")); assert!(stored.contains("\"burn_fee\": 100")); assert!(!stored.contains("\"pow_mine_fee\"")); @@ -225,6 +242,7 @@ mod tests { auth_password_hash: Some("auth-hash".to_string()), mining_enabled: true, pow_mining_enabled: true, + pow_mining_workers: 4, burn_per_block: 50 * MICRO_IUNA, burn_fee: 3 * MICRO_IUNA, keep_track_of_metrics: true, @@ -241,6 +259,7 @@ mod tests { assert_eq!(config.auth_password_hash.as_deref(), Some("auth-hash")); assert!(config.mining_enabled); assert!(config.pow_mining_enabled); + assert_eq!(config.pow_mining_workers, 4); assert_eq!(config.burn_per_block, 50 * MICRO_IUNA); assert_eq!(config.burn_fee, 3 * MICRO_IUNA); assert!(config.keep_track_of_metrics); @@ -284,6 +303,7 @@ mod tests { assert!(config.setup_complete); assert!(!config.mining_enabled); assert!(!config.pow_mining_enabled); + assert_eq!(config.pow_mining_workers, 1); assert_eq!(config.burn_per_block, 0); assert_eq!(config.burn_fee, DEFAULT_BURN_FEE); assert!(!config.keep_track_of_metrics); diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -153,6 +153,7 @@ struct RecoveryVdfSettingsForm { #[derive(Debug, Deserialize)] struct PowMiningForm { enabled: bool, + workers: Option<u8>, } #[derive(Debug, Deserialize)] @@ -1097,7 +1098,7 @@ async fn api_pow_mining_form( State(state): State<HttpState>, Form(form): Form<PowMiningForm>, ) -> Json<ActionResponse> { - action_json(set_pow_mining(&state, form.enabled).await) + action_json(set_pow_mining(&state, form.enabled, form.workers).await) } async fn api_metrics_settings_form( @@ -1227,21 +1228,28 @@ async fn persist_burn_settings_config( config_store::save(config_path, &config) } -async fn set_pow_mining(state: &HttpState, enabled: bool) -> Result<()> { +async fn set_pow_mining(state: &HttpState, enabled: bool, workers: Option<u8>) -> Result<()> { + let workers = match workers { + Some(workers) => workers, + None => state.ui_config.lock().await.pow_mining_workers, + }; { let mut node = state.node.lock().await; + node.set_pow_mining_workers(workers); node.set_pow_mining_enabled(enabled); } - persist_pow_mining_config(&state.ui_config, &state.config_path, enabled).await + persist_pow_mining_config(&state.ui_config, &state.config_path, enabled, workers).await } async fn persist_pow_mining_config( ui_config: &Arc<Mutex<UiConfig>>, config_path: &Path, enabled: bool, + workers: u8, ) -> Result<()> { let mut config = ui_config.lock().await; config.pow_mining_enabled = enabled; + config.pow_mining_workers = config_store::clamp_pow_mining_workers(workers); config_store::save(config_path, &config) } @@ -3228,7 +3236,7 @@ const INDEX_HTML: &str = r#"<!doctype html> .panel-description { max-width: 760px; margin: -4px 0 12px; color: #9eb3bc; font-size: 13px; line-height: 1.45; } .mining-form { width: 100%; display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } .burn-fields { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } - .mine-action-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; align-items: center; } + .mine-action-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 12px; align-items: center; } .mine-settings-form { display: grid; gap: 10px; } .mine-fee-fields { display: flex; flex-wrap: wrap; gap: 10px; align-items: end; } .fee-preview { flex-basis: 100%; color: #9eb3bc; font-size: 12px; font-weight: 700; } @@ -3276,6 +3284,9 @@ const INDEX_HTML: &str = r#"<!doctype html> .toggle-switch.active .toggle-thumb { transform: translateX(20px); background: #d5f55f; } .toggle-switch:focus-within .toggle-track { outline: 2px solid #d5f55f; outline-offset: 2px; } .toggle-text { min-width: 22px; text-align: right; } + .compact-number-field { display: inline-flex; align-items: center; gap: 8px; color: #9fa8ad; font-size: 12px; font-weight: 850; } + .compact-number-field input { width: 58px; min-width: 0; border: 1px solid #3a4248; border-radius: 8px; padding: 7px 8px; background: #101215; color: #dce4e7; font: inherit; font-variant-numeric: tabular-nums; } + .compact-number-field input:focus { border-color: #d5f55f; outline: 2px solid rgba(213,245,95,.2); outline-offset: 2px; } .receive-address { display: grid; gap: 8px; } .address-box { border: 1px solid #2f363c; border-radius: 8px; padding: 11px; background: #111316; } .panel-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 12px; } @@ -3680,6 +3691,10 @@ const INDEX_HTML: &str = r#"<!doctype html> <span class="toggle-track"><span class="toggle-thumb"></span></span> <span class="toggle-text" x-text="powMiningEnabled ? 'On' : 'Off'"></span> </label> + <label class="compact-number-field" title="Local PoW worker count"> + <span>Workers</span> + <input type="number" min="1" :max="maxPowMiningWorkers" :value="powMiningWorkers" @change="setPowMiningWorkers($event.target.value)"> + </label> </div> <div class="fee-preview" x-text="autoPowStatusLabel()"></div> </div> @@ -5833,9 +5848,11 @@ mod tests { assert!(super::INDEX_HTML.contains("aria-label=\"Local mining status\"")); assert!(super::INDEX_HTML.contains("PoB State")); assert!(super::INDEX_HTML.contains("PoW State")); + assert!(super::INDEX_HTML.contains("Workers")); assert!(super::INDEX_HTML.contains("Selected Finalizer")); assert!(app_js.contains("pobStatusLabel()")); assert!(app_js.contains("powStatusShortLabel()")); + assert!(app_js.contains("setPowMiningWorkers(workers)")); assert!(app_js.contains("localMiningMempoolLabel()")); assert!(!super::INDEX_HTML.contains("Needs burns")); } @@ -6051,12 +6068,13 @@ mod tests { let initial_config = ui_config.lock().await.clone(); config_store::save(&config_path, &initial_config).expect("initial config should save"); - persist_pow_mining_config(&ui_config, &config_path, true) + persist_pow_mining_config(&ui_config, &config_path, true, 4) .await .unwrap(); let config = config_store::load_or_create(&config_path).unwrap(); assert!(config.pow_mining_enabled); + assert_eq!(config.pow_mining_workers, 4); } #[test] diff --git a/src/app.rs b/src/app.rs @@ -12,6 +12,9 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use tokio::sync::Mutex; +use crate::adapters::config_store::{ + DEFAULT_POW_MINING_WORKERS, MAX_POW_MINING_WORKERS, clamp_pow_mining_workers, +}; use crate::domain::{ Amount, BlindedReveal, BlindedTransaction, Block, BuiltBlindedTransaction, BurnLeaderRank, ChainSnapshot, ChainStatus, DEFAULT_FEE_PER_BYTE, DEFAULT_TRANSACTION_FEE, Ledger, @@ -34,7 +37,7 @@ pub const PEER_MISBEHAVIOR_BAN_SCORE: u32 = 3; pub const PEER_MISBEHAVIOR_BAN_MS: u64 = 10 * 60 * 1_000; pub const PEER_CLOCK_OFFSET_ACCEPTANCE_MS: i64 = 10 * 60 * 1_000; const PEER_CLOCK_OFFSET_STALE_MS: u64 = 20 * 60 * 1_000; -const AUTO_POW_NONCE_ATTEMPTS_PER_TICK: u64 = 8; +const AUTO_POW_NONCE_ATTEMPTS_PER_WORKER_TICK: u64 = 100_000; const AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS: u64 = 60_000; const AUTO_BLOCK_ANCHOR_BURN_AMOUNT: Amount = 1; const AUTO_BLOCK_ANCHOR_BURN_FEE: Amount = 0; @@ -55,6 +58,7 @@ pub struct NodeConfig { pub vdf_rounds: u64, pub burn_per_block: Amount, pub burn_fee: Amount, + pub pow_mining_workers: u8, pub recovery_vdf_top_rank_percent: u8, } @@ -202,6 +206,8 @@ pub struct LaunchProfileStatus { pub struct MiningStatus { pub automatic: bool, pub pow_mining_enabled: bool, + pub pow_mining_workers: u8, + pub max_pow_mining_workers: u8, pub burn_per_block: Amount, pub automatic_burn_fee: Amount, pub automatic_pow_mine_fee: Amount, @@ -251,6 +257,7 @@ pub struct NodeCore { ledger: Ledger, automatic_mining_enabled: bool, pow_mining_enabled: bool, + pow_mining_workers: u8, burn_per_block: Amount, burn_fee: Amount, recovery_vdf_top_rank_percent: u8, @@ -278,6 +285,7 @@ impl NodeCore { config.burn_per_block, config.burn_fee, ); + node.set_pow_mining_workers(config.pow_mining_workers); node.set_recovery_vdf_top_rank_percent(config.recovery_vdf_top_rank_percent); node } @@ -301,6 +309,7 @@ impl NodeCore { automatic_mining_enabled, burn_per_block, burn_fee, + DEFAULT_POW_MINING_WORKERS, 100, ) } @@ -333,6 +342,7 @@ impl NodeCore { automatic_mining_enabled, burn_per_block, burn_fee, + DEFAULT_POW_MINING_WORKERS, 100, ) } @@ -343,6 +353,7 @@ impl NodeCore { automatic_mining_enabled: bool, burn_per_block: Amount, burn_fee: Amount, + pow_mining_workers: u8, recovery_vdf_top_rank_percent: u8, ) -> Self { Self { @@ -350,6 +361,7 @@ impl NodeCore { ledger, automatic_mining_enabled, pow_mining_enabled: false, + pow_mining_workers: clamp_pow_mining_workers(pow_mining_workers), burn_per_block, burn_fee, recovery_vdf_top_rank_percent: recovery_vdf_top_rank_percent.min(100), @@ -656,6 +668,8 @@ impl NodeCore { mining: MiningStatus { automatic: self.automatic_mining_enabled, pow_mining_enabled: self.pow_mining_enabled, + pow_mining_workers: self.pow_mining_workers, + max_pow_mining_workers: MAX_POW_MINING_WORKERS, burn_per_block: self.burn_per_block, automatic_burn_fee: self.burn_fee, automatic_pow_mine_fee: MINE_FINALIZER_FEE, @@ -765,6 +779,22 @@ impl NodeCore { self.pow_mining_enabled } + pub fn set_pow_mining_workers(&mut self, workers: u8) { + let workers = clamp_pow_mining_workers(workers); + if self.pow_mining_workers != workers { + self.pow_mining_workers = workers; + self.auto_pow_mine_cursor = None; + if self.pow_mining_enabled { + self.last_auto_pow_mine_status = + Some("waiting for next automatic PoW mining tick".to_string()); + } + } + } + + pub fn pow_mining_workers(&self) -> u8 { + self.pow_mining_workers + } + pub fn set_recovery_vdf_top_rank_percent(&mut self, percent: u8) { self.recovery_vdf_top_rank_percent = percent.min(100); } @@ -1562,30 +1592,49 @@ impl NodeCore { .as_ref() .context("automatic PoW cursor was not initialized")? .clone(); - let outcome = self.wallet_build_ledger()?.search_mine( - wallet_address, - cursor.salt, - cursor.next_nonce, - AUTO_POW_NONCE_ATTEMPTS_PER_TICK, - )?; - let mut searched = outcome.attempts; + let budget = AUTO_POW_NONCE_ATTEMPTS_PER_WORKER_TICK + .saturating_mul(u64::from(self.pow_mining_workers)); + let mut remaining = budget; + let mut next_nonce = cursor.next_nonce; + let mut tick_attempts = 0_u64; + let mut queued = 0_u64; + let mut first_tx = None; + while remaining > 0 { + let outcome = self.wallet_build_ledger()?.search_mine( + wallet_address.clone(), + cursor.salt, + next_nonce, + remaining, + )?; + remaining = remaining.saturating_sub(outcome.attempts); + tick_attempts = tick_attempts.saturating_add(outcome.attempts); + next_nonce = outcome.next_nonce; + let Some(tx) = outcome.transaction else { + break; + }; + self.submit_public_mine_action(tx.clone())?; + queued = queued.saturating_add(1); + first_tx.get_or_insert(tx); + } + + let mut searched = tick_attempts; if let Some(cursor) = &mut self.auto_pow_mine_cursor { if cursor.anchor == anchor { - cursor.next_nonce = outcome.next_nonce; - cursor.searched = cursor.searched.saturating_add(outcome.attempts); + cursor.next_nonce = next_nonce; + cursor.searched = cursor.searched.saturating_add(tick_attempts); searched = cursor.searched; } } - let Some(tx) = outcome.transaction else { + let Some(tx) = first_tx else { self.last_auto_pow_mine_status = Some(format!( "searched {searched} PoW nonces for the current tip; no proof yet" )); return Ok(None); }; - self.submit_public_mine_action(tx.clone())?; self.last_auto_pow_mine_anchor = Some(anchor); self.last_auto_pow_mine_status = Some(format!( - "queued mine action after {searched} PoW nonce attempts for the current tip" + "queued {queued} mine action{} after {searched} PoW nonce attempts for the current tip", + if queued == 1 { "" } else { "s" } )); Ok(Some(tx)) } @@ -2496,7 +2545,10 @@ mod tests { RECOVERY_BLOCK_DELAY_MS, Transaction, VDF_TARGET_BLOCK_MS, Wallet, run_vdf, }; - use super::{GossipEnvelope, InMemoryNetwork, NodeConfig, NodeCore}; + use super::{ + DEFAULT_POW_MINING_WORKERS, GossipEnvelope, InMemoryNetwork, MAX_POW_MINING_WORKERS, + NodeConfig, NodeCore, + }; fn wallet_for_address<'a>(wallets: &'a [Wallet], address: &str) -> &'a Wallet { wallets @@ -2534,6 +2586,7 @@ mod tests { vdf_rounds: 10, burn_per_block: 1, burn_fee: 1, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }); @@ -2569,7 +2622,6 @@ mod tests { node.set_pow_mining_enabled(true); let first = node.prepare_automatic_mining(2); - assert!(node.ledger().pending().is_empty()); assert!(node.ledger().pending_blinded_transactions().len() <= 1); let first = std::iter::once(first) .chain((3..10_000).map(|timestamp| node.prepare_automatic_mining(timestamp))) @@ -2591,7 +2643,8 @@ mod tests { *difficulty_bits, node.ledger().current_mine_difficulty_bits() ); - assert_eq!(node.ledger().pending().len(), 1); + let first_pending = node.ledger().pending().len(); + assert!(first_pending >= 1); assert!(node.ledger().pending_blinded_transactions().is_empty()); assert!(node .drain_outbox() @@ -2603,7 +2656,7 @@ mod tests { .last_auto_pow_mine_status .as_deref() .unwrap_or_default() - .contains("queued mine action after") + .contains("queued") ); let second = (10_000..20_000) @@ -2614,7 +2667,7 @@ mod tests { second.pow_mined.as_ref().unwrap().signature(), first_mine.signature() ); - assert_eq!(node.ledger().pending().len(), 2); + assert!(node.ledger().pending().len() > first_pending); assert!(node.ledger().pending_blinded_transactions().is_empty()); } @@ -2629,6 +2682,7 @@ mod tests { vdf_rounds: 10, burn_per_block: 0, burn_fee: 0, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }); @@ -2661,6 +2715,7 @@ mod tests { vdf_rounds: 10, burn_per_block: 0, burn_fee: 0, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }); @@ -2678,6 +2733,34 @@ mod tests { } #[test] + fn automatic_pow_mining_workers_are_clamped_and_reported() { + let wallet = Wallet::from_seed("automatic-pow-workers-wallet"); + let mut node = NodeCore::new(NodeConfig { + wallet, + genesis_allocations: BTreeMap::new(), + vdf_rounds: 10, + burn_per_block: 0, + burn_fee: 0, + pow_mining_workers: 99, + recovery_vdf_top_rank_percent: 100, + }); + + assert_eq!(node.pow_mining_workers(), MAX_POW_MINING_WORKERS); + assert_eq!( + node.status().mining.max_pow_mining_workers, + MAX_POW_MINING_WORKERS + ); + + node.set_pow_mining_workers(0); + + assert_eq!(node.pow_mining_workers(), DEFAULT_POW_MINING_WORKERS); + assert_eq!( + node.status().mining.pow_mining_workers, + DEFAULT_POW_MINING_WORKERS + ); + } + + #[test] fn automatic_pow_mining_skips_unspendable_owned_blinded_payloads() { let alice = Wallet::from_seed("automatic-pow-stale-owned-blind-alice"); let bob = Wallet::from_seed("automatic-pow-stale-owned-blind-bob"); @@ -2741,6 +2824,7 @@ mod tests { vdf_rounds: 10, burn_per_block: 0, burn_fee: 0, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }); @@ -2816,6 +2900,7 @@ mod tests { vdf_rounds: 10, burn_per_block: 0, burn_fee: 0, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }); @@ -2843,6 +2928,7 @@ mod tests { vdf_rounds: 1, burn_per_block: 0, burn_fee: 0, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }); diff --git a/src/main.rs b/src/main.rs @@ -94,6 +94,7 @@ async fn main() -> Result<()> { initial_burn_fee, ), }; + node_core.set_pow_mining_workers(ui_config.pow_mining_workers); node_core.set_pow_mining_enabled(ui_config.pow_mining_enabled); node_core.set_recovery_vdf_top_rank_percent(ui_config.recovery_vdf_top_rank_percent); let node: SharedNode = Arc::new(Mutex::new(node_core)); diff --git a/tests/iuna.rs b/tests/iuna.rs @@ -29,6 +29,7 @@ fn node(_network_key: &str, wallet: Wallet, allocations: BTreeMap<String, Amount vdf_rounds: 25, burn_per_block: DEFAULT_BURN_PER_BLOCK, burn_fee: 1, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }) } @@ -946,16 +947,19 @@ fn pow_only_node_gossips_mine_action_to_pob_only_finalizer() { alice_node.receive(envelope).unwrap(); } - assert_eq!(alice_node.ledger().pending().len(), 1); + assert!(!alice_node.ledger().pending().is_empty()); assert!( alice_node .ledger() .pending_blinded_transactions() .is_empty() ); - assert_eq!( - alice_node.ledger().pending()[0].signature(), - mine.signature() + assert!( + alice_node + .ledger() + .pending() + .iter() + .any(|tx| tx.signature() == mine.signature()) ); } @@ -1828,6 +1832,7 @@ fn mined_block_gossip_does_not_include_full_chain_snapshot() { vdf_rounds: 10, burn_per_block: 1, burn_fee: 1, + pow_mining_workers: 1, recovery_vdf_top_rank_percent: 100, }); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -68,6 +68,8 @@ window.iunaApp = function iunaApp() { burnFeeDraft: "0.0001", miningEnabled: false, powMiningEnabled: false, + powMiningWorkers: 1, + maxPowMiningWorkers: 32, recoveryVdfTopRankPercent: 50, burnAmountDirty: false, transferTo: "", @@ -605,6 +607,9 @@ window.iunaApp = function iunaApp() { this.burnFee = status.mining?.automatic_burn_fee ?? this.burnFee; this.miningEnabled = status.mining?.automatic ?? this.miningEnabled; this.powMiningEnabled = status.mining?.pow_mining_enabled ?? this.powMiningEnabled; + this.powMiningWorkers = status.mining?.pow_mining_workers ?? this.powMiningWorkers; + this.maxPowMiningWorkers = + status.mining?.max_pow_mining_workers ?? this.maxPowMiningWorkers; if (!this.burnAmountDirty) { this.burnAmountDraft = this.amountLabel(this.burnAmount); this.burnFeeDraft = this.amountLabel(this.burnFee); @@ -1223,7 +1228,7 @@ window.iunaApp = function iunaApp() { this.powMiningEnabled = enabled; await this.postForm( "/api/settings/pow-mining", - { enabled }, + { enabled, workers: this.powMiningWorkers }, enabled ? "PoW mining turned on" : "PoW mining turned off" ); } catch (error) { @@ -1232,6 +1237,26 @@ window.iunaApp = function iunaApp() { } }, + async setPowMiningWorkers(workers) { + const previous = this.powMiningWorkers; + const parsed = Number.parseInt(workers, 10); + const clamped = Math.min( + this.maxPowMiningWorkers, + Math.max(1, Number.isFinite(parsed) ? parsed : 1) + ); + try { + this.powMiningWorkers = clamped; + await this.postForm( + "/api/settings/pow-mining", + { enabled: this.powMiningEnabled, workers: clamped }, + `PoW workers set to ${clamped}` + ); + } catch (error) { + this.powMiningWorkers = previous; + this.showFlash(error.message, "error"); + } + }, + async setKeepTrackOfMetrics(enabled) { const previous = this.keepTrackOfMetrics; try { @@ -1326,16 +1351,18 @@ window.iunaApp = function iunaApp() { if (this.status.wallet_locked) return "Wallet locked"; if (!this.powMiningEnabled) return "Off"; const status = this.status.mining?.last_auto_pow_mine_status || ""; - if (status.includes("queued mine action")) return "Queued"; + if (status.includes("queued")) return "Queued"; if (status.includes("searched")) return "Searching"; if (status.includes("waiting")) return "Waiting"; if (status.includes("failed")) return "Error"; - return "On"; + return `${this.powMiningWorkers} worker${this.powMiningWorkers === 1 ? "" : "s"}`; }, autoPowStatusLabel() { if (!this.powMiningEnabled) return "PoW mining is off"; - return this.status.mining?.last_auto_pow_mine_status || "Waiting for next automatic PoW mining tick"; + const status = + this.status.mining?.last_auto_pow_mine_status || "Waiting for next automatic PoW mining tick"; + return `${status} (${this.powMiningWorkers} worker${this.powMiningWorkers === 1 ? "" : "s"})`; }, currentFinalizerLabel() {