commit 629575e02c5cc98ee64b4e7dbd15b52fa61fb64f
parent 925c2887f47d16746138aed90a07d599ea269d2e
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Mon, 24 Aug 2026 19:34:33 +0200
Add burn bundle selection details
Diffstat:
2 files changed, 99 insertions(+), 2 deletions(-)
diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs
@@ -391,11 +391,25 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
.detail-kv { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 8px; font-size: 13px; margin: 7px 0; }
.detail-kv .key { color: #8d989f; }
.detail-link { width: fit-content; max-width: 100%; padding: 0; border: 0; background: transparent; color: #d7f2ff; font: inherit; text-align: left; cursor: pointer; }
- .detail-link code { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
+ .detail-link code, .detail-link .link-value { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
+ .detail-link:hover { color: #d5f55f; }
.rank-list { display: grid; gap: 8px; }
.rank-row { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 10px; align-items: start; border: 1px solid #30383d; border-radius: 8px; padding: 10px; background: #15191d; }
.rank-number { color: #d7f2ff; font-weight: 700; }
.rank-details { display: grid; gap: 6px; min-width: 0; }
+ .bundle-summary { display: grid; gap: 5px; border: 1px solid #425027; border-radius: 8px; padding: 12px; margin-bottom: 14px; background: #1b2116; }
+ .bundle-summary strong { color: #d5f55f; }
+ .bundle-flow { display: grid; grid-template-columns: repeat(auto-fit, minmax(175px, 1fr)); gap: 8px; margin-bottom: 14px; }
+ .bundle-slot { display: grid; align-content: start; gap: 8px; min-width: 0; border: 1px solid #30383d; border-radius: 8px; padding: 11px; background: #111316; }
+ .bundle-slot-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
+ .bundle-slot-number { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 50%; background: #d5f55f; color: #111316; font-size: 12px; font-weight: 900; }
+ .bundle-slot-status { color: #9fbd42; font-size: 11px; font-weight: 850; text-transform: uppercase; }
+ .bundle-slot .tx-field { grid-template-columns: minmax(0, 1fr); gap: 3px; align-items: start; }
+ .bundle-slot code { overflow-wrap: anywhere; color: #9eb3bc; font-size: 11px; }
+ .bundle-how { display: grid; gap: 8px; border-top: 1px solid #30383d; padding-top: 14px; }
+ .bundle-how h3 { margin: 0 0 2px; }
+ .bundle-how-step { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 8px; align-items: start; color: #cbd3d7; }
+ .bundle-how-step > span:first-child { display: inline-grid; place-items: center; width: 22px; height: 22px; border: 1px solid #4a555b; border-radius: 50%; color: #d7f2ff; font-size: 11px; font-weight: 800; }
.tx-list { display: grid; gap: 8px; }
.tx-section { display: grid; gap: 8px; }
.tx-section + .tx-section { margin-top: 10px; padding-top: 10px; border-top: 1px solid #2f363c; }
@@ -976,7 +990,12 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
</div>
<div class="detail-kv"><div class="key">Mode</div><div x-text="selectedBlock.finalizer_mode === 'recovery' ? 'Recovery' : `Rank ${selectedBlock.finalizer_rank ?? 0}`"></div></div>
<div class="detail-kv"><div class="key">Reward</div><div>IUNA <span x-text="amountLabel(selectedBlock.reward)"></span></div></div>
- <div class="detail-kv"><div class="key">Burn Bundles</div><div x-text="blockBurnBundleRatio(selectedBlock)"></div></div>
+ <div class="detail-kv">
+ <div class="key">Burn Bundles</div>
+ <button class="detail-link" type="button" @click="openBurnBundleModal(selectedBlock)" title="How the burn bundle attestations were selected" aria-label="Show burn bundle selection details">
+ <span class="link-value" x-text="blockBurnBundleRatio(selectedBlock)"></span>
+ </button>
+ </div>
<div class="detail-kv"><div class="key">Burns</div><div x-text="blockBurnCount(selectedBlock)"></div></div>
<div class="detail-kv"><div class="key">Transfers</div><div x-text="blockTransferCount(selectedBlock)"></div></div>
<div class="detail-kv"><div class="key">Total Lost</div><div>IUNA <span x-text="amountLabel(blockLostIuna(selectedBlock))"></span></div></div>
@@ -1386,6 +1405,43 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
</div>
</section>
</div>
+ <div class="setup-overlay transaction-overlay" x-show="selectedBurnBundleBlock" x-transition.opacity @click.self="closeBurnBundleModal()" role="dialog" aria-modal="true" aria-labelledby="burn-bundles-title">
+ <section class="tx-modal">
+ <div class="tx-modal-head">
+ <div class="tx-modal-title">
+ <h2 id="burn-bundles-title" x-text="selectedBurnBundleBlock ? `Block ${selectedBurnBundleBlock.height} Burn Bundles` : 'Burn Bundles'"></h2>
+ <div class="tx-field"><span class="tx-label">Attestations</span><span class="tx-value number" x-text="blockBurnBundleRatio(selectedBurnBundleBlock)"></span></div>
+ </div>
+ <button type="button" @click="closeBurnBundleModal">Close</button>
+ </div>
+ <div class="bundle-summary" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length > 0">
+ <strong><span x-text="blockBurnBundleRatio(selectedBurnBundleBlock)"></span> attestations committed</strong>
+ <span class="muted">The first number is the attestations used by this block. The second is the committed committee represented in the block—not every candidate wallet considered by the network.</span>
+ </div>
+ <div class="bundle-flow" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length > 0">
+ <template x-for="slot in blockBurnBundleSlots(selectedBurnBundleBlock)" :key="`${selectedBurnBundleBlock.hash}-${slot.slot}`">
+ <div class="bundle-slot">
+ <div class="bundle-slot-head">
+ <span class="bundle-slot-number" x-text="slot.slot"></span>
+ <span class="bundle-slot-status">Included</span>
+ </div>
+ <div class="tx-field"><span class="tx-label" x-text="slot.role"></span><code x-text="addressLabel(slot.member)"></code></div>
+ <div class="tx-field"><span class="tx-label">Contribution</span><span class="tx-value text" x-text="burnBundleSlotDetail(slot)"></span></div>
+ <div class="tx-field" x-show="slot.hash"><span class="tx-label">Bundle</span><code x-text="short(slot.hash)"></code></div>
+ </div>
+ </template>
+ </div>
+ <div class="tx-modal-empty" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length === 0">Recovery blocks do not require burn-bundle attestations.</div>
+ <div class="bundle-how" x-show="blockBurnBundleSlots(selectedBurnBundleBlock).length > 0">
+ <h3>How are they chosen?</h3>
+ <div class="bundle-how-step"><span>1</span><span><strong>Finalizer:</strong> the burn-ticket ranking chooses the block finalizer, which implicitly fills slot 0.</span></div>
+ <div class="bundle-how-step"><span>2</span><span><strong>Root groups:</strong> mature UTXO lineage weight selects independent groups for the additional committee slots. One root can win at most one slot.</span></div>
+ <div class="bundle-how-step"><span>3</span><span><strong>Wallet:</strong> a wallet with a valid ticket represents each winning root group. Its burn amount does not increase that root's committee weight.</span></div>
+ <div class="bundle-how-step"><span>4</span><span><strong>Burn list:</strong> each member bundles valid pending burns by highest absolute fee, using the signature as the deterministic tie-breaker.</span></div>
+ <p class="muted">This block records the included members and attestations. Historical lineage weights and candidates that were not included are not stored in the block.</p>
+ </div>
+ </section>
+ </div>
<div class="setup-overlay transaction-overlay" x-show="selectedBurnLeaderBlock" x-transition.opacity @click.self="closeBurnLeaderRanksModal()" role="dialog" aria-modal="true" aria-labelledby="burn-ranks-title">
<section class="tx-modal">
<div class="tx-modal-head">
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -8,6 +8,7 @@ window.iunaApp = function iunaApp() {
blocks: [],
selectedBlock: null,
selectedByteBlock: null,
+ selectedBurnBundleBlock: null,
selectedTransaction: null,
selectedBurnLeaderBlock: null,
loadingInitialBlocks: false,
@@ -1201,6 +1202,14 @@ window.iunaApp = function iunaApp() {
this.selectedByteBlock = null;
},
+ openBurnBundleModal(block) {
+ this.selectedBurnBundleBlock = block;
+ },
+
+ closeBurnBundleModal() {
+ this.selectedBurnBundleBlock = null;
+ },
+
openTransactionModal(tx, context = {}) {
this.selectedTransaction = { tx, context };
},
@@ -1277,6 +1286,7 @@ window.iunaApp = function iunaApp() {
this.closeWalletUtxosModal();
this.closePowDifficultyInfo();
this.closeBurnLeaderRanksModal();
+ this.closeBurnBundleModal();
this.closeChainResetModal();
},
@@ -2846,6 +2856,37 @@ window.iunaApp = function iunaApp() {
return `${included}/${committeeSize}`;
},
+ blockBurnBundleSlots(block) {
+ if (!block || block.finalizer_mode === "recovery") return [];
+ const bundles = block.burn_bundles || block.burnBundles || [];
+ return [
+ {
+ slot: 0,
+ member: block.miner,
+ role: "Finalizer",
+ implicit: true,
+ burns: this.blockTransactions(block).filter((tx) => tx.kind === "burn"),
+ byteSize: 0,
+ hash: "",
+ },
+ ...bundles.map((bundle) => ({
+ slot: Number(bundle.slot),
+ member: bundle.member,
+ role: "Committee member",
+ implicit: false,
+ burns: bundle.burns || [],
+ byteSize: Number(bundle.byte_size ?? bundle.byteSize ?? 0),
+ hash: bundle.hash || "",
+ })),
+ ].sort((left, right) => left.slot - right.slot);
+ },
+
+ burnBundleSlotDetail(slot) {
+ if (slot?.implicit) return "Implicit block attestation";
+ const count = slot?.burns?.length || 0;
+ return `${count} burn${count === 1 ? "" : "s"} · ${slot?.byteSize || 0}B`;
+ },
+
blockByteBreakdown(block) {
const transactionRows = this.blockTransactionByteBreakdown(block);
return [