commit 071fefae8b02690c40f3a438ac67cc46080e0698
parent 9aebf6a491c7d003595fba89c3df7f6ef22c4954
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Sat, 22 Aug 2026 07:38:11 +0200
Improve local testnet burn defaults and warnings
Diffstat:
6 files changed, 81 insertions(+), 17 deletions(-)
diff --git a/README.md b/README.md
@@ -132,22 +132,30 @@ Node3 also exposes Stratum on `127.0.0.1:3333`. P2P ports are mapped to
`19444`, `19445`, and `19446` for local inspection, while nodes announce their
stable Docker-network addresses to each other.
-The local compose file sets `IUNA_WALLET_PASSWORD` for every container. On first
-start this configures the management UI password and encrypts the wallet; on
-restart it unlocks the encrypted wallet so finalization and automatic mining can
-continue without UI login. Compose also sets `IUNA_SETUP_COMPLETE=true`, so
-after unlocking the management UI you land directly in the node instead of the
-initial setup wizard. Override the local password defaults from your shell or a
-`.env` file:
+The local compose file uses `test` as the management UI and wallet password for
+all three nodes. On first start this configures the management UI password and
+encrypts the wallet; on restart it unlocks the encrypted wallet so finalization
+and automatic mining can continue without UI login. Compose also sets
+`IUNA_SETUP_COMPLETE=true`, so after unlocking the management UI you land
+directly in the node instead of the initial setup wizard. Override the shared
+local-testnet password from your shell or a `.env` file:
```sh
-IUNA_BOOTSTRAP_WALLET_PASSWORD='change-this-bootstrap-password' \
-IUNA_NODE2_WALLET_PASSWORD='change-this-node2-password' \
-IUNA_NODE3_WALLET_PASSWORD='change-this-node3-password' \
+IUNA_TESTNET_PASSWORD='change-this-testnet-password' \
docker compose up --build
```
Do not use compose-file default passwords for public nodes or valuable wallets.
+If the volumes were created with the older per-node passwords, change those
+passwords first or recreate the disposable local-testnet volumes before starting
+the updated compose stack.
+
+Bootstrap uses `--genesis` only while `/data/chain.sqlite3` is absent or empty.
+Once the chain database exists, container restarts launch bootstrap normally and
+preserve the existing chain.
+
+Fresh nodes prefill automatic burns at `0.0001 IUNA` per block with a
+`0.000001 IUNA` per-byte fee.
For unattended local nodes, startup environment flags can also persist mining
settings:
diff --git a/docker-compose.yml b/docker-compose.yml
@@ -19,7 +19,7 @@ services:
<<: *iuna-node
hostname: iuna-bootstrap
environment:
- IUNA_WALLET_PASSWORD: ${IUNA_BOOTSTRAP_WALLET_PASSWORD:-local-testnet-bootstrap-password}
+ IUNA_WALLET_PASSWORD: ${IUNA_TESTNET_PASSWORD:-test}
IUNA_SETUP_COMPLETE: "true"
IUNA_LOCAL_TESTNET: "true"
entrypoint: ["/bin/sh", "-c"]
@@ -38,7 +38,7 @@ services:
<<: *iuna-node
hostname: iuna-node2
environment:
- IUNA_WALLET_PASSWORD: ${IUNA_NODE2_WALLET_PASSWORD:-local-testnet-node2-password}
+ IUNA_WALLET_PASSWORD: ${IUNA_TESTNET_PASSWORD:-test}
IUNA_SETUP_COMPLETE: "true"
IUNA_POW_MINING_ENABLED: "true"
entrypoint: ["/bin/sh", "-c"]
@@ -60,7 +60,7 @@ services:
<<: *iuna-node
hostname: iuna-node3
environment:
- IUNA_WALLET_PASSWORD: ${IUNA_NODE3_WALLET_PASSWORD:-local-testnet-node3-password}
+ IUNA_WALLET_PASSWORD: ${IUNA_TESTNET_PASSWORD:-test}
IUNA_SETUP_COMPLETE: "true"
IUNA_AUTOMATIC_BURN_ENABLED: "true"
entrypoint: ["/bin/sh", "-c"]
diff --git a/src/adapters/config_store.rs b/src/adapters/config_store.rs
@@ -17,7 +17,7 @@ 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_AMOUNT: Amount = MICRO_IUNA / 10_000;
-pub const DEFAULT_BURN_FEE: Amount = MICRO_IUNA / 10_000;
+pub const DEFAULT_BURN_FEE: Amount = 1;
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;
@@ -335,7 +335,7 @@ mod tests {
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("\"burn_fee\": 1"));
assert!(!stored.contains("\"pow_mine_fee\""));
assert!(!stored.contains("required_burn"));
assert!(stored.contains("\"keep_track_of_metrics\": false"));
@@ -477,6 +477,8 @@ mod tests {
assert!(!config.mining_enabled);
assert_eq!(config.burn_per_block, DEFAULT_BURN_AMOUNT);
assert_eq!(config.burn_fee, DEFAULT_BURN_FEE);
+ assert_eq!(config.burn_per_block, 100);
+ assert_eq!(config.burn_fee, 1);
}
#[test]
diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs
@@ -214,6 +214,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
.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; }
+ .fee-warning { flex-basis: 100%; color: #ffd070; font-size: 12px; font-weight: 800; }
.mine-stats { display: grid; grid-template-columns: repeat(4, minmax(112px, 1fr)); gap: 8px; min-width: 0; }
.local-mining-stats { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.fee-history { grid-template-columns: repeat(3, minmax(112px, 1fr)); margin-top: 12px; }
@@ -536,6 +537,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
</div>
<label>Fee / byte<input x-model="transferFee" @input="scheduleFeeEstimates" type="number" min="0" step="0.000001" required></label>
<div class="fee-preview" x-text="feeEstimateLabel('transfer')"></div>
+ <div class="fee-warning" x-show="feeExceedsAmount('transfer')" x-text="feeExceedsAmountLabel('transfer')" role="status" aria-live="polite"></div>
<button class="advanced-toggle" type="button" @click="toggleSendAdvanced" x-text="showSendAdvanced ? 'Hide UTXOs' : 'UTXOs'"></button>
<div class="send-utxo-summary" x-show="showSendAdvanced">
<div>Selected UTXOs: <span x-text="selectedTransferUtxos.length"></span></div>
@@ -713,6 +715,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
<button class="primary" type="submit">Save</button>
</div>
<div class="fee-preview" x-text="feeEstimateLabel('burn')"></div>
+ <div class="fee-warning" x-show="feeExceedsAmount('burn')" x-text="feeExceedsAmountLabel('burn')" role="status" aria-live="polite"></div>
</form>
<div class="mine-stats fee-history" aria-label="Recent block fees">
<div class="mine-stat">
diff --git a/src/main_tests.rs b/src/main_tests.rs
@@ -42,6 +42,37 @@ fn help_mentions_dev_seed_verify_bypass_env() {
assert!(help_text().contains("--debug"));
}
+#[test]
+fn local_testnet_compose_uses_one_obvious_test_password() {
+ let compose = include_str!("../docker-compose.yml");
+ assert_eq!(
+ compose
+ .matches("IUNA_WALLET_PASSWORD: ${IUNA_TESTNET_PASSWORD:-test}")
+ .count(),
+ 3
+ );
+ assert!(!compose.contains("IUNA_BOOTSTRAP_WALLET_PASSWORD"));
+ assert!(!compose.contains("IUNA_NODE2_WALLET_PASSWORD"));
+ assert!(!compose.contains("IUNA_NODE3_WALLET_PASSWORD"));
+}
+
+#[test]
+fn local_testnet_bootstrap_only_uses_genesis_for_an_empty_chain() {
+ let compose = include_str!("../docker-compose.yml");
+ assert!(compose.contains("if [ -s /data/chain.sqlite3 ]; then exec iuna --data-dir /data"));
+ assert!(compose.contains("else exec iuna --genesis --data-dir /data"));
+}
+
+#[test]
+fn management_ui_warns_when_estimated_fees_exceed_amounts() {
+ let html = include_str!("adapters/http/index_html.rs");
+ let javascript = include_str!("../www/assets/iuna-ui.js");
+ assert!(html.contains("feeExceedsAmount('transfer')"));
+ assert!(html.contains("feeExceedsAmount('burn')"));
+ assert!(javascript.contains("feeExceedsAmount(kind)"));
+ assert!(javascript.contains("estimated fee IUNA ${this.amountLabel(fee)} exceeds"));
+}
+
fn ledger_with_one_spendable_iuna(wallet: &Wallet) -> Ledger {
let mut genesis = BTreeMap::new();
genesis.insert(wallet.address().to_string(), 3);
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -82,8 +82,8 @@ window.iunaApp = function iunaApp() {
setupFeedback: null,
burnAmount: 100,
burnAmountDraft: "0.0001",
- burnFee: 100,
- burnFeeDraft: "0.0001",
+ burnFee: 1,
+ burnFeeDraft: "0.000001",
miningEnabled: false,
powMiningEnabled: false,
powMiningWorkers: 1,
@@ -1418,6 +1418,26 @@ window.iunaApp = function iunaApp() {
return `${estimate.bytes} bytes -> IUNA ${this.amountLabel(estimate.fee)}`;
},
+ feeExceedsAmount(kind) {
+ const estimate = this.feeEstimates[kind];
+ if (!estimate || estimate.error) return false;
+ const amount = kind === "burn"
+ ? this.parseiunaAmount(this.burnAmountDraft)
+ : this.parseiunaAmount(this.transferAmount);
+ const fee = Number(estimate.fee);
+ return amount > 0 && Number.isFinite(fee) && fee > amount;
+ },
+
+ feeExceedsAmountLabel(kind) {
+ if (!this.feeExceedsAmount(kind)) return "";
+ const label = kind === "burn" ? "burn" : "transfer";
+ const amount = kind === "burn"
+ ? this.parseiunaAmount(this.burnAmountDraft)
+ : this.parseiunaAmount(this.transferAmount);
+ const fee = Number(this.feeEstimates[kind].fee);
+ return `Warning: estimated fee IUNA ${this.amountLabel(fee)} exceeds the ${label} amount IUNA ${this.amountLabel(amount)}.`;
+ },
+
async saveBurn() {
try {
const amount = this.parseiunaAmount(this.burnAmountDraft);