iuna

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

commit 4dfcb23cbca69e8935fa9c78b527e155a51d38e2
parent 923ec4427b1672e33fc3eefa340fa7e69288d8fb
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Fri,  7 Aug 2026 23:13:39 +0200

Improve mempool ordering and block byte breakdown

Diffstat:
Msrc/adapters/http.rs | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mwww/assets/iuna-ui.js | 43+++++++++++++++++++++++++++++++++++--------
2 files changed, 114 insertions(+), 11 deletions(-)

diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -373,6 +373,7 @@ struct UiBlock { total_fees: Amount, total_bytes: usize, transaction_bytes: usize, + transaction_byte_breakdown: Vec<UiByteBreakdown>, blinded_transaction_bytes: usize, reveal_bundle_bytes: usize, vdf_rounds: u64, @@ -386,6 +387,12 @@ struct UiBlock { } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] +struct UiByteBreakdown { + label: &'static str, + bytes: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "camelCase")] struct UiRevealBundle { slot: u8, @@ -1874,6 +1881,7 @@ fn ui_block( .iter() .map(|tx| tx.serialized_size_bytes().unwrap_or_default()) .sum::<usize>(); + let transaction_byte_breakdown = transaction_byte_breakdown(&block.transactions); let blinded_transaction_bytes = block .blinded_transactions .iter() @@ -1940,6 +1948,7 @@ fn ui_block( total_fees: block.reward.saturating_add(revealed_fees), total_bytes, transaction_bytes, + transaction_byte_breakdown, blinded_transaction_bytes, reveal_bundle_bytes, vdf_rounds: block.vdf_rounds, @@ -1965,6 +1974,28 @@ fn ui_revealed_transaction( row } +fn transaction_byte_breakdown(transactions: &[Transaction]) -> Vec<UiByteBreakdown> { + let mut transfer_bytes = 0_usize; + let mut burn_bytes = 0_usize; + let mut mine_bytes = 0_usize; + for transaction in transactions { + let bytes = transaction.serialized_size_bytes().unwrap_or_default(); + match transaction { + Transaction::Transfer { .. } => transfer_bytes = transfer_bytes.saturating_add(bytes), + Transaction::Burn { .. } => burn_bytes = burn_bytes.saturating_add(bytes), + Transaction::Mine { .. } => mine_bytes = mine_bytes.saturating_add(bytes), + } + } + [ + ("transfer", transfer_bytes), + ("burn", burn_bytes), + ("mine", mine_bytes), + ] + .into_iter() + .filter_map(|(label, bytes)| (bytes > 0).then_some(UiByteBreakdown { label, bytes })) + .collect() +} + fn ui_pending_revealed_transaction( revealed: &RevealedBlindedTransaction, outputs_by_outpoint: &BTreeMap<OutPoint, TxOutput>, @@ -3402,7 +3433,7 @@ const INDEX_HTML: &str = r#"<!doctype html> .block-card { flex-basis: 108px; } } </style> - <script defer src="/assets/iuna-ui.js?v=85"></script> + <script defer src="/assets/iuna-ui.js?v=86"></script> <script defer src="/assets/alpine.min.js"></script> </head> <body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak> @@ -4122,7 +4153,11 @@ const INDEX_HTML: &str = r#"<!doctype html> <div class="rank-row"> <div class="rank-number" x-text="`${row[1]}B`"></div> <div class="rank-details"> - <div class="tx-field"><span class="tx-label">Category</span><span class="tx-value text" x-text="row[0]"></span></div> + <div class="tx-field"> + <span class="tx-label">Category</span> + <span class="pill" x-show="row[2]" :class="row[2]" x-text="row[0]"></span> + <span class="tx-value text" x-show="!row[2]" x-text="row[0]"></span> + </div> </div> </div> </template> @@ -4472,6 +4507,38 @@ mod tests { assert!(ui_block.total_bytes > 0); assert!(ui_block.blinded_transaction_bytes > 0); assert_eq!(ui_block.reveal_bundle_bytes, 0); + + let burn = ledger.build_burn(&alice, 5, 1).unwrap(); + let mine = Transaction::Mine { + recipient: bob.address().to_string(), + anchor: "a".repeat(64), + salt: 1, + nonce: 2, + difficulty_bits: 12, + proof_header: None, + signature: "b".repeat(64), + }; + let typed_block = super::ui_block( + fake_block( + 8, + vec![ + ledger.build_transfer(&alice, bob.address(), 7, 1).unwrap(), + burn, + mine, + ], + ), + &BTreeMap::new(), + &BTreeMap::new(), + &[], + ); + let typed_bytes = typed_block + .transaction_byte_breakdown + .iter() + .map(|row| (row.label, row.bytes)) + .collect::<BTreeMap<_, _>>(); + assert!(typed_bytes["transfer"] > 0); + assert!(typed_bytes["burn"] > 0); + assert!(typed_bytes["mine"] > 0); } #[test] @@ -4536,6 +4603,7 @@ mod tests { assert!(super::INDEX_HTML.contains(".pill.reveal, .pill.revealed")); assert!(super::INDEX_HTML.contains(":class=\"mempoolItemClass(tx)\"")); assert!(super::INDEX_HTML.contains(".mempool-item.before-last-block")); + assert!(super::INDEX_HTML.contains(":class=\"row[2]\"")); assert!(super::INDEX_HTML.contains("New since last block")); assert!(super::INDEX_HTML.contains("class=\"mempool-top\"")); assert!(super::INDEX_HTML.contains("mempoolSeenTimeLabel(tx)")); @@ -4546,6 +4614,8 @@ mod tests { assert!(app_js.contains("tx?.revealed ? \"revealed\"")); assert!(app_js.contains("transactions.some((tx) => tx?.revealed)")); assert!(app_js.contains("blockCommitCount(block)")); + assert!(app_js.contains("blockTransactionByteBreakdown(block)")); + assert!(app_js.contains("[label, Number(row.bytes ?? 0), label]")); } #[test] @@ -5642,7 +5712,7 @@ mod tests { #[test] fn metrics_screen_includes_block_range_filter() { - assert!(super::INDEX_HTML.contains("iuna-ui.js?v=85")); + assert!(super::INDEX_HTML.contains("iuna-ui.js?v=86")); assert!(super::INDEX_HTML.contains("aria-label=\"Metrics block range\"")); assert!(super::INDEX_HTML.contains("setMetricsRange(100)")); assert!(super::INDEX_HTML.contains("setMetricsRange(1000)")); @@ -5663,6 +5733,12 @@ mod tests { ); assert!(app_js.contains("mempoolFirstSeenHeights")); assert!(app_js.contains("mempoolFirstSeenAt")); + assert!( + app_js.contains( + "this.trackMempoolFirstSeenHeights({ append: options.replace !== true })" + ) + ); + assert!(app_js.contains("return rightSeenAt - leftSeenAt")); assert!(app_js.contains("syncMempoolBlockMarker")); assert!(app_js.contains("this.status.chain?.height ?? this.lastBlockMempoolHeight")); assert!(app_js.contains("mempoolItemClass")); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -715,7 +715,7 @@ window.iunaApp = function iunaApp() { ? normalized.items : this.mergeDatasetItems(this[config.items], normalized.items, config.key); if (kind === "mempool") { - this.trackMempoolFirstSeenHeights(); + this.trackMempoolFirstSeenHeights({ append: options.replace !== true }); this.sortMempoolNewestFirst(); } page.offset = normalized.nextOffset ?? this[config.items].length; @@ -783,20 +783,28 @@ window.iunaApp = function iunaApp() { } }, - trackMempoolFirstSeenHeights() { + trackMempoolFirstSeenHeights(options = {}) { const height = Number(this.status.chain?.height); if (!Number.isFinite(height)) return; const active = new Set(); const firstBatch = !this.mempoolSeenInitialized; const seenHeight = firstBatch ? height - 1 : height; - const seenAt = Date.now(); + const knownSeenTimes = Object.values(this.mempoolFirstSeenAt) + .map((value) => Number(value)) + .filter((value) => Number.isFinite(value)); + const oldestSeenAt = knownSeenTimes.length ? Math.min(...knownSeenTimes) : Date.now(); + const baseSeenAt = options.append && this.mempoolSeenInitialized + ? oldestSeenAt - 1 + : Date.now(); + let newIndex = 0; for (const tx of this.mempool) { const key = this.mempoolKey(tx); if (!key) continue; active.add(key); if (this.mempoolFirstSeenHeights[key] === undefined) { this.mempoolFirstSeenHeights[key] = seenHeight; - this.mempoolFirstSeenAt[key] = seenAt; + this.mempoolFirstSeenAt[key] = baseSeenAt - newIndex; + newIndex += 1; } } this.mempoolSeenInitialized = true; @@ -832,6 +840,11 @@ window.iunaApp = function iunaApp() { sortMempoolNewestFirst() { this.mempool = [...this.mempool].sort((left, right) => { + const leftSeenAt = Number(this.mempoolFirstSeenAt[this.mempoolKey(left)]); + const rightSeenAt = Number(this.mempoolFirstSeenAt[this.mempoolKey(right)]); + if (Number.isFinite(leftSeenAt) && Number.isFinite(rightSeenAt) && leftSeenAt !== rightSeenAt) { + return rightSeenAt - leftSeenAt; + } const leftSeen = Number(this.mempoolFirstSeenHeights[this.mempoolKey(left)]); const rightSeen = Number(this.mempoolFirstSeenHeights[this.mempoolKey(right)]); if (Number.isFinite(leftSeen) && Number.isFinite(rightSeen) && leftSeen !== rightSeen) { @@ -1879,14 +1892,28 @@ window.iunaApp = function iunaApp() { }, blockByteBreakdown(block) { + const transactionRows = this.blockTransactionByteBreakdown(block); return [ - ["Header and proof", Math.max(0, this.blockTotalBytes(block) - this.blockPayloadBytes(block))], - ["Transactions", Number(block?.transactionBytes ?? block?.transaction_bytes ?? 0)], - ["Blinded commits", Number(block?.blindedTransactionBytes ?? block?.blinded_transaction_bytes ?? 0)], - ["Reveal bundles", Number(block?.revealBundleBytes ?? block?.reveal_bundle_bytes ?? 0)], + ["Header and proof", Math.max(0, this.blockTotalBytes(block) - this.blockPayloadBytes(block)), ""], + ...(transactionRows.length + ? transactionRows + : [["Transactions", Number(block?.transactionBytes ?? block?.transaction_bytes ?? 0), ""]]), + ["Blinded commits", Number(block?.blindedTransactionBytes ?? block?.blinded_transaction_bytes ?? 0), "blinded"], + ["Reveal bundles", Number(block?.revealBundleBytes ?? block?.reveal_bundle_bytes ?? 0), "reveal"], ]; }, + blockTransactionByteBreakdown(block) { + const rows = block?.transactionByteBreakdown ?? block?.transaction_byte_breakdown; + if (!Array.isArray(rows)) return []; + return rows + .map((row) => { + const label = row.label || row.kind || "transaction"; + return [label, Number(row.bytes ?? 0), label]; + }) + .filter((row) => row[1] > 0); + }, + recentBlockFeeAverage(count) { const sample = this.blocks.filter((block) => block.height > 0).slice(0, count); if (sample.length === 0) return 0;