iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 08da719266fdabc8e64527ff342dc9e006b65328
parent 1c8bdef605a4ab54b76d093f12087041749b2d12
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Tue, 25 Aug 2026 00:35:05 +0200

Show block reward fee flow

Diffstat:
Msrc/adapters/http/index_html.rs | 25++++++++++++++++---------
Msrc/main_tests.rs | 12++++++++++++
Mwww/assets/iuna-ui.js | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 102 insertions(+), 11 deletions(-)

diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs @@ -432,6 +432,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> .pill.burn { background: #332918; color: #ffd070; } .pill.transfer { background: #17312a; color: #8de9cd; } .pill.mine { background: #172a34; color: #8bdcff; } + .pill.reward { background: #263016; color: #d5f55f; } .pill.error { background: #341918; color: #ffb1a8; } .mempool-panel { min-width: 0; overflow: hidden; } .mempool-strip { width: 100%; min-width: 0; display: flex; gap: 8px; overflow-x: auto; overscroll-behavior-x: contain; padding: 1px 0 10px; scroll-snap-type: x proximity; } @@ -447,6 +448,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> .utxo-node { display: grid; gap: 5px; border: 1px solid #2f363c; border-radius: 8px; padding: 10px; background: #111316; min-width: 0; } .utxo-node.burned { border-color: #5e4821; background: #1f1a12; } .utxo-node.fee { border-color: #4b5260; background: #171a20; } + .utxo-node.reward { border-color: #526329; background: #1a2013; } .utxo-node-label { display: flex; justify-content: space-between; gap: 8px; color: #8d989f; font-size: 11px; font-weight: 800; text-transform: uppercase; } .utxo-node-amount { color: #d5f55f; font-weight: 850; font-variant-numeric: tabular-nums; } .utxo-node-address, .utxo-node-ref { color: #9eb3bc; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; overflow-wrap: anywhere; } @@ -481,7 +483,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> .block-card { flex-basis: 108px; } } </style> - <script defer src="/assets/iuna-ui.js?v=111"></script> + <script defer src="/assets/iuna-ui.js?v=112"></script> <script defer src="/assets/alpine.min.js"></script> </head> <body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak> @@ -989,7 +991,12 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> </button> </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">Reward</div> + <button class="detail-link" type="button" @click="openBlockRewardModal(selectedBlock)" title="Show paid and distributed fees" aria-label="Show block reward fee flow"> + <span class="link-value">IUNA <span x-text="amountLabel(selectedBlock.reward)"></span></span> + </button> + </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"> @@ -1500,9 +1507,9 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> <div class="tx-modal-summary"> <div class="tx-field"><span class="tx-label">Source</span><span class="tx-value text" x-text="selectedTransactionLabel()"></span></div> <div class="tx-field"><span class="tx-label">Amount</span><span class="tx-value money">IUNA <span x-text="amountLabel(txAmount(selectedTransaction?.tx || {}))"></span></span></div> - <div class="tx-field"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(selectedTransaction?.tx)"></span></div> - <div class="tx-field"><span class="tx-label">From</span><code class="tx-value hash" x-text="addressLabel(txFrom(selectedTransaction?.tx || {}))"></code></div> - <div class="tx-field" x-show="txTo(selectedTransaction?.tx || {})"><span class="tx-label">To</span><code class="tx-value hash" x-text="addressLabel(txTo(selectedTransaction?.tx || {}))"></code></div> + <div class="tx-field" x-show="!isRewardTx(selectedTransaction?.tx)"><span class="tx-label">Fee</span><span class="tx-value money" x-text="txFeeLabel(selectedTransaction?.tx)"></span></div> + <div class="tx-field" x-show="!isRewardTx(selectedTransaction?.tx)"><span class="tx-label">From</span><code class="tx-value hash" x-text="addressLabel(txFrom(selectedTransaction?.tx || {}))"></code></div> + <div class="tx-field" x-show="!isRewardTx(selectedTransaction?.tx) && txTo(selectedTransaction?.tx || {})"><span class="tx-label">To</span><code class="tx-value hash" x-text="addressLabel(txTo(selectedTransaction?.tx || {}))"></code></div> <div class="tx-field" x-show="isMineTx(selectedTransaction?.tx)"><span class="tx-label">Difficulty</span><span class="tx-value number" x-text="txDifficultyBits(selectedTransaction?.tx) ?? '-'"></span></div> <div class="tx-field" x-show="isMineTx(selectedTransaction?.tx)"><span class="tx-label">Proof Bits</span><span class="tx-value number" x-text="txProofBits(selectedTransaction?.tx) ?? '-'"></span></div> <div class="tx-field" x-show="isMineTx(selectedTransaction?.tx)"><span class="tx-label">Proof Hash</span><code class="tx-value hash" x-text="txProofHash(selectedTransaction?.tx) || '-'"></code></div> @@ -1512,11 +1519,11 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> <h3>Inputs</h3> <template x-for="(input, index) in txInputs(selectedTransaction?.tx || {})" :key="txInputKey(input, index)"> <div class="utxo-node"> - <div class="utxo-node-label"><span>Input <span x-text="index + 1"></span></span><span>spent</span></div> + <div class="utxo-node-label"><span x-text="input.rewardFee ? `Paid fee ${index + 1}` : `Input ${index + 1}`"></span><span x-text="input.rewardFee ? input.transactionKind : 'spent'"></span></div> <div class="utxo-node-ref" x-text="txInputOutpoint(input)"></div> <div class="tx-field"><span class="tx-label">Value</span><span class="tx-value money" x-text="txInputAmountLabel(input)"></span></div> - <div class="tx-field"><span class="tx-label">Owner</span><code class="tx-value hash" x-text="addressLabel(input.owner)"></code></div> - <div class="tx-field"><span class="tx-label">Sig</span><code class="tx-value hash" x-text="short(input.signature)"></code></div> + <div class="tx-field" x-show="input.owner"><span class="tx-label" x-text="input.rewardFee ? 'Paid by' : 'Owner'"></span><code class="tx-value hash" x-text="addressLabel(input.owner)"></code></div> + <div class="tx-field"><span class="tx-label" x-text="input.rewardFee ? 'Transaction' : 'Sig'"></span><code class="tx-value hash" x-text="short(input.signature)"></code></div> </div> </template> <div class="tx-modal-empty" x-show="txInputs(selectedTransaction?.tx || {}).length === 0">No inputs</div> @@ -1525,7 +1532,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html> <div class="utxo-column"> <h3>Outputs</h3> <template x-for="(output, index) in txVisualOutputs(selectedTransaction?.tx || {})" :key="txOutputKey(output, index)"> - <div class="utxo-node" :class="{ burned: output.kind === 'burned', fee: output.kind === 'fee' }"> + <div class="utxo-node" :class="{ burned: output.kind === 'burned', fee: output.kind === 'fee', reward: output.kind === 'reward' }"> <div class="utxo-node-label"><span x-text="output.label"></span><span x-text="output.kind"></span></div> <div class="utxo-node-amount">IUNA <span x-text="amountLabel(output.amount)"></span></div> <template x-if="output.address"> diff --git a/src/main_tests.rs b/src/main_tests.rs @@ -95,6 +95,18 @@ fn management_ui_blocks_interaction_while_the_node_is_syncing() { assert!(html.contains("@click=\"openChainResetModal\"")); } +#[test] +fn management_ui_block_reward_opens_a_fee_flow_transaction() { + let html = include_str!("adapters/http/index_html.rs"); + let javascript = include_str!("../www/assets/iuna-ui.js"); + assert!(html.contains("@click=\"openBlockRewardModal(selectedBlock)\"")); + assert!(html.contains("input.rewardFee ? `Paid fee ${index + 1}`")); + assert!(javascript.contains("blockRewardTransaction(block)")); + assert!(javascript.contains("blockRewardOutputs(block)")); + assert!(javascript.contains("label: \"Finalizer reward\"")); + assert!(javascript.contains("label: `Committee reward (slot ${bundle.slot})`")); +} + fn ledger_with_one_spendable_iuna(wallet: &Wallet) -> Ledger { let mut genesis = BTreeMap::new(); genesis.insert(wallet.address().to_string(), 3); diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js @@ -1246,6 +1246,15 @@ window.iunaApp = function iunaApp() { this.selectedTransaction = { tx, context }; }, + openBlockRewardModal(block) { + this.openTransactionModal(this.blockRewardTransaction(block), { + source: "Block reward", + blockHeight: block.height, + blockFinalizer: block.miner, + reward: true, + }); + }, + closeTransactionModal() { this.selectedTransaction = null; }, @@ -2634,6 +2643,10 @@ window.iunaApp = function iunaApp() { return tx?.kind === "mine"; }, + isRewardTx(tx) { + return tx?.kind === "reward"; + }, + txFeeLabel(tx) { return `IUNA ${this.amountLabel(tx?.fee ?? 0)}`; }, @@ -2685,8 +2698,8 @@ window.iunaApp = function iunaApp() { const directOutputs = Array.isArray(tx.outputs) ? tx.outputs : []; for (const [index, output] of directOutputs.entries()) { rows.push({ - kind: "output", - label: `Output ${index + 1}`, + kind: output.kind || "output", + label: output.label || `Output ${index + 1}`, amount: output.amount, address: output.address, }); @@ -2837,6 +2850,7 @@ window.iunaApp = function iunaApp() { selectedTransactionLabel() { if (!this.selectedTransaction) return "-"; const { tx, context } = this.selectedTransaction; + if (context.reward && context.blockHeight !== undefined) return `Block ${context.blockHeight} reward`; if (context.blockHeight !== undefined) return `Block ${context.blockHeight}`; if (tx?.status === "pending") return "Wallet pending"; if (tx?.blockHeight !== null && tx?.blockHeight !== undefined) { @@ -2859,6 +2873,64 @@ window.iunaApp = function iunaApp() { return this.blockTransactions(block).reduce((sum, tx) => sum + Number(tx.fee || 0), 0); }, + blockRewardTransaction(block) { + const inputs = this.blockTransactions(block) + .filter((tx) => Number(tx.fee || 0) > 0) + .map((tx) => ({ + rewardFee: true, + transactionKind: tx.kind || "transaction", + amount: tx.fee, + owner: this.txFrom(tx) || this.txTo(tx) || null, + signature: tx.signature, + outpoint: { txid: tx.signature, index: "fee" }, + })); + return { + kind: "reward", + signature: block.hash, + amount: block.reward, + fee: 0, + inputs, + outputs: this.blockRewardOutputs(block), + }; + }, + + blockRewardOutputs(block) { + const reward = Math.max(0, Math.trunc(Number(block?.reward || 0))); + if (reward === 0) return []; + + const bundles = Array.isArray(block?.burn_bundles) + ? block.burn_bundles + : (Array.isArray(block?.burnBundles) ? block.burnBundles : []); + const committee = block?.finalizer_mode === "ticket" && Number(block?.finalizer_rank || 0) <= 1 + ? [...bundles] + .sort((left, right) => Number(left.slot || 0) - Number(right.slot || 0)) + .filter((bundle) => bundle.member && bundle.member !== block.miner) + : []; + const committeePool = committee.length > 0 ? Math.floor(reward / 2) : 0; + const outputs = [{ + kind: "reward", + label: "Finalizer reward", + amount: reward - committeePool, + address: block.miner, + }]; + + let remaining = committeePool; + committee.forEach((bundle, index) => { + const membersLeft = committee.length - index; + const amount = membersLeft === 1 ? remaining : Math.floor(remaining / membersLeft); + remaining -= amount; + if (amount > 0) { + outputs.push({ + kind: "reward", + label: `Committee reward (slot ${bundle.slot})`, + amount, + address: bundle.member, + }); + } + }); + return outputs; + }, + blockTimestampLabel(block) { const timestamp = Number(block?.timestamp_ms ?? block?.timestampMs); if (Number(block?.height) === 0) return "Genesis";