iuna

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

commit fd542e99811a874d0662b81d1b1cb93a26e5a769
parent 04a13b097eb8f7cf39df34b5e52743b4768ab324
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Thu,  6 Aug 2026 09:02:09 +0200

Prepare v0.2.26

Diffstat:
MCargo.lock | 2+-
MCargo.toml | 2+-
Msrc/adapters/http.rs | 20++++++++++++++++++++
Msrc/app.rs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Mwww/assets/iuna-ui.js | 20++++++++++++++++++--
5 files changed, 101 insertions(+), 14 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -537,7 +537,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "iuna" -version = "0.2.25" +version = "0.2.26" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "iuna" -version = "0.2.25" +version = "0.2.26" edition = "2024" license = "Apache-2.0" diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -5488,6 +5488,26 @@ mod tests { fn polling_refreshes_paged_datasets_without_visible_loaders() { let app_js = include_str!("../../www/assets/iuna-ui.js"); assert!(app_js.contains("setInterval(() => this.refresh({ silent: true }), 5000)")); + assert!( + app_js.contains( + "return this.authLoaded && this.auth.configured === true && this.auth.authenticated === true;" + ) + ); + assert!(app_js.contains( + "async refresh(options = {}) {\n if (!this.canUseProtectedApi()) return;" + )); + assert!(app_js.contains("async refreshPagedDataset(kind, options = {}) {\n if (!this.canUseProtectedApi()) return;")); + assert!( + app_js.contains( + "async loadNextPage(kind) {\n if (!this.canUseProtectedApi()) return;" + ) + ); + assert!( + app_js.contains( + "async loadOlderBlocks() {\n if (!this.canUseProtectedApi()) return;" + ) + ); + assert!(app_js.contains("this.stopPolling();\n await this.refreshAuth();")); assert!(app_js.contains("backgroundLoading")); assert!(app_js.contains("options.silent === true ? \"backgroundLoading\" : \"loading\"")); } diff --git a/src/app.rs b/src/app.rs @@ -1516,8 +1516,8 @@ impl NodeCore { fn automatic_burn_needs_plaintext_anchor(&self, timestamp_ms: u64) -> bool { self.ledger - .expected_leader_for_next_block() - .is_none_or(|leader| leader == self.wallet.address()) + .finalizer_rank_for_next_block(self.wallet.address()) + .is_some() || self.ledger.recovery_block_available_at(timestamp_ms) || timestamp_ms.saturating_add(AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS) >= self.ledger.recovery_block_min_timestamp() @@ -2223,7 +2223,7 @@ mod tests { use crate::domain::{ FinalizerMode, GenesisBurn, Ledger, MICRO_IUNA, MINE_FINALIZER_FEE, - RECOVERY_BLOCK_DELAY_MS, Transaction, Wallet, run_vdf, + RECOVERY_BLOCK_DELAY_MS, Transaction, VDF_TARGET_BLOCK_MS, Wallet, run_vdf, }; use super::{GossipEnvelope, InMemoryNetwork, NodeConfig, NodeCore}; @@ -2971,10 +2971,12 @@ mod tests { fn automatic_non_leader_burn_is_queued_as_blinded() { let alice = Wallet::from_seed("auto-blinded-burn-alice"); let bob = Wallet::from_seed("auto-blinded-burn-bob"); + let carol = Wallet::from_seed("auto-blinded-burn-carol"); let finalizers = [alice.clone(), bob.clone()]; let mut allocations = BTreeMap::new(); allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); + allocations.insert(carol.address().to_string(), 10 * MICRO_IUNA); let ledger = Ledger::new_with_genesis_burns( allocations, finalizers @@ -2984,14 +2986,9 @@ mod tests { 1, ) .unwrap(); - let leader = ledger.expected_leader_for_next_block().unwrap(); - let non_leader = finalizers - .iter() - .find(|wallet| wallet.address() != leader) - .unwrap() - .clone(); + assert_eq!(ledger.finalizer_rank_for_next_block(carol.address()), None); let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( - non_leader, + carol, ledger, true, MICRO_IUNA / 10, @@ -3012,6 +3009,60 @@ mod tests { } #[test] + fn automatic_fallback_finalizer_burn_stays_plaintext_for_block_anchor() { + let alice = Wallet::from_seed("auto-fallback-burn-alice"); + let bob = Wallet::from_seed("auto-fallback-burn-bob"); + let finalizers = [alice.clone(), bob.clone()]; + let mut allocations = BTreeMap::new(); + allocations.insert(alice.address().to_string(), 10 * MICRO_IUNA); + allocations.insert(bob.address().to_string(), 10 * MICRO_IUNA); + let ledger = Ledger::new_with_genesis_burns( + allocations, + finalizers + .iter() + .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA)) + .collect(), + 1, + ) + .unwrap(); + let fallback = finalizers + .iter() + .find(|wallet| ledger.finalizer_rank_for_next_block(wallet.address()) == Some(1)) + .unwrap() + .clone(); + let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled( + fallback, + ledger, + true, + MICRO_IUNA / 10, + 1, + ); + + let plan = node.prepare_automatic_finalization(1); + let outbox = node.drain_outbox(); + + assert!(plan.burned.is_some()); + assert!( + plan.skipped_reason.is_none(), + "fallback should not be skipped: {:?}", + plan.skipped_reason + ); + assert!(node.ledger().pending().is_empty()); + assert!(node.ledger().pending_blinded_transactions().is_empty()); + assert!(node.local_block_anchor_burn.is_some()); + assert!(outbox.is_empty()); + let work = plan.work.expect("fallback work should be prepared"); + let vdf_output = run_vdf(work.vdf_seed(), work.vdf_rounds()); + let block = node + .complete_prepared_block_at(work, vdf_output, VDF_TARGET_BLOCK_MS) + .unwrap(); + + assert_eq!(block.finalizer_rank, 1); + assert_eq!(block.finalizer_mode, FinalizerMode::Ticket); + assert_eq!(node.ledger().height(), 1); + } + + #[test] fn automatic_leader_burn_stays_plaintext_for_block_anchor() { let alice = Wallet::from_seed("auto-plaintext-burn-alice"); let bob = Wallet::from_seed("auto-plaintext-burn-bob"); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -124,6 +124,16 @@ window.iunaApp = function iunaApp() { } }, + canUseProtectedApi() { + return this.authLoaded && this.auth.configured === true && this.auth.authenticated === true; + }, + + stopPolling() { + if (!this.pollHandle) return; + clearInterval(this.pollHandle); + this.pollHandle = null; + }, + tabFromHash() { const hash = window.location.hash.replace(/^#\/?/, ""); return this.allowedTabs().includes(hash) ? hash : "wallet"; @@ -303,6 +313,7 @@ window.iunaApp = function iunaApp() { async logout() { try { await this.postAuth("/api/auth/logout", ""); + this.stopPolling(); await this.refreshAuth(); this.showFlash("Locked", "success"); } catch (error) { @@ -540,6 +551,7 @@ window.iunaApp = function iunaApp() { }, async refresh(options = {}) { + if (!this.canUseProtectedApi()) return; try { const [config, status, blocks, p2pMetrics, blockchainMetrics, networkHealth] = await Promise.all([ this.fetchJson("/api/config"), @@ -581,6 +593,7 @@ window.iunaApp = function iunaApp() { this.scheduleFeeEstimates(); } catch (error) { if (String(error.message || "").includes("401")) { + this.stopPolling(); await this.refreshAuth(); return; } @@ -640,6 +653,7 @@ window.iunaApp = function iunaApp() { }, async refreshPagedDataset(kind, options = {}) { + if (!this.canUseProtectedApi()) return; const config = this.datasetConfig(kind); if (!config) return; const page = this[config.page]; @@ -655,6 +669,7 @@ window.iunaApp = function iunaApp() { }, async loadNextPage(kind) { + if (!this.canUseProtectedApi()) return; const config = this.datasetConfig(kind); if (!config) return; const page = this[config.page]; @@ -734,7 +749,7 @@ window.iunaApp = function iunaApp() { observePageSentinel(kind, element) { if (!element || element.__iunaPageObserver) return; const observer = new IntersectionObserver((entries) => { - if (entries.some((entry) => entry.isIntersecting)) { + if (this.canUseProtectedApi() && entries.some((entry) => entry.isIntersecting)) { this.loadNextPage(kind); } }, { root: null, rootMargin: "180px 0px" }); @@ -745,7 +760,7 @@ window.iunaApp = function iunaApp() { observeBlockSentinel(element) { if (!element || element.__iunaBlockObserver) return; const observer = new IntersectionObserver((entries) => { - if (entries.some((entry) => entry.isIntersecting)) { + if (this.canUseProtectedApi() && entries.some((entry) => entry.isIntersecting)) { this.loadOlderBlocks(); } }, { root: null, rootMargin: "180px 0px" }); @@ -892,6 +907,7 @@ window.iunaApp = function iunaApp() { }, async loadOlderBlocks() { + if (!this.canUseProtectedApi()) return; if (this.loadingOlder || !this.hasMoreBlocks || this.blocks.length === 0) return; const oldest = Math.min(...this.blocks.map((block) => block.height)); if (oldest <= 0) {