commit fd542e99811a874d0662b81d1b1cb93a26e5a769
parent 04a13b097eb8f7cf39df34b5e52743b4768ab324
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Thu, 6 Aug 2026 09:02:09 +0200
Prepare v0.2.26
Diffstat:
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) {