commit cef24ab982068d129428a00c46d102b597852421
parent cbf1eba2475b85b3d7d5ec4f8750edfc3d125a93
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Sat, 1 Aug 2026 21:18:18 +0200
Clarify PoW mining controls
Diffstat:
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() {