iuna

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

commit cef24ab982068d129428a00c46d102b597852421
parent cbf1eba2475b85b3d7d5ec4f8750edfc3d125a93
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Sat,  1 Aug 2026 21:18:18 +0200

Clarify PoW mining controls

Diffstat:
Msrc/adapters/config_store.rs | 24++++++++++++++++--------
Msrc/adapters/http.rs | 52++++++++++++++++++++++++++++++++++++++++------------
Msrc/main.rs | 6+++++-
Mwww/assets/iuna-ui.js | 56+++++++++++++++++++++++++++++++++++++++++++++++++++-----
4 files changed, 112 insertions(+), 26 deletions(-)

diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs @@ -9,14 +9,13 @@ use std::{ use anyhow::{Context, Result, bail}; use serde::{Deserialize, Serialize}; -use crate::domain::{ - Amount, DEFAULT_FEE_PER_BYTE, DEFAULT_MINE_REQUIRED_BURN_MULTIPLIER_BPS, MICRO_IUNA, -}; +use crate::domain::{Amount, DEFAULT_MINE_REQUIRED_BURN_MULTIPLIER_BPS, MICRO_IUNA}; const CONFIG_FILE_VERSION: u32 = 1; const AMOUNT_UNIT_MICROIUNA: &str = "microiuna"; const LEGACY_AMOUNT_UNIT_PRE_RENAME: &str = concat!("micro", "l", "uun"); -pub const DEFAULT_BURN_FEE: Amount = DEFAULT_FEE_PER_BYTE; +pub const DEFAULT_BURN_AMOUNT: Amount = MICRO_IUNA / 10_000; +pub const DEFAULT_BURN_FEE: Amount = DEFAULT_BURN_AMOUNT; #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct UiConfig { @@ -41,7 +40,7 @@ impl Default for UiConfig { auth_password_hash: None, mining_enabled: false, pow_mining_enabled: false, - burn_per_block: 0, + burn_per_block: DEFAULT_BURN_AMOUNT, burn_fee: DEFAULT_BURN_FEE, mine_required_burn_multiplier_bps: DEFAULT_MINE_REQUIRED_BURN_MULTIPLIER_BPS, keep_track_of_metrics: false, @@ -185,7 +184,7 @@ mod tests { use crate::domain::MICRO_IUNA; - use super::{DEFAULT_BURN_FEE, UiConfig, load_or_create, save}; + use super::{DEFAULT_BURN_AMOUNT, DEFAULT_BURN_FEE, UiConfig, load_or_create, save}; #[test] fn creates_default_config_file() { @@ -202,8 +201,8 @@ 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("\"burn_per_block\": 0")); - assert!(stored.contains("\"burn_fee\": 1")); + assert!(stored.contains("\"burn_per_block\": 100")); + assert!(stored.contains("\"burn_fee\": 100")); assert!(!stored.contains("\"pow_mine_fee\"")); assert!(stored.contains("\"mine_required_burn_multiplier_bps\": 8000")); assert!(stored.contains("\"keep_track_of_metrics\": false")); @@ -293,6 +292,15 @@ mod tests { } #[test] + fn default_burn_settings_are_prefilled_while_disabled() { + let config = UiConfig::default(); + + assert!(!config.mining_enabled); + assert_eq!(config.burn_per_block, DEFAULT_BURN_AMOUNT); + assert_eq!(config.burn_fee, DEFAULT_BURN_FEE); + } + + #[test] fn legacy_config_with_announce_address_keeps_public_node_enabled() { let dir = tempdir().unwrap(); let path = dir.path().join("config.json"); diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -2667,6 +2667,16 @@ const INDEX_HTML: &str = r#"<!doctype html> .mine-stat-label { display: flex; gap: 5px; align-items: center; color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; } .mine-stat-value { margin-top: 5px; color: #dce4e7; font-size: 14px; font-weight: 850; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; } .mine-stat-value.money { color: #d5f55f; } + .mine-reward-control { display: grid; gap: 7px; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; } + .mine-reward-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; color: #a8b2b8; font-size: 13px; font-weight: 800; } + .mine-reward-head strong { color: #d5f55f; font-size: 14px; font-variant-numeric: tabular-nums; } + .mine-reward-control input[type="range"] { width: 100%; min-width: 0; padding: 0; accent-color: #d5f55f; } + .mine-slider-hints { display: flex; justify-content: space-between; gap: 10px; color: #879198; font-size: 11px; font-weight: 750; } + .mine-include-status { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 7px; color: #9eb3bc; font-size: 12px; font-weight: 800; } + .mine-include-status.waiting { color: #ffd280; } + .mine-include-status.ready { color: #d5f55f; } + .mine-include-status.muted { color: #879198; } + .mine-save-row { display: flex; justify-content: flex-start; } .panel-separator { border-top: 1px solid #2f363c; margin: 14px 0 12px; } .stratum-config { display: grid; gap: 10px; } .stratum-note { max-width: 760px; color: #9eb3bc; font-size: 12px; line-height: 1.45; } @@ -2828,7 +2838,7 @@ const INDEX_HTML: &str = r#"<!doctype html> .block-card { flex-basis: 108px; } } </style> - <script defer src="/assets/iuna-ui.js?v=66"></script> + <script defer src="/assets/iuna-ui.js?v=69"></script> <script defer src="/assets/alpine.min.js"></script> </head> <body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak> @@ -3040,21 +3050,21 @@ const INDEX_HTML: &str = r#"<!doctype html> </div> <div class="panel"> <h3>Mine</h3> - <div class="panel-description">Mine with PoW to introduce new IUNA. Your mine action sets a minimum burn amount; the block finalizer must include that much burn to use it.</div> + <div class="panel-description">Search for PoW actions that mint IUNA when blocks have enough burns.</div> <form class="mine-settings-form" @submit.prevent="savePowMining"> <div class="mine-action-row"> <div class="mine-stats" aria-label="PoW issuance settings"> <div class="mine-stat"> - <div class="mine-stat-label">Miner cap</div> - <div class="mine-stat-value money">IUNA <span x-text="amountLabel(status.chain?.mine_reward ?? 0)"></span></div> + <div class="mine-stat-label">You receive</div> + <div class="mine-stat-value money">IUNA <span x-text="amountLabel(powMineNetReward())"></span></div> </div> <div class="mine-stat"> - <div class="mine-stat-label">Finalizer fee</div> - <div class="mine-stat-value">IUNA <span x-text="amountLabel(feeEstimates.mine?.fee ?? 0)"></span></div> + <div class="mine-stat-label">Needs burns</div> + <div class="mine-stat-value">IUNA <span x-text="amountLabel(powMineRequiredBurn())"></span></div> </div> <div class="mine-stat"> - <div class="mine-stat-label">Miner receives</div> - <div class="mine-stat-value money">IUNA <span x-text="amountLabel(powMineNetReward())"></span></div> + <div class="mine-stat-label">Finalizer earns</div> + <div class="mine-stat-value">IUNA <span x-text="amountLabel(feeEstimates.mine?.fee ?? 0)"></span></div> </div> <div class="mine-stat"> <div class="mine-stat-label">Difficulty <button class="info-button" type="button" @click="openPowDifficultyInfo" title="How difficulty is adjusted" aria-label="How PoW difficulty is adjusted">i</button></div> @@ -3067,9 +3077,18 @@ const INDEX_HTML: &str = r#"<!doctype html> <span class="toggle-text" x-text="powMiningEnabled ? 'On' : 'Off'"></span> </label> </div> - <label>Burn requirement multiplier<input x-model="powRequiredBurnMultiplierDraft" @input="powRequiredBurnMultiplierDirty = true" type="number" min="0.1" max="1" step="0.05"></label> - <div class="muted">Required burn is this multiplier times the minimum total burn amount from the last 10 non-genesis blocks, clamped between 0.1 and 1 IUNA.</div> - <div class="fee-preview" x-text="feeEstimateLabel('mine')"></div> + <div class="mine-reward-control"> + <div class="mine-reward-head"><span>Reward setting</span><strong>IUNA <span x-text="amountLabel(powMineNetReward())"></span></strong></div> + <input x-model="powRequiredBurnMultiplierDraft" @input="powRequiredBurnMultiplierDirty = true" type="range" min="0.1" max="1" step="0.05" aria-label="PoW reward setting"> + <div class="mine-slider-hints"><span>Easier to include</span><span>Higher reward</span></div> + <div class="mine-include-status" :class="powMineIncludeStatus().kind"> + <span x-text="powMineIncludeStatus().label"></span> + <span x-text="powMineIncludeStatus().recent"></span> + </div> + </div> + <div class="mine-save-row" x-show="powRequiredBurnMultiplierDirty"> + <button class="primary" type="submit">Save</button> + </div> <div class="fee-preview" x-text="autoPowStatusLabel()"></div> </form> <div class="panel-separator"></div> @@ -4683,13 +4702,22 @@ mod tests { #[test] fn metrics_screen_includes_block_range_filter() { - assert!(super::INDEX_HTML.contains("iuna-ui.js?v=66")); + assert!(super::INDEX_HTML.contains("iuna-ui.js?v=69")); assert!(super::INDEX_HTML.contains("aria-label=\"Metrics block range\"")); assert!(super::INDEX_HTML.contains("setMetricsRange(100)")); assert!(super::INDEX_HTML.contains("setMetricsRange(1000)")); assert!(super::INDEX_HTML.contains("setMetricsRange('all')")); } + #[test] + fn mine_screen_shows_recent_burn_fit_status() { + let app_js = include_str!("../../www/assets/iuna-ui.js"); + assert!(app_js.contains("Recent high:")); + assert!(app_js.contains("May wait for bigger burn blocks")); + assert!(super::INDEX_HTML.contains("Easier to include")); + assert!(super::INDEX_HTML.contains("Higher reward")); + } + async fn auth_test_state(config_path: std::path::PathBuf, config: UiConfig) -> HttpState { config_store::save(&config_path, &config).unwrap(); let wallet_path = config_path.with_file_name("wallet.json"); diff --git a/src/main.rs b/src/main.rs @@ -991,7 +991,11 @@ mod tests { assert_eq!( initial_burn_per_block(&setup, &UiConfig::default()), - DEFAULT_BURN_PER_BLOCK + UiConfig::default().burn_per_block + ); + assert_eq!( + initial_burn_fee(&setup, &UiConfig::default()), + UiConfig::default().burn_fee ); } diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -59,10 +59,10 @@ window.iunaApp = function iunaApp() { importSeedPhrase: "", walletVerified: false, setupFeedback: null, - burnAmount: 0, - burnAmountDraft: 0, - burnFee: 1, - burnFeeDraft: "0.000001", + burnAmount: 100, + burnAmountDraft: "0.0001", + burnFee: 100, + burnFeeDraft: "0.0001", miningEnabled: false, powMiningEnabled: false, powRequiredBurnMultiplier: 0.8, @@ -1143,8 +1143,54 @@ window.iunaApp = function iunaApp() { return Math.round(this.powRequiredBurnMultiplierValue() * 10000); }, + powMineRewardLimit() { + return Math.max(100000, Math.trunc(Number(this.status.chain?.mine_reward ?? 1000000))); + }, + + powMineRecentBurnTotals() { + return this.blocks + .filter((block) => block.height > 0) + .slice(0, 10) + .map((block) => this.blockBurnAmount(block)); + }, + + powMineRecentBurnMin() { + const totals = this.powMineRecentBurnTotals(); + if (totals.length === 0) return null; + return Math.min(...totals); + }, + + powMineRequiredBurn() { + const current = Math.max(0, Math.trunc(Number(this.status.mining?.automatic_pow_required_burn_amount ?? 0))); + if (!this.powRequiredBurnMultiplierDirty) return current; + const minimum = 100000; + const maximum = this.powMineRewardLimit(); + const recentMin = this.powMineRecentBurnMin(); + const base = recentMin === null ? maximum : recentMin; + return Math.min(maximum, Math.max(minimum, Math.round(base * this.powRequiredBurnMultiplierValue()))); + }, + powMineNetReward() { - return Math.max(0, Math.trunc(Number(this.status.mining?.automatic_pow_required_burn_amount ?? 0))); + return this.powMineRequiredBurn(); + }, + + powMineRecentBurnMax() { + const totals = this.powMineRecentBurnTotals(); + if (totals.length === 0) return null; + return Math.max(...totals); + }, + + powMineIncludeStatus() { + const required = this.powMineRequiredBurn(); + const recentMax = this.powMineRecentBurnMax(); + if (recentMax === null) { + return { kind: "muted", label: "Waiting for burn history", recent: "" }; + } + const recent = `Recent high: IUNA ${this.amountLabel(recentMax)}`; + if (recentMax >= required) { + return { kind: "ready", label: "Recent blocks can include this", recent }; + } + return { kind: "waiting", label: "May wait for bigger burn blocks", recent }; }, autoPowStatusLabel() {