iuna

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

commit e59bdc98ccecd9a6b46a3bba46b1e1babf3cb000
parent 9c557bb3e1eabcab5b24dcc74c11a08113669c65
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Fri,  7 Aug 2026 13:39:25 +0200

Highlight new mempool items

Diffstat:
Msrc/adapters/http.rs | 16+++++++++++++---
Mwww/assets/iuna-ui.js | 55++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 67 insertions(+), 4 deletions(-)

diff --git a/src/adapters/http.rs b/src/adapters/http.rs @@ -3264,6 +3264,9 @@ const INDEX_HTML: &str = r#"<!doctype html> details.tx-section:not([open]) { gap: 0; } .tx-section-meta { color: #8e979e; font-size: 12px; font-weight: 600; } .tx-card, .mempool-item { position: relative; display: grid; gap: 6px; border: 1px solid #2f363c; border-radius: 8px; padding: 12px; background: #111316; cursor: pointer; text-align: left; } + .mempool-item.before-last-block { opacity: .56; } + .mempool-item.new-since-block { border-color: #49543b; background: #151a12; opacity: 1; } + .mempool-item.new-since-block::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 3px; border-radius: 8px 0 0 8px; background: #d5f55f; } .tx-card .pill, .mempool-item .pill { position: absolute; top: 10px; right: 10px; } .pill { display: inline-flex; align-items: center; border-radius: 999px; padding: 2px 8px; font-size: 12px; font-weight: 800; background: #2b3136; color: #d6dee2; } .pill.burn { background: #332918; color: #ffd070; } @@ -3319,7 +3322,7 @@ const INDEX_HTML: &str = r#"<!doctype html> .block-card { flex-basis: 108px; } } </style> - <script defer src="/assets/iuna-ui.js?v=84"></script> + <script defer src="/assets/iuna-ui.js?v=85"></script> <script defer src="/assets/alpine.min.js"></script> </head> <body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak> @@ -3784,8 +3787,9 @@ const INDEX_HTML: &str = r#"<!doctype html> <h2>Mempool</h2> <div class="mempool-strip"> <template x-for="tx in mempool" :key="tx.signature"> - <div class="mempool-item" role="button" tabindex="0" @click="openTransactionModal(tx, { source: 'Mempool' })" @keydown.enter.prevent="openTransactionModal(tx, { source: 'Mempool' })" @keydown.space.prevent="openTransactionModal(tx, { source: 'Mempool' })"> + <div class="mempool-item" :class="mempoolItemClass(tx)" role="button" tabindex="0" @click="openTransactionModal(tx, { source: 'Mempool' })" @keydown.enter.prevent="openTransactionModal(tx, { source: 'Mempool' })" @keydown.space.prevent="openTransactionModal(tx, { source: 'Mempool' })"> <span class="pill" :class="tx.kind" x-text="tx.kind"></span> + <div class="tx-field" x-show="mempoolItemClass(tx) === 'new-since-block'"><span class="tx-label">Seen</span><span class="tx-value text">after last block</span></div> <div class="tx-field" x-show="!isBlindedMempoolItem(tx)"><span class="tx-label">Amount</span><span class="tx-value money">IUNA <span x-text="amountLabel(txAmount(tx))"></span></span></div> <div class="tx-field"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(tx)"></span></div> <div class="tx-field" x-show="!isBlindedMempoolItem(tx)"><span class="tx-label">From</span><code class="tx-value hash" x-text="short(txFrom(tx))"></code></div> @@ -4382,6 +4386,9 @@ mod tests { assert!(super::INDEX_HTML.contains("commitCountLabel(block)")); assert!(super::INDEX_HTML.contains(".pill.blinded")); 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("after last block")); assert!(super::INDEX_HTML.contains("<details class=\"tx-section\">")); assert!(super::INDEX_HTML.contains("<summary class=\"tx-section-title\">")); assert!(super::INDEX_HTML.contains("Commitment")); @@ -5485,7 +5492,7 @@ mod tests { #[test] fn metrics_screen_includes_block_range_filter() { - assert!(super::INDEX_HTML.contains("iuna-ui.js?v=84")); + assert!(super::INDEX_HTML.contains("iuna-ui.js?v=85")); assert!(super::INDEX_HTML.contains("aria-label=\"Metrics block range\"")); assert!(super::INDEX_HTML.contains("setMetricsRange(100)")); assert!(super::INDEX_HTML.contains("setMetricsRange(1000)")); @@ -5500,6 +5507,9 @@ mod tests { assert!(app_js.contains("txFeeLabel(tx)")); assert!(app_js.contains("tx?.kind === \"reveal\"")); assert!(app_js.contains("unknown until reveal")); + assert!(app_js.contains("mempoolFirstSeenHeights")); + assert!(app_js.contains("syncMempoolBlockMarker")); + assert!(app_js.contains("mempoolItemClass")); assert!(super::INDEX_HTML.contains("x-text=\"txFeeLabel(tx)\"")); assert!(super::INDEX_HTML.contains("x-text=\"txFeeLabel(selectedTransaction?.tx)\"")); } diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -88,6 +88,9 @@ window.iunaApp = function iunaApp() { hashListenerInstalled: false, newBlockHashes: new Set(), newBlockTimer: null, + lastBlockMempoolHeight: null, + mempoolFirstSeenHeights: {}, + mempoolSeenInitialized: false, blockPageSize: 20, datasetPageSize: 25, walletTxPage: { offset: 0, total: 0, hasMore: true, loading: false, backgroundLoading: false }, @@ -561,6 +564,8 @@ window.iunaApp = function iunaApp() { this.fetchJson("/api/metrics"), this.fetchJson("/api/network/health"), ]); + const previousChainHeight = this.status.chain?.height; + this.status = status; await Promise.all([ this.refreshPagedDataset("walletTx", { silent: options.silent === true }), this.refreshPagedDataset("walletUtxo", { silent: options.silent === true }), @@ -575,7 +580,7 @@ window.iunaApp = function iunaApp() { if (!this.config.setup_complete) { await this.refreshWalletSetup(); } - this.status = status; + this.syncMempoolBlockMarker(previousChainHeight, status.chain?.height); this.mergeFreshBlocks(blocks, { animateHead: true }); this.pruneSelectedTransferUtxos(); this.p2pMetrics = p2pMetrics; @@ -694,6 +699,9 @@ window.iunaApp = function iunaApp() { this[config.items] = options.replace ? normalized.items : this.mergeDatasetItems(this[config.items], normalized.items, config.key); + if (kind === "mempool") { + this.trackMempoolFirstSeenHeights(); + } page.offset = normalized.nextOffset ?? this[config.items].length; page.total = normalized.total; page.hasMore = normalized.hasMore; @@ -746,6 +754,51 @@ window.iunaApp = function iunaApp() { return rows; }, + syncMempoolBlockMarker(previousHeight, currentHeight) { + const normalizedCurrent = Number(currentHeight); + if (!Number.isFinite(normalizedCurrent)) return; + const normalizedPrevious = Number(previousHeight); + if (this.lastBlockMempoolHeight === null) { + this.lastBlockMempoolHeight = normalizedCurrent; + return; + } + if (!Number.isFinite(normalizedPrevious) || normalizedCurrent > normalizedPrevious) { + this.lastBlockMempoolHeight = normalizedCurrent; + } + }, + + trackMempoolFirstSeenHeights() { + 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; + 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.mempoolSeenInitialized = true; + for (const key of Object.keys(this.mempoolFirstSeenHeights)) { + if (!active.has(key)) delete this.mempoolFirstSeenHeights[key]; + } + }, + + mempoolKey(tx) { + return tx?.signature || tx?.commitment || ""; + }, + + mempoolItemClass(tx) { + const key = this.mempoolKey(tx); + const firstSeenHeight = Number(this.mempoolFirstSeenHeights[key]); + const markerHeight = Number(this.lastBlockMempoolHeight); + if (!key || !Number.isFinite(firstSeenHeight) || !Number.isFinite(markerHeight)) return ""; + return firstSeenHeight >= markerHeight ? "new-since-block" : "before-last-block"; + }, + observePageSentinel(kind, element) { if (!element || element.__iunaPageObserver) return; const observer = new IntersectionObserver((entries) => {