Merge explorer-dn3 91b64b80 into master (gate: green on 91b64b80, recorded by tools/ci/pre-push.sh; landed on the build mirror)
This commit is contained in:
commit
43547048be
17 changed files with 1745 additions and 156 deletions
|
|
@ -127,3 +127,21 @@ site. None of this is in the repo; the stats API of this branch is the network s
|
|||
price when ordered.
|
||||
- `EXPLORER_EVM_RPC` on Vercel: no public devnet EVM RPC exists, so balances show "no EVM RPC configured" on the devnet
|
||||
deployment; the testnet's `https://rpc.testnet.igneum.network` is the value for the testnet.
|
||||
|
||||
## 8. Devnet 3 explorer (8 October 2026, the explorer lane; the founder's order of 11:00 UK: a public explorer the same day)
|
||||
|
||||
What serves, all at igneum.network (the host explorer.igneum.network is a CNAME to the same Vercel project with a root rewrite to /explorer):
|
||||
|
||||
| Page | What it shows | Data |
|
||||
|---|---|---|
|
||||
| `/explorer` | the Devnet 3 strip (status, height, DAA, hash rate, chain rate, finality, program class, proving), the DAG picture (newest 150 blocks, one lane per miner, parents drawn, lock and paid marks), latest blocks (class, shards paid, final), latest transactions, the proving summary, the API list | `/api/explorer?stats=1`, `?dag=150`, `?blocks=50`, `?txs=30`, `?proving=1` |
|
||||
| `/block/<hash>` or `/block/<number>` | header, parents, children, mergeset, coinbase, the EVM transactions it executed (from the indexer), the shard plan with each shard's state and the node's verdict, the checkpoint and certificate, the lock that covers it, the class; **the verified badge**: for a block under a lock the browser fetches `/api/checkpoint?index=N` and runs `site/verify/core.js` (header hashes recomputed, BLS keys aggregated, the signature checked over the weight rule) and the badge reads "verified under lock N in this browser" or the failure reason | `?block=`, `?height=`, `/api/checkpoint?index=` |
|
||||
| `/tx/<hash>` | status, value, fee, gas, nonce, type, input, the receipt's Igneum section (miner tip, both burns, pgas, app share, including block and miner), the executing block's lock and shard state, the logs | `?tx=` |
|
||||
| `/address/<addr>` | balance, nonce, code (contract or account), sent and received counts, the transactions of the last 7 days, the blocks mined and what they earned | `?address=` (`&before=<number>` pages the transactions) |
|
||||
| `/proving` | the node's proving state, the newest 60 chain blocks with their shards by state and median lag, the provers of 24 h with payouts, the block-to-paid latency of the last hour (p50, p90) and the 24-hour totals by state | `?proving=1` |
|
||||
|
||||
The indexer: `tools/observer/explorer-indexer.mjs`, the observer's sibling on build-1 (unit `igneum-explorer-dn3`, User build, the same env file, `LIVE_TABLE_PREFIX=dn3_`, `IGNEUM_EVM_RPC` on loopback). It reads `eth_getBlockByNumber` with transactions and `eth_getBlockReceipts` per chain block, refreshes `eth_getBalance`, `eth_getTransactionCount` and `eth_getCode` for every address a block touches and for the observer's miners of the last 10 minutes, and keeps `igneum_getNodeInfo` (the class floors and every activation) in `dn3_explorer_state`. Tables `dn3_live_txs`, `dn3_live_accounts`, `dn3_explorer_blocks`, `dn3_explorer_state`; 7 days of transactions. Measured 10:04 to 10:07 UTC: 26,847 chain blocks and 71,218 transactions from genesis in 3 minutes; steady state 0.4 chain blocks per second at 16 percent of one core. Two classes met the same morning, each with its guard: the unit's first start read the first devnet's node for 25 s (the env file's `IGNEUM_EVM_RPC`), so the indexer now refuses any node whose `igneum_getNodeInfo.network` is not `EXPLORER_NETWORK`; and the observer node's restart at 10:21 UTC on 0324-5b673577 re-executed from block 0, so a tip below the indexed number re-reads from the tip and the API settles `chain_now` against the highest number ever indexed, not the current tip.
|
||||
|
||||
Read against the EVM: a block the observer called a chain block whose number the EVM later gave to another block (tip reorgs on Devnet 3 run to depth 18, `igneum_getBudgets.deepestReorg` at 10:0x UTC) reads "replaced" on the list and the block page. The table prefix default of `/api/explorer`, `/api/stats`, `/api/supply` and `/api/checkpoint` moved to `dn3_` (the live deploy had served the first devnet on those four while `/api/live` served Devnet 3).
|
||||
|
||||
Not yet: the STARK itself is not re-run in the browser (the node's verdict is shown as the node's; the explorer-verify branch's binding check of proof bytes to records is not merged); `eth_sendRawTransaction` and live balances from the public RPC (`rpc.devnet.igneum.network`, up since 11:08 UK) are not wired into the pages (`EXPLORER_EVM_RPC` on the deployment switches the balance read to live); a checkpoints list page; the program id per epoch (no RPC); internal transactions (no tracing RPC).
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Igneum address</title>
|
||||
<meta name="description" content="One Igneum address: the blocks it mined in the last day, what they earned, and its balance.">
|
||||
<title>Igneum Devnet 3 address</title>
|
||||
<meta name="description" content="One Devnet 3 address: its balance, nonce and code, its transactions of the last seven days, the blocks it mined in the last day and what they earned.">
|
||||
<link rel="canonical" href="https://igneum.network/address">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="theme-color" content="#0C0C0E">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Igneum">
|
||||
<meta property="og:title" content="Igneum address">
|
||||
<meta property="og:description" content="One Igneum address: the blocks it mined in the last day, what they earned, and its balance.">
|
||||
<meta property="og:description" content="One Devnet 3 address: its balance, nonce and code, its transactions of the last seven days, the blocks it mined in the last day and what they earned.">
|
||||
<meta property="og:url" content="https://igneum.network/address">
|
||||
<meta property="og:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta property="og:image:width" content="256">
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Igneum address">
|
||||
<meta name="twitter:description" content="One Igneum address: the blocks it mined in the last day, what they earned, and its balance.">
|
||||
<meta name="twitter:description" content="One Devnet 3 address: its balance, nonce and code, its transactions of the last seven days, the blocks it mined in the last day and what they earned.">
|
||||
<meta name="twitter:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
|
|
@ -92,6 +92,12 @@ dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
|
|||
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}.two .card{margin-top:0}}
|
||||
.two-wrap{margin-top:var(--gap)}
|
||||
main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--line);border-radius:16px;padding:25px;margin-top:24px}.card .viz-head h2{font-size:19px}.card .viz-head .eyebrow{margin:0}
|
||||
.badge{display:inline-flex;align-items:center;gap:6px;font-family:var(--f-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;padding:4px 9px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2);margin:0 6px 6px 0}
|
||||
.badge.ok{border-color:var(--green,#5CC48A);color:var(--green,#5CC48A)}.badge.hot{border-color:var(--molten);color:var(--molten)}.badge.bad{border-color:var(--excluded);color:var(--excluded)}.badge.wait{color:var(--ash)}
|
||||
.bar{display:inline-block;width:72px;height:7px;border-radius:4px;background:var(--line);vertical-align:middle;margin-right:7px;overflow:hidden}.bar i{display:block;height:100%;background:var(--green,#5CC48A)}.bar i.part{background:var(--molten)}
|
||||
.dagbox{position:relative;width:100%;height:320px;border:1px solid var(--line);border-radius:12px;background:var(--obsidian);overflow:hidden}.dagbox canvas{width:100%;height:100%;display:block;cursor:pointer}
|
||||
.dagtip{position:absolute;pointer-events:none;font:11px/1.5 var(--mono);color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:8px;padding:8px 10px;max-width:300px;display:none}
|
||||
.kv{color:var(--ash)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -131,8 +137,8 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
</div>
|
||||
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/live" data-nav="live" aria-current="page"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
|
|
@ -170,8 +176,8 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Network</div>
|
||||
<a href="/live" data-nav="live" aria-current="page"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
|
|
@ -246,7 +252,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<main id="main" class="wrap">
|
||||
<div class="head">
|
||||
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><a href="/explorer">Explorer</a><span>/</span><span>Address</span></div>
|
||||
<div class="eyebrow"><a href="/explorer">Explorer</a> · address</div>
|
||||
<div class="eyebrow"><a href="/explorer">Devnet 3 explorer</a> · address</div>
|
||||
<h1 class="hash" id="title">Address</h1>
|
||||
<div id="chips"></div>
|
||||
<div class="msg" id="msg" aria-live="polite"></div>
|
||||
|
|
@ -254,11 +260,19 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<div id="body" hidden>
|
||||
<div class="strip">
|
||||
<div class="cell"><div class="k">Balance</div><div class="v" id="a-balance">n/a</div><div class="s" id="a-balance-s">eth_getBalance</div></div>
|
||||
<div class="cell"><div class="k">Nonce</div><div class="v" id="a-nonce">n/a</div><div class="s" id="a-nonce-s">transactions sent from this account</div></div>
|
||||
<div class="cell"><div class="k">Transactions, 7 d</div><div class="v" id="a-txs">0</div><div class="s" id="a-txs-s">sent and received, as indexed</div></div>
|
||||
<div class="cell"><div class="k">Code</div><div class="v" id="a-code">n/a</div><div class="s" id="a-code-s">eth_getCode</div></div>
|
||||
<div class="cell"><div class="k">Blocks, 24 h</div><div class="v" id="a-blocks">0</div><div class="s" id="a-blocks-s"></div></div>
|
||||
<div class="cell"><div class="k">Earned, 24 h</div><div class="v" id="a-earned">0<small>IGN</small></div><div class="s">80% of each blue block's subsidy</div></div>
|
||||
<div class="cell"><div class="k">Last block</div><div class="v" id="a-last">n/a</div><div class="s" id="a-first"></div></div>
|
||||
<div class="cell"><div class="k">Network</div><div class="v">Devnet 3</div><div class="s">chain id 4463, coins have no value</div></div>
|
||||
</div>
|
||||
<div class="card"><div class="viz-head"><h2>Identity</h2></div><dl id="ident"></dl><p class="note">A miner names its EVM payout address in every coinbase (the IGNA tag) beside the coinbase script's own address, so the two forms name the same miner. The vote keys are the finality identities that signed these blocks; a card runs several.</p></div>
|
||||
<div class="card"><div class="viz-head"><h2>Transactions</h2><div class="eyebrow" id="tx-eyebrow">newest 50 of the last 7 days</div></div>
|
||||
<div class="tbl"><table><thead><tr><th>Transaction</th><th class="n">Block</th><th>Direction</th><th>Counterparty</th><th class="n">Value</th><th>Status</th><th>Time</th></tr></thead><tbody id="txs"></tbody></table></div>
|
||||
<button class="btn" id="more-tx" type="button" style="margin-top:14px">Older transactions</button>
|
||||
</div>
|
||||
<div class="card"><div class="viz-head"><h2>Blocks mined</h2><div class="eyebrow">newest 50 of the last 24 hours</div></div>
|
||||
<div class="tbl"><table><thead><tr><th>Block</th><th class="n">Number</th><th class="n">DAA</th><th class="n">Blue score</th><th>Colour</th><th class="n">Subsidy</th><th class="n">Txs</th><th>Time</th></tr></thead><tbody id="blocks"></tbody></table></div>
|
||||
</div>
|
||||
|
|
@ -341,26 +355,43 @@ const esc = s => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').re
|
|||
const link = h => `<a class="hash" href="/block/${esc(h)}" title="${esc(h)}">${esc(shortHash(h, 10, 6))}</a>`;
|
||||
const dl = pairs => pairs.filter(p => p).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join('');
|
||||
const a = decodeURIComponent(location.pathname.split('/').pop() || '');
|
||||
let now = 0, oldestTx = 0, me = null;
|
||||
function txRow(t) {
|
||||
const out = me && t.from === me; const cp = out ? t.to : t.from;
|
||||
return `<tr><td><a class="hash" href="/tx/${esc(t.hash)}">${esc(shortHash(t.hash, 10, 6))}</a></td><td class="n"><a href="/block/${esc(t.block_hash)}">${int(t.number)}</a></td><td>${out ? '<span class="chip">out</span>' : '<span class="chip ok">in</span>'}</td><td>${cp ? `<a class="hash" href="/address/${esc(cp)}" title="${esc(cp)}">${esc(shortHash(cp, 8, 6))}</a>` : (t.contract_address ? `<span class="kv">created</span> <a class="hash" href="/address/${esc(t.contract_address)}">${esc(shortHash(t.contract_address, 8, 6))}</a>` : '<span class="kv">none</span>')}</td><td class="n">${ignFromWei(t.value_wei, 4)} IGN</td><td>${t.status === 'success' ? '<span class="chip ok">ok</span>' : t.status === 'failed' ? '<span class="chip" style="border-color:var(--excluded);color:var(--excluded)">failed</span>' : '<span class="kv">no receipt</span>'}</td><td title="${t.ts ? new Date(t.ts).toISOString() : ''}">${t.ts ? rel(now - t.ts) : 'n/a'}</td></tr>`;
|
||||
}
|
||||
async function loadMore() {
|
||||
if (!oldestTx) return;
|
||||
const j = await fetch('/api/explorer?address=' + encodeURIComponent(a) + '&before=' + oldestTx).then(r => r.json());
|
||||
if (!j.ok) return; $('txs').insertAdjacentHTML('beforeend', j.txs.map(txRow).join('')); if (j.txs.length) oldestTx = j.txs[j.txs.length - 1].number; else $('more-tx').disabled = true;
|
||||
}
|
||||
$('more-tx').addEventListener('click', () => { loadMore().catch(() => {}); });
|
||||
async function load() {
|
||||
const r = await fetch('/api/explorer?address=' + encodeURIComponent(a)); const j = await r.json();
|
||||
$('title').textContent = a;
|
||||
if (!j.ok) { $('msg').textContent = j.error || 'Not found.'; return; }
|
||||
document.title = `Igneum address ${shortHash(a, 8, 6)}`;
|
||||
const now = new Date(j.now).getTime();
|
||||
$('chips').innerHTML = `<span class="chip">${j.kind === 'evm' ? 'EVM payout address' : 'coinbase address'}</span>` + (j.blocks_10m ? '<span class="chip hot">mining now</span>' : '');
|
||||
if (j.balance.available) { $('a-balance').innerHTML = esc(ignFromWei(j.balance.wei, 4)) + '<small>IGN</small>'; $('a-balance-s').textContent = `${j.balance.source}, ${j.network || ''}`; }
|
||||
document.title = `Devnet 3 address ${shortHash(a, 8, 6)}`;
|
||||
now = new Date(j.now).getTime(); me = j.evm_address; const ac = j.account;
|
||||
$('chips').innerHTML = `<span class="chip">${j.kind === 'evm' ? (ac && ac.is_contract ? 'contract' : 'EVM account') : 'coinbase address'}</span>` + (j.blocks_10m ? '<span class="chip hot">mining now</span>' : '') + (ac && ac.miner ? '<span class="chip">miner payout address</span>' : '');
|
||||
if (j.balance.available) { $('a-balance').innerHTML = esc(ignFromWei(j.balance.wei, 4)) + '<small>IGN</small>'; $('a-balance-s').textContent = j.balance.source === 'indexer' ? `read by the indexer ${j.balance.refreshed_at ? rel(now - new Date(j.balance.refreshed_at).getTime()) : ''}` : `${j.balance.source}, live`; }
|
||||
else { $('a-balance').textContent = 'n/a'; $('a-balance-s').textContent = j.balance.reason; }
|
||||
$('a-nonce').textContent = ac && ac.nonce !== null ? int(ac.nonce) : 'n/a';
|
||||
$('a-txs').textContent = ac ? int((ac.sent || 0) + (ac.received || 0)) : '0'; $('a-txs-s').textContent = ac ? `${int(ac.sent)} sent, ${int(ac.received)} received, as indexed` : 'none indexed';
|
||||
$('a-code').textContent = ac && ac.code_bytes !== null ? (ac.is_contract ? int(ac.code_bytes) + ' B' : 'none') : 'n/a'; $('a-code-s').textContent = ac && ac.is_contract ? 'a contract: bytes of code at this address' : 'an externally owned account';
|
||||
$('a-blocks').textContent = int(j.blocks_24h); $('a-blocks-s').textContent = `${j.blocks_10m} in 10 min, ${j.blue_24h} blue, ${j.red_24h} red, ${j.chain_24h} on the chain`;
|
||||
$('a-earned').innerHTML = esc(ign(j.earned_24h_sompi, 2)) + '<small>IGN</small>';
|
||||
$('a-last').textContent = j.last_seen ? rel(now - new Date(j.last_seen).getTime()) : 'none in 24 h';
|
||||
$('a-first').textContent = j.first_seen ? 'first in the window ' + rel(now - new Date(j.first_seen).getTime()) : '';
|
||||
$('ident').innerHTML = dl([
|
||||
['EVM address', j.evm_address ? `<a class="hash" href="/address/${esc(j.evm_address)}">${esc(j.evm_address)}</a>` : '<span style="color:var(--ash)">none seen</span>'],
|
||||
['Coinbase address', j.utxo_address ? `<a class="hash" href="/address/${esc(j.utxo_address)}">${esc(j.utxo_address)}</a>` : '<span style="color:var(--ash)">none seen</span>'],
|
||||
['Vote keys', j.miner_ids.length ? j.miner_ids.map(m => `<span class="id">${esc(m.id)}</span> <span style="color:var(--ash)">${m.blocks}</span>`).join(' ') : 'none'],
|
||||
['Earned, 24 h', `${ign(j.earned_24h_sompi, 8)} IGN <span style="color:var(--ash)">${esc(j.earned_note)}</span>`],
|
||||
['EVM address', j.evm_address ? `<a class="hash" href="/address/${esc(j.evm_address)}">${esc(j.evm_address)}</a>` : '<span class="kv">none seen</span>'],
|
||||
['Coinbase address', j.utxo_address ? `<a class="hash" href="/address/${esc(j.utxo_address)}">${esc(j.utxo_address)}</a>` : '<span class="kv">none seen</span>'],
|
||||
ac ? ['First and last seen', `${ac.first_seen_number === null ? 'no transaction indexed' : `chain block ${int(ac.first_seen_number)} to ${int(ac.last_seen_number)}`}`] : null,
|
||||
['Vote keys', j.miner_ids.length ? j.miner_ids.map(m => `<span class="id">${esc(m.id)}</span> <span class="kv">${m.blocks}</span>`).join(' ') : 'none'],
|
||||
['Earned, 24 h', `${ign(j.earned_24h_sompi, 8)} IGN <span class="kv">${esc(j.earned_note)}</span>`],
|
||||
]);
|
||||
$('blocks').innerHTML = j.recent.length ? j.recent.map(b => `<tr><td><i class="sw ${b.chain ? 'chain' : b.color}"></i>${link(b.hash)}</td><td class="n">${b.number === null ? '<span style="color:var(--ash)">off chain</span>' : int(b.number)}</td><td class="n">${int(b.daa)}</td><td class="n">${int(b.blue_score)}</td><td>${b.chain ? 'chain' : esc(b.color)}</td><td class="n">${b.subsidy_sompi ? ign(b.subsidy_sompi, 4) : 'n/a'}</td><td class="n">${b.tx_count ?? 'n/a'}</td><td title="${new Date(b.ts).toISOString()}">${rel(now - b.ts)}</td></tr>`).join('') : '<tr><td colspan="8" class="empty">No blocks from this address in the last 24 hours.</td></tr>';
|
||||
$('txs').innerHTML = j.txs.length ? j.txs.map(txRow).join('') : `<tr><td colspan="7" class="empty">${j.evm_address ? 'No transaction indexed for this address in the last 7 days.' : 'A coinbase address has no EVM transactions; its EVM payout address does.'}</td></tr>`;
|
||||
if (j.txs.length) oldestTx = j.txs[j.txs.length - 1].number; else $('more-tx').hidden = true;
|
||||
$('blocks').innerHTML = j.recent.length ? j.recent.map(b => `<tr><td><i class="sw ${b.chain ? 'chain' : b.color}"></i>${link(b.hash)}</td><td class="n">${b.number === null ? '<span class="kv">off chain</span>' : int(b.number)}</td><td class="n">${int(b.daa)}</td><td class="n">${int(b.blue_score)}</td><td>${b.chain ? 'chain' : esc(b.color)}</td><td class="n">${b.subsidy_sompi ? ign(b.subsidy_sompi, 4) + ' IGN' : 'n/a'}</td><td class="n">${b.tx_count ?? 'n/a'}</td><td title="${new Date(b.ts).toISOString()}">${rel(now - b.ts)}</td></tr>`).join('') : '<tr><td colspan="8" class="empty">No block mined by this address in the last 24 hours.</td></tr>';
|
||||
$('body').hidden = false;
|
||||
}
|
||||
load().catch(e => { $('msg').textContent = 'The API did not answer: ' + e.message; });
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ export function neon(url = process.env.DATABASE_URL) {
|
|||
};
|
||||
}
|
||||
export const num = v => (v === null || v === undefined ? null : Number(v));
|
||||
export const tablePrefix = () => (process.env.LIVE_TABLE_PREFIX || '').replace(/[^a-z0-9_]/gi, '');
|
||||
// LIVE_TABLE_PREFIX names the observer whose tables are read; unset means dn3_ (Devnet 3, the igneum-observer-dn3 unit on the build
|
||||
// box, the same default as live.mjs since 8 October 2026: before that the explorer, stats and supply read the first devnet's tables
|
||||
// while /api/live read Devnet 3). An empty string reads the unprefixed tables.
|
||||
export const tablePrefix = () => (process.env.LIVE_TABLE_PREFIX === undefined ? 'dn3_' : process.env.LIVE_TABLE_PREFIX).replace(/[^a-z0-9_]/gi, '');
|
||||
/** EVM chain id from the network name (docs/design/execution-layer.md 8.1: 4461 mainnet, 4462 testnet, 4463 devnet). */
|
||||
export function chainIdOf(network) {
|
||||
const n = String(network || '');
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
// selected-chain headers from the previous locked checkpoint to this one. Read from what tools/observer wrote to
|
||||
// Neon (table live_certificates, or fintest_live_certificates for the test network).
|
||||
//
|
||||
// ?source=live the live devnet (default; falls back to the test network while the live chain has no lock yet)
|
||||
// ?source=test tonight's finality test network (igneum-devnet-7)
|
||||
// ?source=live Devnet 3 (default: the dn3_ tables, or LIVE_TABLE_PREFIX; falls back to the test network while the chain has no lock yet)
|
||||
// ?source=dn3 the same as live (the Devnet 3 name, for callers that want to say so)
|
||||
// ?source=test the finality test network's tables (fintest_live_certificates)
|
||||
// ?index=N one certified checkpoint by index instead of the newest (the explorer's per-block re-check, 8 October 2026)
|
||||
//
|
||||
// Zero dependencies: Neon's HTTP SQL endpoint over Node's built-in fetch, like live.mjs.
|
||||
|
||||
|
|
@ -29,9 +31,10 @@ function neon() {
|
|||
|
||||
const num = v => (v === null || v === undefined ? null : Number(v));
|
||||
|
||||
async function latest(sql, table) {
|
||||
const tablePrefix = () => (process.env.LIVE_TABLE_PREFIX === undefined ? 'dn3_' : process.env.LIVE_TABLE_PREFIX).replace(/[^a-z0-9_]/gi, '');
|
||||
async function latest(sql, table, index = null) {
|
||||
// A table the observer has not created yet (the live chain before the cut-over) reads as "no certificate"
|
||||
const rows = await sql(`SELECT * FROM ${table} ORDER BY index DESC LIMIT 1`).catch(() => []);
|
||||
const rows = await (index === null ? sql(`SELECT * FROM ${table} ORDER BY index DESC LIMIT 1`) : sql(`SELECT * FROM ${table} WHERE index = $1 LIMIT 1`, [index])).catch(() => []);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
|
|
@ -82,9 +85,10 @@ export default async function handler(req, res) {
|
|||
try {
|
||||
const sql = neon();
|
||||
const want = String((req.query && req.query.source) || 'live');
|
||||
const idx = req.query && req.query.index !== undefined && /^\d{1,12}$/.test(String(req.query.index)) ? Number(req.query.index) : null;
|
||||
let row = null, source = null;
|
||||
if (want !== 'test') { row = await latest(sql, 'live_certificates'); if (row) source = 'live'; }
|
||||
if (!row) { row = await latest(sql, 'fintest_live_certificates'); if (row) source = 'test'; }
|
||||
if (want !== 'test') { row = await latest(sql, `${tablePrefix()}live_certificates`, idx); if (row) source = 'live'; }
|
||||
if (!row && idx === null) { row = await latest(sql, 'fintest_live_certificates'); if (row) source = 'test'; }
|
||||
if (!row) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
return res.status(404).json({ ok: false, error: 'no certified checkpoint stored yet' });
|
||||
|
|
|
|||
|
|
@ -1,19 +1,47 @@
|
|||
// Igneum DAG explorer, server half. GET /api/explorer with one of:
|
||||
// ?blocks=N[&before=<ms>] the latest N blocks (10 to 200, default 50), older than `before` when given
|
||||
// ?block=<hash> one block: row, header detail, parents, mergeset, transactions, proof records, shards,
|
||||
// checkpoint and certificate when it is one, children, the chain block that merged it
|
||||
// Igneum Devnet 3 explorer, server half. GET /api/explorer with one of:
|
||||
// ?blocks=N[&before=<ms>] the latest N blocks (10 to 200, default 50), older than `before` when given; each with its shard
|
||||
// summary (planned, paid), its lock state and its program class
|
||||
// ?block=<hash> one block: row, header detail, parents, mergeset, transactions (from the indexer), proof records,
|
||||
// shards, checkpoint and certificate when it is one, children, the chain block that merged it,
|
||||
// the lock that covers it, its class
|
||||
// ?height=N the chain block with that number (the EVM block number), same shape as ?block
|
||||
// ?address=<0x.. | igneum..:bech32> blocks mined, 24-hour and 10-minute counts, what those blocks earned, and the
|
||||
// balance through eth_getBalance when EXPLORER_EVM_RPC names a reachable EVM JSON-RPC
|
||||
// ?tx=<hash> one EVM transaction: the indexed row (receipt, logs, the igneum fee split) and its block's state
|
||||
// ?txs=N[&before=<number>] the latest N transactions (10 to 200, default 50), from chain blocks numbered below `before`
|
||||
// ?address=<0x.. | igneum..:bech32> the account (balance, nonce, code, from the indexer), its transactions, the blocks it
|
||||
// mined, 24-hour and 10-minute counts, what those blocks earned; the balance comes live through
|
||||
// eth_getBalance when EXPLORER_EVM_RPC names a reachable EVM JSON-RPC, else from the indexer's read
|
||||
// ?proving=1 the proving view: the node's proving state, the last 60 chain blocks with their shards, the
|
||||
// provers of the last 24 hours with their payouts, the block-to-paid latency
|
||||
// ?stats=1 the network: hash rate, difficulty, DAA, blue score, chain rate, voters and weight, the newest
|
||||
// lock, the class floor and every activation the node reports, the indexer's state
|
||||
// ?dag=N the last N blocks (20 to 400, default 120) with parents, colour, miner, shards and lock, for the DAG view
|
||||
// ?search=<q> routes a hash, a height or an address to its page, or says what it is not
|
||||
// Everything is read from what tools/observer wrote to Neon; the only live call is the balance. Cached 5 s.
|
||||
// Everything is read from what tools/observer and tools/observer/explorer-indexer wrote to Neon (table prefix dn3_ by default,
|
||||
// LIVE_TABLE_PREFIX otherwise); the only live call is the balance. Cached 5 s. Every answer names the network: Devnet 3.
|
||||
import { neon, num, tablePrefix, chainIdOf } from './_neon.mjs';
|
||||
import { classify } from '../lib/explorer.mjs';
|
||||
|
||||
const STALE_AFTER_S = 30;
|
||||
export const NETWORK_LABEL = 'Devnet 3';
|
||||
const ROW = `hash, number, blue_score, daa_score, timestamp_ms, parents, parent_hashes, is_chain_block, vote_key_hash, miner_address, evm_miner, engine,
|
||||
tx_count, proof_records, subsidy_sompi, paid_sompi, selected_parent, color, received_at`;
|
||||
// the EVM's own hash for the block's number (the indexer's explorer_blocks): a block the observer called a chain block that a tip
|
||||
// reorg later replaced keeps its number in live_blocks, so the page reads it against this and says "replaced" (Devnet 3 reorgs at the
|
||||
// tip run to depth 18, igneum_getBudgets.deepestReorg read 8 October 2026 10:0x UTC)
|
||||
const evmHash = T => `(SELECT e.hash FROM ${T}explorer_blocks e WHERE e.number = b.number LIMIT 1) AS evm_hash`;
|
||||
const TXROW = `hash, block_hash, block_number, tx_index, from_addr, to_addr, value_wei, nonce, gas, gas_price, gas_used, effective_gas_price, status,
|
||||
contract_address, input_bytes, selector, logs_count, tx_type, ts_ms`;
|
||||
|
||||
/** The program class a block of this DAA score was mined under, from the node's parameters (program_class_v5_activation_daa). */
|
||||
export function classAt(daa, params) {
|
||||
const p = params || {};
|
||||
const floors = Object.entries(p).filter(([k]) => /^program_class_v(\d+)_activation_daa$/.test(k)).map(([k, v]) => ({ v: Number(k.match(/v(\d+)/)[1]), daa: Number(v) })).filter(x => Number.isFinite(x.daa)).sort((a, b) => a.v - b.v);
|
||||
let cls = 4; // class v4 is the floor of Devnet 3 (every activation at 0 in its network object)
|
||||
for (const f of floors) if (daa !== null && daa !== undefined && daa >= f.daa && f.v > cls) cls = f.v;
|
||||
return { class: `v${cls}`, floors: floors.map(f => ({ class: `v${f.v}`, daa: f.daa })) };
|
||||
}
|
||||
|
||||
let settledBelow = -1; // numbers above this are still moving at the tip; chain_now is left null for them (set per request from the indexer's number)
|
||||
function row(b) {
|
||||
return {
|
||||
hash: b.hash, number: num(b.number), blue_score: num(b.blue_score), daa: num(b.daa_score), ts: num(b.timestamp_ms), rx: new Date(b.received_at).getTime(),
|
||||
|
|
@ -21,8 +49,28 @@ function row(b) {
|
|||
miner_id: b.vote_key_hash ? String(b.vote_key_hash).slice(0, 8) : null, vote_key_hash: b.vote_key_hash || null, miner: b.miner_address || null, evm_miner: b.evm_miner || null, engine: b.engine || null,
|
||||
tx_count: num(b.tx_count), proof_records: num(b.proof_records), subsidy_sompi: b.subsidy_sompi === null || b.subsidy_sompi === undefined ? null : String(b.subsidy_sompi),
|
||||
paid_sompi: b.paid_sompi === null || b.paid_sompi === undefined ? null : String(b.paid_sompi), selected_parent: b.selected_parent || null,
|
||||
// chain_now: still the chain block of its number by the EVM's reading (null when the indexer has not reached the number)
|
||||
chain_now: b.is_chain_block ? (b.evm_hash === undefined || b.evm_hash === null || num(b.number) > settledBelow ? null : b.evm_hash === b.hash) : false,
|
||||
};
|
||||
}
|
||||
function txrow(t) {
|
||||
return {
|
||||
hash: t.hash, block_hash: t.block_hash, number: num(t.block_number), index: num(t.tx_index), from: t.from_addr, to: t.to_addr, value_wei: t.value_wei === null || t.value_wei === undefined ? '0' : String(t.value_wei),
|
||||
nonce: num(t.nonce), gas: num(t.gas), gas_price: t.gas_price === null || t.gas_price === undefined ? null : String(t.gas_price), gas_used: num(t.gas_used),
|
||||
effective_gas_price: t.effective_gas_price === null || t.effective_gas_price === undefined ? null : String(t.effective_gas_price),
|
||||
status: t.status === null || t.status === undefined ? null : (Number(t.status) === 1 ? 'success' : 'failed'), contract_address: t.contract_address || null,
|
||||
input_bytes: num(t.input_bytes), selector: t.selector || null, logs_count: num(t.logs_count), type: num(t.tx_type), ts: num(t.ts_ms),
|
||||
};
|
||||
}
|
||||
const shardSummary = s => ({ planned: num(s.planned) || 0, proving: num(s.proving) || 0, verified: num(s.verified) || 0, paid: num(s.paid) || 0, total: num(s.total) || 0, median_lag: s.median_lag === null || s.median_lag === undefined ? null : Number(s.median_lag) });
|
||||
|
||||
/** Lock state of a block against the newest lock: a chain block at or under the lock's blue score is final; a merged block is final with its merger. */
|
||||
function lockState(b, lock, mergerBlue) {
|
||||
if (!lock) return { final: false, reason: 'no lock yet' };
|
||||
if (b.chain) return b.blue_score <= lock.blue_score ? { final: true, index: lock.index, reason: `chain block under lock ${lock.index}` } : { final: false, index: lock.index, reason: `above lock ${lock.index} (blue score ${lock.blue_score})` };
|
||||
if (mergerBlue !== null && mergerBlue !== undefined) return mergerBlue <= lock.blue_score ? { final: true, index: lock.index, reason: `merged by a chain block under lock ${lock.index}` } : { final: false, index: lock.index, reason: `merged above lock ${lock.index}` };
|
||||
return { final: false, index: lock.index, reason: 'not merged yet' };
|
||||
}
|
||||
|
||||
async function evmCall(url, method, params) {
|
||||
const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(4000) });
|
||||
|
|
@ -41,10 +89,22 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) {
|
|||
const EVM = env.EXPLORER_EVM_RPC || null;
|
||||
try {
|
||||
sql = sql || neon(env.DATABASE_URL);
|
||||
const head = await sql(`SELECT now() AS now, (SELECT row_to_json(s) FROM ${T}live_state s WHERE s.id = 1) AS state`);
|
||||
const now = new Date(head[0].now).getTime(); const s = head[0].state || null;
|
||||
const head = await sql(`SELECT now() AS now, (SELECT row_to_json(s) FROM ${T}live_state s WHERE s.id = 1) AS state,
|
||||
(SELECT row_to_json(x) FROM ${T}explorer_state x WHERE x.id = 1) AS indexer, (SELECT max(number) FROM ${T}explorer_blocks) AS indexed_max`).catch(async e => {
|
||||
if (/explorer_state/.test(String(e.message))) return sql(`SELECT now() AS now, (SELECT row_to_json(s) FROM ${T}live_state s WHERE s.id = 1) AS state, NULL AS indexer`);
|
||||
throw e;
|
||||
});
|
||||
const now = new Date(head[0].now).getTime(); const s = head[0].state || null; const ix = head[0].indexer || null;
|
||||
const updated = s && s.updated_at ? new Date(s.updated_at).getTime() : null;
|
||||
const base = { ok: true, now: new Date(now).toISOString(), network: s ? s.network : null, chain_id: s ? chainIdOf(s.network) : null, stale: updated === null || (now - updated) / 1000 > STALE_AFTER_S, evm_rpc_configured: !!EVM };
|
||||
const params = ix && ix.node_info && ix.node_info.params ? ix.node_info.params : null;
|
||||
const indexer = ix ? { indexed_number: num(ix.indexed_number), tip_number: num(ix.tip_number), backlog: num(ix.backlog), txs_total: num(ix.txs_total), accounts_total: num(ix.accounts_total), blocks_per_s: num(ix.blocks_per_s), updated_at: ix.updated_at, stale: !ix.updated_at || (now - new Date(ix.updated_at).getTime()) / 1000 > STALE_AFTER_S, last_error: ix.last_error || null } : { stale: true, reason: 'the explorer indexer has not written yet' };
|
||||
// the highest number the indexer has ever read, less three: a node re-syncing from a snapshot reports a low tip for a while
|
||||
// (10:21 UTC on 8 October 2026: the observer node restarted and its tip read 1,981 against 27,145 indexed) and must not unsettle the table
|
||||
settledBelow = head[0].indexed_max === null || head[0].indexed_max === undefined ? -1 : Number(head[0].indexed_max) - 3;
|
||||
const base = { ok: true, now: new Date(now).toISOString(), network: s ? s.network : null, network_label: NETWORK_LABEL, chain_id: ix && ix.chain_id ? num(ix.chain_id) : (s ? chainIdOf(s.network) : null), stale: updated === null || (now - updated) / 1000 > STALE_AFTER_S, evm_rpc_configured: !!EVM, indexer };
|
||||
const lockRow = await sql(`SELECT index, hash, blue_score, daa_score, fraction_total, voters, locked_at FROM ${T}live_checkpoints WHERE state = 'locked' ORDER BY index DESC LIMIT 1`).catch(() => []);
|
||||
const lock = lockRow[0] ? { index: num(lockRow[0].index), hash: lockRow[0].hash, blue_score: num(lockRow[0].blue_score), daa: num(lockRow[0].daa_score), fraction_total: num(lockRow[0].fraction_total), voters: num(lockRow[0].voters), locked_at: lockRow[0].locked_at } : null;
|
||||
base.lock = lock;
|
||||
|
||||
// ---- search ----
|
||||
if (q.has('search')) {
|
||||
|
|
@ -58,20 +118,110 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) {
|
|||
}
|
||||
const b = await sql(`SELECT hash FROM ${T}live_blocks WHERE hash = $1`, [c.value]);
|
||||
if (b.length) return res.status(200).json({ ...base, type: 'block', href: `/block/${c.value}` });
|
||||
const t = await sql(`SELECT hash FROM ${T}live_blocks WHERE tx_count > 0 AND detail->'txs' @> $1::jsonb LIMIT 1`, [JSON.stringify([{ hash: '0x' + c.value }])]);
|
||||
if (t.length) return res.status(200).json({ ...base, type: 'tx', href: `/block/${t[0].hash}#tx-${c.value}` });
|
||||
return res.status(404).json({ ...base, ok: false, error: 'No block or transaction with that hash in the last 24 hours.' });
|
||||
const t = await sql(`SELECT hash FROM ${T}live_txs WHERE hash = $1`, ['0x' + c.value]).catch(() => []);
|
||||
if (t.length) return res.status(200).json({ ...base, type: 'tx', href: `/tx/0x${c.value}` });
|
||||
return res.status(404).json({ ...base, ok: false, error: 'No block in the last 24 hours and no transaction in the last 7 days with that hash.' });
|
||||
}
|
||||
|
||||
// ---- stats ----
|
||||
if (q.has('stats')) {
|
||||
const [chain10, fin] = await Promise.all([
|
||||
sql(`SELECT count(*)::int AS blocks_10m, count(*) FILTER (WHERE is_chain_block)::int AS chain_10m, count(DISTINCT vote_key_hash)::int AS miners_10m, max(daa_score) AS daa, max(number) AS height FROM ${T}live_blocks WHERE received_at > now() - interval '10 minutes'`),
|
||||
Promise.resolve(s && s.finality ? s.finality : null),
|
||||
]);
|
||||
const c = chain10[0] || {};
|
||||
const w = fin && fin.weights ? fin.weights : null;
|
||||
const activations = params ? Object.entries(params).filter(([k, v]) => /_activation_daa$/.test(k) && Number.isFinite(Number(v))).map(([k, v]) => ({ name: k.replace(/_activation_daa$/, ''), daa: Number(v) })) : [];
|
||||
return res.status(200).json({
|
||||
...base,
|
||||
node_version: s ? s.node_version : null, height: num(c.height), daa: num(c.daa), blue_score: s ? num(s.blue_score) : null, block_count: s ? num(s.block_count) : null,
|
||||
difficulty: s ? num(s.difficulty) : null, hashrate: s ? num(s.hashes_per_second_estimate) : null, peers: s ? num(s.peers) : null, mempool: s ? num(s.mempool) : null,
|
||||
blocks_10m: num(c.blocks_10m), chain_blocks_10m: num(c.chain_10m), blocks_per_s_10m: c.blocks_10m === null ? null : Math.round(Number(c.blocks_10m) / 600 * 100) / 100, chain_blocks_per_s_10m: c.chain_10m === null ? null : Math.round(Number(c.chain_10m) / 600 * 100) / 100,
|
||||
miners_10m: num(c.miners_10m),
|
||||
finality: fin ? { active: !!fin.finality_active, voters: w ? num(w.voters) : null, total_weight: w ? num(w.total_weight) : null, active_weight: w ? num(w.active_weight) : null, keys: w && Array.isArray(w.keys) ? w.keys.length : null, latest_locked_index: num(fin.latest_locked_index), latest_locked_blue_score: num(fin.latest_locked_blue_score), params: fin.params || null } : null,
|
||||
proving: s && s.proving ? s.proving : null,
|
||||
class: classAt(num(c.daa), params), activations, digest: ix && ix.node_info ? ix.node_info.digest : null, genesis: ix && ix.node_info ? ix.node_info.genesis : null,
|
||||
budgets: null,
|
||||
});
|
||||
}
|
||||
|
||||
// ---- proving view ----
|
||||
if (q.has('proving')) {
|
||||
const [blocks, provers24, provers1, lat, totals] = await Promise.all([
|
||||
sql(`SELECT b.hash, b.number, b.daa_score, b.blue_score, b.timestamp_ms, b.vote_key_hash, b.evm_miner, b.tx_count, b.proof_records,
|
||||
count(p.shard)::int AS total, count(*) FILTER (WHERE p.state = 'planned')::int AS planned, count(*) FILTER (WHERE p.state = 'proving')::int AS proving,
|
||||
count(*) FILTER (WHERE p.state = 'verified')::int AS verified, count(*) FILTER (WHERE p.state = 'paid')::int AS paid,
|
||||
percentile_cont(0.5) WITHIN GROUP (ORDER BY p.lag_daa) FILTER (WHERE p.state = 'paid') AS median_lag, sum(p.pgas) AS pgas
|
||||
FROM ${T}live_blocks b LEFT JOIN ${T}live_proofs p ON p.block_hash = b.hash
|
||||
WHERE b.is_chain_block AND b.number IS NOT NULL AND b.received_at > now() - interval '24 hours'
|
||||
GROUP BY b.hash, b.number, b.daa_score, b.blue_score, b.timestamp_ms, b.vote_key_hash, b.evm_miner, b.tx_count, b.proof_records ORDER BY b.number DESC LIMIT 60`),
|
||||
sql(`SELECT prover, count(*)::int AS shards, sum(payout_wei::numeric)::text AS payout_wei, percentile_cont(0.5) WITHIN GROUP (ORDER BY lag_daa) AS median_lag, max(updated_at) AS last_paid
|
||||
FROM ${T}live_proofs WHERE state = 'paid' AND updated_at > now() - interval '24 hours' AND prover IS NOT NULL GROUP BY prover ORDER BY shards DESC LIMIT 64`),
|
||||
sql(`SELECT prover, count(*)::int AS shards, sum(payout_wei::numeric)::text AS payout_wei FROM ${T}live_proofs WHERE state = 'paid' AND updated_at > now() - interval '1 hour' AND prover IS NOT NULL GROUP BY prover`),
|
||||
sql(`SELECT count(*)::int AS paid_1h, percentile_cont(0.5) WITHIN GROUP (ORDER BY lag_daa) AS p50, percentile_cont(0.9) WITHIN GROUP (ORDER BY lag_daa) AS p90, max(lag_daa) AS max_lag, min(lag_daa) AS min_lag
|
||||
FROM ${T}live_proofs WHERE state = 'paid' AND updated_at > now() - interval '1 hour'`),
|
||||
sql(`SELECT state, count(*)::int AS n, sum(payout_wei::numeric)::text AS payout_wei FROM ${T}live_proofs WHERE received_at > now() - interval '24 hours' GROUP BY state`),
|
||||
]);
|
||||
const hour = new Map(provers1.map(p => [p.prover, p]));
|
||||
return res.status(200).json({
|
||||
...base, proving: s && s.proving ? s.proving : null,
|
||||
blocks: blocks.map(b => ({ hash: b.hash, number: num(b.number), daa: num(b.daa_score), blue_score: num(b.blue_score), ts: num(b.timestamp_ms), miner_id: b.vote_key_hash ? String(b.vote_key_hash).slice(0, 8) : null, evm_miner: b.evm_miner, tx_count: num(b.tx_count), proof_records: num(b.proof_records), pgas: num(b.pgas), shards: shardSummary(b), class: classAt(num(b.daa_score), params).class })),
|
||||
provers: provers24.map(p => ({ id: p.prover, shards_24h: num(p.shards), payout_wei_24h: p.payout_wei || '0', median_lag_24h: p.median_lag === null ? null : Number(p.median_lag), shards_1h: hour.has(p.prover) ? num(hour.get(p.prover).shards) : 0, payout_wei_1h: hour.has(p.prover) ? hour.get(p.prover).payout_wei || '0' : '0', last_paid: p.last_paid })),
|
||||
latency_1h: lat[0] ? { paid: num(lat[0].paid_1h), p50_daa: lat[0].p50 === null ? null : Number(lat[0].p50), p90_daa: lat[0].p90 === null ? null : Number(lat[0].p90), min_daa: num(lat[0].min_lag), max_daa: num(lat[0].max_lag), note: 'lag in DAA score from the proven block to the block whose coinbase paid the shard; Devnet 3 targets one DAA step per second' } : null,
|
||||
totals_24h: Object.fromEntries(totals.map(t => [t.state, { shards: num(t.n), payout_wei: t.payout_wei || '0' }])),
|
||||
});
|
||||
}
|
||||
|
||||
// ---- the DAG ----
|
||||
if (q.has('dag')) {
|
||||
const n = Math.max(20, Math.min(400, Number(q.get('dag')) || 120));
|
||||
const rows = await sql(`SELECT ${ROW}, ${evmHash(T)}, (SELECT json_build_object('total', count(*), 'paid', count(*) FILTER (WHERE state = 'paid'), 'planned', count(*) FILTER (WHERE state = 'planned'), 'proving', count(*) FILTER (WHERE state = 'proving'), 'verified', count(*) FILTER (WHERE state = 'verified')) FROM ${T}live_proofs p WHERE p.block_hash = b.hash) AS sh
|
||||
FROM ${T}live_blocks b ORDER BY received_at DESC LIMIT $1`, [n]).catch(() => sql(`SELECT ${ROW} FROM ${T}live_blocks b ORDER BY received_at DESC LIMIT $1`, [n]));
|
||||
const cps = await sql(`SELECT hash, index, state FROM ${T}live_checkpoints ORDER BY index DESC LIMIT 400`).catch(() => []);
|
||||
const cp = new Map(cps.map(c => [c.hash, { index: num(c.index), state: c.state }]));
|
||||
return res.status(200).json({ ...base, blocks: rows.map(b => { const r = row(b); return { ...r, shards: b.sh ? shardSummary(b.sh) : null, checkpoint: cp.get(b.hash) || null, final: lockState(r, lock, null).final, class: classAt(r.daa, params).class }; }).reverse() });
|
||||
}
|
||||
|
||||
// ---- latest transactions ----
|
||||
if (q.has('txs')) {
|
||||
const n = Math.max(10, Math.min(200, Number(q.get('txs')) || 50));
|
||||
const before = Number(q.get('before')) || 0;
|
||||
const rows = before
|
||||
? await sql(`SELECT ${TXROW} FROM ${T}live_txs WHERE block_number < $2 ORDER BY block_number DESC, tx_index ASC LIMIT $1`, [n, before])
|
||||
: await sql(`SELECT ${TXROW} FROM ${T}live_txs ORDER BY block_number DESC, tx_index ASC LIMIT $1`, [n]);
|
||||
return res.status(200).json({ ...base, txs: rows.map(txrow) });
|
||||
}
|
||||
|
||||
// ---- one transaction ----
|
||||
if (q.has('tx')) {
|
||||
const h = String(q.get('tx') || '').toLowerCase().replace(/^(0x)?/, '0x');
|
||||
if (!/^0x[0-9a-f]{64}$/.test(h)) return res.status(400).json({ ...base, ok: false, error: 'A transaction hash is 0x plus 64 hex characters.' });
|
||||
const rows = await sql(`SELECT ${TXROW}, logs, igneum FROM ${T}live_txs WHERE hash = $1`, [h]);
|
||||
if (!rows.length) return res.status(404).json({ ...base, ok: false, error: 'No transaction with that hash in the last 7 days. The explorer keeps seven days of transactions; older ones live in the node.' });
|
||||
const t = rows[0];
|
||||
const [blk, sh, acct] = await Promise.all([
|
||||
sql(`SELECT ${ROW}, ${evmHash(T)} FROM ${T}live_blocks b WHERE hash = $1`, [t.block_hash]),
|
||||
sql(`SELECT count(*)::int AS total, count(*) FILTER (WHERE state = 'planned')::int AS planned, count(*) FILTER (WHERE state = 'proving')::int AS proving, count(*) FILTER (WHERE state = 'verified')::int AS verified, count(*) FILTER (WHERE state = 'paid')::int AS paid, percentile_cont(0.5) WITHIN GROUP (ORDER BY lag_daa) FILTER (WHERE state = 'paid') AS median_lag FROM ${T}live_proofs WHERE block_hash = $1`, [t.block_hash]).catch(() => []),
|
||||
sql(`SELECT address, is_contract, code_bytes FROM ${T}live_accounts WHERE address = ANY($1::text[])`, [[t.from_addr, t.to_addr, t.contract_address].filter(Boolean)]).catch(() => []),
|
||||
]);
|
||||
const b = blk[0] ? row(blk[0]) : null;
|
||||
const contracts = Object.fromEntries(acct.filter(a => a.is_contract).map(a => [a.address, num(a.code_bytes)]));
|
||||
return res.status(200).json({
|
||||
...base, tx: { ...txrow(t), logs: t.logs || [], igneum: t.igneum || null, contracts },
|
||||
block: b ? { ...b, class: classAt(b.daa, params).class, shards: sh[0] ? shardSummary(sh[0]) : null, lock: lockState(b, lock, null), in_observer: true } : { hash: t.block_hash, number: num(t.block_number), in_observer: false, note: 'the observer keeps one day of blocks; the transaction is older or its block left the observer window' },
|
||||
});
|
||||
}
|
||||
|
||||
// ---- latest blocks ----
|
||||
if (q.has('blocks') || (!q.has('block') && !q.has('height') && !q.has('address'))) {
|
||||
const n = Math.max(10, Math.min(200, Number(q.get('blocks')) || 50));
|
||||
const before = Number(q.get('before')) || 0;
|
||||
const sel = `SELECT ${ROW}, ${evmHash(T)}, (SELECT json_build_object('total', count(*), 'paid', count(*) FILTER (WHERE state = 'paid'), 'planned', count(*) FILTER (WHERE state = 'planned'), 'proving', count(*) FILTER (WHERE state = 'proving'), 'verified', count(*) FILTER (WHERE state = 'verified')) FROM ${T}live_proofs p WHERE p.block_hash = b.hash) AS sh FROM ${T}live_blocks b`;
|
||||
const rows = before
|
||||
? await sql(`SELECT ${ROW} FROM ${T}live_blocks WHERE received_at < to_timestamp($2 / 1000.0) ORDER BY received_at DESC LIMIT $1`, [n, before])
|
||||
: await sql(`SELECT ${ROW} FROM ${T}live_blocks ORDER BY received_at DESC LIMIT $1`, [n]);
|
||||
? await sql(`${sel} WHERE received_at < to_timestamp($2 / 1000.0) ORDER BY received_at DESC LIMIT $1`, [n, before])
|
||||
: await sql(`${sel} ORDER BY received_at DESC LIMIT $1`, [n]);
|
||||
const locked = new Set((await sql(`SELECT hash FROM ${T}live_checkpoints WHERE state = 'locked' ORDER BY index DESC LIMIT 200`).catch(() => [])).map(c => c.hash));
|
||||
return res.status(200).json({ ...base, blocks: rows.map(b => ({ ...row(b), locked: locked.has(b.hash) })) });
|
||||
return res.status(200).json({ ...base, blocks: rows.map(b => { const r = row(b); return { ...r, locked: locked.has(b.hash), shards: b.sh ? shardSummary(b.sh) : null, final: lockState(r, lock, null).final, class: classAt(r.daa, params).class }; }) });
|
||||
}
|
||||
|
||||
// ---- one block ----
|
||||
|
|
@ -83,30 +233,34 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) {
|
|||
hash = r[0].hash;
|
||||
}
|
||||
if (!/^[0-9a-f]{64}$/.test(hash)) return res.status(400).json({ ...base, ok: false, error: 'A block hash is 64 hex characters.' });
|
||||
const rows = await sql(`SELECT ${ROW}, detail FROM ${T}live_blocks WHERE hash = $1`, [hash]);
|
||||
if (!rows.length) return res.status(404).json({ ...base, ok: false, error: 'No block with that hash in the last 24 hours. The explorer keeps one day; older blocks live in the node.' });
|
||||
const rows = await sql(`SELECT ${ROW}, ${evmHash(T)}, detail FROM ${T}live_blocks b WHERE hash = $1`, [hash]).catch(() => sql(`SELECT ${ROW}, detail FROM ${T}live_blocks WHERE hash = $1`, [hash]));
|
||||
if (!rows.length) return res.status(404).json({ ...base, ok: false, error: 'No block with that hash in the last 24 hours. The explorer keeps one day of blocks; older blocks live in the node.' });
|
||||
const b = rows[0];
|
||||
const [shards, cps, certs, children, mergedBy] = await Promise.all([
|
||||
sql(`SELECT shard, shards, state, prover, lag_daa, payout_wei, pgas, carried_by, carrier_number FROM ${T}live_proofs WHERE block_hash = $1 ORDER BY shard`, [hash]).catch(() => []),
|
||||
const [shards, cps, certs, children, mergedBy, txs] = await Promise.all([
|
||||
sql(`SELECT shard, shards, state, prover, lag_daa, payout_wei, pgas, carried_by, carrier_number, verified FROM ${T}live_proofs WHERE block_hash = $1 ORDER BY shard`, [hash]).catch(() => []),
|
||||
sql(`SELECT index, state, signed_weight, active_weight, total_weight, fraction_active, fraction_total, votes_seen, voters, locked_at FROM ${T}live_checkpoints WHERE hash = $1`, [hash]).catch(() => []),
|
||||
sql(`SELECT index, hash, carrier, voter_count, aggregator, total_weight, active_weight, prev_index, prev_hash, headers_complete, created_at FROM ${T}live_certificates WHERE hash = $1 OR carrier = $1`, [hash]).catch(() => []),
|
||||
sql(`SELECT hash FROM ${T}live_blocks WHERE received_at >= $2::timestamptz - interval '10 seconds' AND received_at < $2::timestamptz + interval '10 minutes' AND $1 = ANY(parent_hashes) ORDER BY received_at LIMIT 20`, [hash, b.received_at]),
|
||||
b.is_chain_block ? Promise.resolve([]) : sql(`SELECT hash FROM ${T}live_blocks WHERE is_chain_block AND received_at >= $2::timestamptz - interval '10 seconds' AND received_at < $2::timestamptz + interval '10 minutes' AND (detail->'mergeset'->'blues' ? $1 OR detail->'mergeset'->'reds' ? $1) LIMIT 1`, [hash, b.received_at]),
|
||||
b.is_chain_block ? Promise.resolve([]) : sql(`SELECT hash, blue_score FROM ${T}live_blocks WHERE is_chain_block AND received_at >= $2::timestamptz - interval '10 seconds' AND received_at < $2::timestamptz + interval '10 minutes' AND (detail->'mergeset'->'blues' ? $1 OR detail->'mergeset'->'reds' ? $1) LIMIT 1`, [hash, b.received_at]),
|
||||
sql(`SELECT ${TXROW} FROM ${T}live_txs WHERE block_hash = $1 ORDER BY tx_index LIMIT 500`, [hash]).catch(() => []),
|
||||
]);
|
||||
let evmView = null; // the EVM's view of a chain block, when the deployment has an EVM RPC
|
||||
if (EVM && b.is_chain_block) {
|
||||
try {
|
||||
const e = await evm(EVM, 'eth_getBlockByHash', ['0x' + hash, true]);
|
||||
const e = await evm(EVM, 'eth_getBlockByHash', ['0x' + hash, false]);
|
||||
if (e) evmView = { number: parseInt(e.number, 16), gas_used: parseInt(e.gasUsed, 16), base_fee_per_gas: e.baseFeePerGas, state_root: e.stateRoot, miner: e.miner,
|
||||
transactions: (e.transactions || []).map(t => ({ hash: t.hash, from: t.from, to: t.to, value: t.value, nonce: parseInt(t.nonce, 16), gas: parseInt(t.gas, 16) })),
|
||||
igneum: e.igneum ? { rewards: e.igneum.rewards, proving_pool_credit: e.igneum.provingPoolCredit, pgas_used: e.igneum.pgasUsed, skipped: e.igneum.skipped } : null };
|
||||
} catch (err) { evmView = { error: String(err.message || err).slice(0, 120) }; }
|
||||
}
|
||||
const r = row(b);
|
||||
const mergerBlue = b.is_chain_block ? null : (mergedBy[0] ? num(mergedBy[0].blue_score) : null);
|
||||
const counts = { total: shards.length, planned: shards.filter(x => x.state === 'planned').length, proving: shards.filter(x => x.state === 'proving').length, verified: shards.filter(x => x.state === 'verified').length, paid: shards.filter(x => x.state === 'paid').length };
|
||||
return res.status(200).json({
|
||||
...base, block: { ...row(b), detail: b.detail || null }, shards: shards.map(x => ({ i: num(x.shard), n: num(x.shards), state: x.state, prover: x.prover, lag: num(x.lag_daa), payout_wei: x.payout_wei, pgas: num(x.pgas), carried_by: x.carried_by, carrier_number: num(x.carrier_number) })),
|
||||
...base, block: { ...r, detail: b.detail || null, class: classAt(r.daa, params), lock: lockState(r, lock, mergerBlue), shard_summary: counts },
|
||||
shards: shards.map(x => ({ i: num(x.shard), n: num(x.shards), state: x.state, prover: x.prover, lag: num(x.lag_daa), payout_wei: x.payout_wei, pgas: num(x.pgas), carried_by: x.carried_by, carrier_number: num(x.carrier_number), node_verified: x.verified === null || x.verified === undefined ? null : !!x.verified })),
|
||||
checkpoint: cps[0] ? { index: num(cps[0].index), state: cps[0].state, signed: num(cps[0].signed_weight), active: num(cps[0].active_weight), total: num(cps[0].total_weight), fraction_active: num(cps[0].fraction_active), fraction_total: num(cps[0].fraction_total), votes: num(cps[0].votes_seen), voters: num(cps[0].voters), locked_at: cps[0].locked_at } : null,
|
||||
certificates: certs.map(c => ({ index: num(c.index), checkpoint: c.hash, carrier: c.carrier, role: c.hash === hash ? 'checkpoint' : 'carrier', voter_count: num(c.voter_count), aggregator: c.aggregator, total_weight: num(c.total_weight), active_weight: num(c.active_weight), previous: c.prev_index === null ? null : { index: num(c.prev_index), hash: c.prev_hash }, headers_complete: !!c.headers_complete })),
|
||||
children: children.map(c => c.hash), merged_by: b.is_chain_block ? hash : (mergedBy[0] ? mergedBy[0].hash : null), evm: evmView,
|
||||
children: children.map(c => c.hash), merged_by: b.is_chain_block ? hash : (mergedBy[0] ? mergedBy[0].hash : null), evm: evmView, txs: txs.map(txrow),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -122,30 +276,40 @@ export function createHandler({ env = process.env, sql, evm = evmCall } = {}) {
|
|||
coalesce(sum(subsidy_sompi) FILTER (WHERE color = 'blue'), 0)::text AS blue_subsidy_sompi,
|
||||
max(evm_miner) AS evm_miner, max(miner_address) AS miner_address
|
||||
FROM ${T}live_blocks WHERE ${col} = $1`, [c.value]),
|
||||
sql(`SELECT ${ROW} FROM ${T}live_blocks WHERE ${col} = $1 ORDER BY received_at DESC LIMIT 50`, [c.value]),
|
||||
sql(`SELECT ${ROW}, ${evmHash(T)} FROM ${T}live_blocks b WHERE ${col} = $1 ORDER BY received_at DESC LIMIT 50`, [c.value]),
|
||||
sql(`SELECT vote_key_hash, count(*)::int AS n FROM ${T}live_blocks WHERE ${col} = $1 AND vote_key_hash IS NOT NULL GROUP BY 1 ORDER BY n DESC LIMIT 32`, [c.value]),
|
||||
]);
|
||||
const t = sum[0];
|
||||
const evmAddress = c.kind === 'evm' ? c.value : (t.evm_miner || null);
|
||||
let balance = { available: false, reason: EVM ? 'no EVM address known for this coinbase address' : 'no EVM RPC configured on this deployment (EXPLORER_EVM_RPC)' };
|
||||
let account = null, txs = [];
|
||||
if (evmAddress) {
|
||||
const before = Number(q.get('before')) || 0;
|
||||
const [a, tx] = await Promise.all([
|
||||
sql(`SELECT address, balance_wei, nonce, code_bytes, is_contract, sent, received, first_seen_number, last_seen_number, miner, refreshed_at FROM ${T}live_accounts WHERE address = $1`, [evmAddress]).catch(() => []),
|
||||
sql(`SELECT ${TXROW} FROM ${T}live_txs WHERE (from_addr = $1 OR to_addr = $1) ${before ? 'AND block_number < $2' : ''} ORDER BY block_number DESC, tx_index ASC LIMIT 50`, before ? [evmAddress, before] : [evmAddress]).catch(() => []),
|
||||
]);
|
||||
if (a[0]) account = { address: a[0].address, balance_wei: a[0].balance_wei === null ? null : String(a[0].balance_wei), nonce: num(a[0].nonce), code_bytes: num(a[0].code_bytes), is_contract: !!a[0].is_contract, sent: num(a[0].sent), received: num(a[0].received), first_seen_number: num(a[0].first_seen_number), last_seen_number: num(a[0].last_seen_number), miner: !!a[0].miner, refreshed_at: a[0].refreshed_at, source: 'the explorer indexer (eth_getBalance, eth_getTransactionCount, eth_getCode on the observer node)' };
|
||||
txs = tx.map(txrow);
|
||||
}
|
||||
let balance = account && account.balance_wei !== null ? { available: true, wei: account.balance_wei, address: evmAddress, source: 'indexer', refreshed_at: account.refreshed_at } : { available: false, reason: evmAddress ? 'not read yet: the indexer reads an address after its first transaction or mined block' : 'no EVM address known for this coinbase address' };
|
||||
if (EVM && evmAddress) {
|
||||
try { const wei = await evm(EVM, 'eth_getBalance', [evmAddress, 'latest']); balance = { available: true, wei: BigInt(wei).toString(), address: evmAddress, source: 'eth_getBalance latest' }; }
|
||||
catch (err) { balance = { available: false, reason: String(err.message || err).slice(0, 120) }; }
|
||||
catch (err) { if (!balance.available) balance = { available: false, reason: String(err.message || err).slice(0, 120) }; }
|
||||
}
|
||||
// what the blocks earned: each blue block pays 80% of the subsidy its payload declares to this miner, through the coinbase of the block that merges it
|
||||
const blue = BigInt(t.blue_subsidy_sompi || 0); const earned = blue * 80n / 100n;
|
||||
return res.status(200).json({
|
||||
...base, address: c.value, kind: c.kind, evm_address: evmAddress, utxo_address: c.kind === 'utxo' ? c.value : (t.miner_address || null),
|
||||
account, txs,
|
||||
blocks_24h: t.blocks_24h, blocks_10m: t.blocks_10m, blue_24h: t.blue_24h, red_24h: t.red_24h, chain_24h: t.chain_24h,
|
||||
first_seen: t.first_ts ? new Date(num(t.first_ts)).toISOString() : null, last_seen: t.last_ts ? new Date(num(t.last_ts)).toISOString() : null,
|
||||
earned_24h_sompi: earned.toString(), earned_note: '80% of the declared subsidy of every blue block this address mined in the last 24 hours, paid by the coinbase of the block that merged each one (coinbase.rs); red blocks pay the merging miner instead',
|
||||
miner_ids: ids.map(i => ({ id: String(i.vote_key_hash).slice(0, 8), blocks: i.n })), balance, recent: recent.map(row),
|
||||
});
|
||||
}
|
||||
return res.status(400).json({ ...base, ok: false, error: 'blocks, block, height, address or search' });
|
||||
return res.status(400).json({ ...base, ok: false, error: 'blocks, block, height, tx, txs, address, proving, stats, dag or search' });
|
||||
} catch (e) {
|
||||
res.setHeader('Cache-Control', 'no-store');
|
||||
return res.status(500).json({ ok: false, error: String(e.message || e) });
|
||||
return res.status(500).json({ ok: false, network_label: NETWORK_LABEL, error: String(e.message || e) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Igneum block</title>
|
||||
<title>Igneum Devnet 3 block</title>
|
||||
<meta name="description" content="One Igneum block: header, parents, mergeset, transactions, proof records and, when it is a checkpoint, its finality certificate.">
|
||||
<link rel="canonical" href="https://igneum.network/block">
|
||||
<meta name="robots" content="noindex">
|
||||
|
|
@ -92,6 +92,12 @@ dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
|
|||
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}.two .card{margin-top:0}}
|
||||
.two-wrap{margin-top:var(--gap)}
|
||||
main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--line);border-radius:16px;padding:25px;margin-top:24px}.card .viz-head h2{font-size:19px}.card .viz-head .eyebrow{margin:0}
|
||||
.badge{display:inline-flex;align-items:center;gap:6px;font-family:var(--f-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;padding:4px 9px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2);margin:0 6px 6px 0}
|
||||
.badge.ok{border-color:var(--green,#5CC48A);color:var(--green,#5CC48A)}.badge.hot{border-color:var(--molten);color:var(--molten)}.badge.bad{border-color:var(--excluded);color:var(--excluded)}.badge.wait{color:var(--ash)}
|
||||
.bar{display:inline-block;width:72px;height:7px;border-radius:4px;background:var(--line);vertical-align:middle;margin-right:7px;overflow:hidden}.bar i{display:block;height:100%;background:var(--green,#5CC48A)}.bar i.part{background:var(--molten)}
|
||||
.dagbox{position:relative;width:100%;height:320px;border:1px solid var(--line);border-radius:12px;background:var(--obsidian);overflow:hidden}.dagbox canvas{width:100%;height:100%;display:block;cursor:pointer}
|
||||
.dagtip{position:absolute;pointer-events:none;font:11px/1.5 var(--mono);color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:8px;padding:8px 10px;max-width:300px;display:none}
|
||||
.kv{color:var(--ash)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -131,8 +137,8 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
</div>
|
||||
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/live" data-nav="live" aria-current="page"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
|
|
@ -170,8 +176,8 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Network</div>
|
||||
<a href="/live" data-nav="live" aria-current="page"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
|
|
@ -246,7 +252,7 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<main id="main" class="wrap">
|
||||
<div class="head">
|
||||
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><a href="/explorer">Explorer</a><span>/</span><span>Block</span></div>
|
||||
<div class="eyebrow"><a href="/explorer">Explorer</a> · block</div>
|
||||
<div class="eyebrow"><a href="/explorer">Devnet 3 explorer</a> · block</div>
|
||||
<h1 class="hash" id="title">Block</h1>
|
||||
<div id="chips"></div>
|
||||
<div class="msg" id="msg" aria-live="polite"></div>
|
||||
|
|
@ -261,8 +267,8 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<div class="card"><div class="viz-head"><h2>Mergeset</h2><div class="eyebrow">what this block merges</div></div><div id="mergeset"></div><p class="note">A chain block merges the blocks in its past that no earlier chain block merged. Blue ones are paid by this block's coinbase; red ones are excluded from the order and their reward goes to this block's miner.</p></div>
|
||||
</div>
|
||||
<div class="card"><div class="viz-head"><h2>Transactions</h2><div class="eyebrow" id="tx-n"></div></div><div id="coinbase"></div><div class="tbl" id="txs"></div></div>
|
||||
<div class="card"><div class="viz-head"><h2>Proof records and shards</h2><div class="eyebrow" id="proof-eyebrow"></div></div><div id="proofs"></div></div>
|
||||
<div class="card"><div class="viz-head"><h2>Finality</h2><div class="eyebrow" id="fin-eyebrow"></div></div><div id="finality"></div></div>
|
||||
<div class="card"><div class="viz-head"><h2>Shard plan and proofs</h2><div class="eyebrow" id="proof-eyebrow"></div></div><div id="proofs"></div></div>
|
||||
<div class="card"><div class="viz-head"><h2>Finality</h2><div class="eyebrow" id="fin-eyebrow"></div></div><div id="finality"></div><div id="recheck"></div></div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- footer:start -->
|
||||
|
|
@ -344,28 +350,47 @@ const addr = a => a ? `<a class="hash" href="/address/${esc(a)}">${esc(a)}</a>`
|
|||
const dl = pairs => pairs.filter(p => p).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join('');
|
||||
const id = decodeURIComponent(location.pathname.split('/').pop() || '');
|
||||
const query = /^\d{1,12}$/.test(id) ? 'height=' + id : 'block=' + encodeURIComponent(id.replace(/^0x/i, ''));
|
||||
async function recheck(index, blockHash, isCheckpoint) {
|
||||
// the verified badge: this browser fetches the certificate that covers the block and checks it with the site's light client
|
||||
const el = $('recheck'); el.innerHTML = `<p class="note" id="rc-line">Re-checking certificate ${int(index)} in this browser.</p>`;
|
||||
const badge = document.createElement('span'); badge.className = 'badge wait'; badge.textContent = 'checking the certificate'; $('chips').appendChild(badge);
|
||||
try {
|
||||
const { fetchCheckpoint, verify } = await import('/verify/verify.js');
|
||||
const data = await fetchCheckpoint('/api/checkpoint?index=' + index);
|
||||
const r = verify(data);
|
||||
if (r.verified) {
|
||||
badge.className = 'badge ok'; badge.textContent = isCheckpoint ? 'verified in this browser' : `verified under lock ${int(index)} in this browser`;
|
||||
$('rc-line').innerHTML = `Certificate ${int(index)} re-checked in this browser in ${r.ms} ms: ${int(r.headers_checked)} header hashes recomputed, ${int(r.signers)} of ${int(r.voters)} voters' BLS keys aggregated, the signature verifies over ${(r.weight_fraction_total * 100).toFixed(1)}% of all weight. ${isCheckpoint ? 'This block is the certified checkpoint.' : 'This block sits under that lock on the selected chain.'} The light client is <a href="/verify/core.js">/verify/core.js</a>; the data it checked is <a href="/api/checkpoint?index=${index}">/api/checkpoint?index=${index}</a>.`;
|
||||
} else { badge.className = 'badge bad'; badge.textContent = 'certificate did not verify'; $('rc-line').textContent = `Certificate ${int(index)} did not verify in this browser: ${r.error || 'unknown reason'}.`; }
|
||||
} catch (e) { badge.className = 'badge wait'; badge.textContent = 're-check unavailable'; $('rc-line').textContent = 'The re-check could not run: ' + (e.message || e); }
|
||||
}
|
||||
async function load() {
|
||||
const r = await fetch('/api/explorer?' + query); const j = await r.json();
|
||||
if (!j.ok) { $('msg').textContent = j.error || 'Not found.'; $('title').textContent = /^\d/.test(id) ? 'Chain block ' + id : shortHash(id, 10, 8); return; }
|
||||
const b = j.block, d = b.detail || {}, now = new Date(j.now).getTime();
|
||||
document.title = `Igneum block ${shortHash(b.hash, 8, 6)}`;
|
||||
const b = j.block, d = b.detail || {}, now = new Date(j.now).getTime(), sum = b.shard_summary || { total: 0, paid: 0 };
|
||||
document.title = `Devnet 3 block ${shortHash(b.hash, 8, 6)}`;
|
||||
$('title').textContent = b.hash;
|
||||
const chips = [b.chain ? '<span class="chip hot">chain block</span>' : `<span class="chip">${esc(b.color)}</span>`];
|
||||
const chips = [b.chain ? (b.chain_now === false ? '<span class="chip" title="the observer saw it as the chain block of its number; a tip reorg gave the number to another block">chain block, replaced</span>' : '<span class="chip hot">chain block</span>') : `<span class="chip">${esc(b.color)}</span>`, `<span class="chip" title="the hash program family this block was mined under">class ${esc(b.class ? b.class.class : 'v4')}</span>`];
|
||||
if (j.checkpoint) chips.push(`<span class="chip ${j.checkpoint.state === 'locked' ? 'ok' : ''}">checkpoint ${j.checkpoint.index}, ${esc(j.checkpoint.state)}</span>`);
|
||||
if (b.proof_records) chips.push(`<span class="chip">${b.proof_records} proof record${b.proof_records === 1 ? '' : 's'}</span>`);
|
||||
if (b.tx_count) chips.push(`<span class="chip">${b.tx_count} transaction${b.tx_count === 1 ? '' : 's'}</span>`);
|
||||
chips.push(b.lock && b.lock.final ? `<span class="chip ok" title="${esc(b.lock.reason)}">final under lock ${int(b.lock.index)}</span>` : `<span class="chip" title="${esc(b.lock ? b.lock.reason : '')}">not under a lock yet</span>`);
|
||||
if (b.chain) chips.push(sum.total ? `<span class="chip ${sum.paid === sum.total ? 'ok' : ''}">${sum.paid} of ${sum.total} shards paid</span>` : '<span class="chip">plan not read yet</span>');
|
||||
if (b.proof_records) chips.push(`<span class="chip">carries ${b.proof_records} proof record${b.proof_records === 1 ? '' : 's'}</span>`);
|
||||
if (j.txs.length) chips.push(`<span class="chip">${j.txs.length} transaction${j.txs.length === 1 ? '' : 's'} executed</span>`);
|
||||
$('chips').innerHTML = chips.join('');
|
||||
$('summary').innerHTML = dl([
|
||||
['Number', b.number === null ? '<span style="color:var(--ash)">none, not a chain block</span>' : int(b.number)],
|
||||
['Time', `${utc(b.ts)} <span style="color:var(--ash)">(${rel(now - b.ts)})</span>`],
|
||||
['Network', 'Devnet 3, chain id ' + esc(j.chain_id ?? 4463)],
|
||||
['Number', b.number === null ? '<span class="kv">none, not a chain block</span>' : (b.chain_now === false ? `${int(b.number)} <span class="kv">as the observer first saw it; a tip reorg gave this number to another block, so the EVM executed that one</span>` : int(b.number))],
|
||||
['Time', `${utc(b.ts)} <span class="kv">(${rel(now - b.ts)})</span>`],
|
||||
['DAA score', int(b.daa)], ['Blue score', int(b.blue_score)],
|
||||
['Class', `${esc(b.class ? b.class.class : 'v4')} <span class="kv">${b.class && b.class.floors ? b.class.floors.map(f => `${f.class} from DAA ${int(f.daa)}`).join(', ') : ''}</span>`],
|
||||
['Lock state', b.lock ? `${b.lock.final ? 'final' : 'not final'} <span class="kv">${esc(b.lock.reason)}${j.lock ? `; newest lock ${int(j.lock.index)} at blue score ${int(j.lock.blue_score)}, ${(j.lock.fraction_total * 100).toFixed(1)}% of all weight` : ''}</span>` : 'n/a'],
|
||||
['Miner', addr(b.evm_miner)], ['Coinbase address', addr(b.miner)],
|
||||
['Vote key', `<span class="id">${esc(b.miner_id || 'n/a')}</span> <span class="hash" style="color:var(--ash);font-size:12px">${esc(b.vote_key_hash || '')}</span>`],
|
||||
['Vote key', `<span class="id">${esc(b.miner_id || 'n/a')}</span> <span class="hash kv" style="font-size:12px">${esc(b.vote_key_hash || '')}</span>`],
|
||||
b.engine ? ['Engine', esc(b.engine)] : null,
|
||||
['Difficulty', d.difficulty ? int(d.difficulty) : 'n/a'],
|
||||
['Declared subsidy', b.subsidy_sompi ? `${ign(b.subsidy_sompi, 8)} IGN <span style="color:var(--ash)">E(DAA) of spec 2.5, paid when a later chain block merges this one</span>` : 'n/a'],
|
||||
['Coinbase paid', b.paid_sompi ? `${ign(b.paid_sompi, 8)} IGN <span style="color:var(--ash)">to the blocks it merges</span>` : 'n/a'],
|
||||
['Merged by', b.chain ? 'itself (chain block)' : (j.merged_by ? link(j.merged_by) : '<span style="color:var(--ash)">not yet</span>')],
|
||||
['Declared subsidy', b.subsidy_sompi ? `${ign(b.subsidy_sompi, 8)} IGN <span class="kv">E(DAA) of spec 2.5, paid when a later chain block merges this one</span>` : 'n/a'],
|
||||
['Coinbase paid', b.paid_sompi ? `${ign(b.paid_sompi, 8)} IGN <span class="kv">to the blocks it merges</span>` : 'n/a'],
|
||||
['Merged by', b.chain ? 'itself (chain block)' : (j.merged_by ? link(j.merged_by) : '<span class="kv">not yet</span>')],
|
||||
['Selected parent', b.selected_parent ? link(b.selected_parent) : 'n/a'],
|
||||
]);
|
||||
$('header').innerHTML = dl([
|
||||
|
|
@ -380,21 +405,25 @@ async function load() {
|
|||
const ms = d.mergeset || { blues: [], reds: [] };
|
||||
$('mergeset').innerHTML = (ms.blues.length || ms.reds.length) ? `<div style="display:flex;flex-direction:column;gap:6px">${ms.blues.map(h => `<div><i class="sw blue"></i>${link(h)}</div>`).join('')}${ms.reds.map(h => `<div><i class="sw red"></i>${link(h)}</div>`).join('')}</div>` : '<div class="empty">Empty.</div>';
|
||||
const cb = d.coinbase;
|
||||
$('tx-n').textContent = `${b.tx_count ?? 0} EVM, 1 coinbase`;
|
||||
$('coinbase').innerHTML = cb ? `<dl>${dl([['Coinbase', `<span class="hash">${esc(cb.id)}</span>`], ['Outputs', cb.outputs.map(o => `${o.pool ? '<span style="color:var(--ash)">proving pool (burns on the UTXO side, credited on the EVM side)</span>' : addr(o.address)} <b>${ign(o.value, 8)} IGN</b>`).join('<br>') || 'none'], ['Carries', `${cb.records} proof record${cb.records === 1 ? '' : 's'}${cb.reveal ? ', a key reveal' : ''}${(d.certificates || []).length ? `, certificate ${d.certificates.join(', ')}` : ''}`]])}</dl>` : '<div class="empty">No coinbase decoded.</div>';
|
||||
const txs = d.txs || []; const ev = j.evm && j.evm.transactions ? new Map(j.evm.transactions.map(t => [t.hash, t])) : null;
|
||||
$('txs').innerHTML = txs.length ? `<table><thead><tr><th>Transaction</th><th>From</th><th>To</th><th class="n">Value</th><th class="n">Bytes</th></tr></thead><tbody>${txs.map(t => { const e = ev && ev.get(t.hash); return `<tr id="tx-${esc((t.hash || '').replace(/^0x/, ''))}"><td class="hash">${esc(t.hash || '?')}</td><td>${e ? addr(e.from) : '<span style="color:var(--ash)">needs an EVM RPC</span>'}</td><td>${e ? addr(e.to) : ''}</td><td class="n">${e ? ignFromWei(e.value) + ' IGN' : ''}</td><td class="n">${t.size ?? ''}</td></tr>`; }).join('')}</tbody></table>` : '<div class="empty" style="margin-top:8px">No EVM transactions in this block.</div>';
|
||||
$('tx-n').textContent = `${b.tx_count ?? 0} carried, ${j.txs.length} executed by the EVM, 1 coinbase`;
|
||||
$('coinbase').innerHTML = cb ? `<dl>${dl([['Coinbase', `<span class="hash">${esc(cb.id)}</span>`], ['Outputs', cb.outputs.map(o => `${o.pool ? '<span class="kv">proving pool (burns on the UTXO side, credited on the EVM side)</span>' : addr(o.address)} <b>${ign(o.value, 8)} IGN</b>`).join('<br>') || 'none'], ['Carries', `${cb.records} proof record${cb.records === 1 ? '' : 's'}${cb.reveal ? ', a key reveal' : ''}${(d.certificates || []).length ? `, certificate ${d.certificates.join(', ')}` : ''}`]])}</dl>` : '<div class="empty">No coinbase decoded.</div>';
|
||||
const carried = d.txs || [];
|
||||
$('txs').innerHTML = j.txs.length ? `<table><thead><tr><th>Transaction</th><th>From</th><th>To</th><th class="n">Value</th><th>Status</th><th class="n">Gas used</th></tr></thead><tbody>${j.txs.map(t => `<tr id="tx-${esc(t.hash.replace(/^0x/, ''))}"><td><a class="hash" href="/tx/${esc(t.hash)}">${esc(shortHash(t.hash, 12, 8))}</a></td><td>${t.from ? `<a class="hash" href="/address/${esc(t.from)}">${esc(shortHash(t.from, 8, 6))}</a>` : ''}</td><td>${t.to ? `<a class="hash" href="/address/${esc(t.to)}">${esc(shortHash(t.to, 8, 6))}</a>` : (t.contract_address ? `<span class="kv">created</span> <a class="hash" href="/address/${esc(t.contract_address)}">${esc(shortHash(t.contract_address, 8, 6))}</a>` : '<span class="kv">none</span>')}</td><td class="n">${ignFromWei(t.value_wei, 6)} IGN</td><td>${t.status === 'success' ? '<span class="chip ok">ok</span>' : t.status === 'failed' ? '<span class="chip" style="border-color:var(--excluded);color:var(--excluded)">failed</span>' : '<span class="kv">no receipt</span>'}</td><td class="n">${int(t.gas_used)}</td></tr>`).join('')}</tbody></table>` :
|
||||
(carried.length ? `<p class="note">This block carries ${carried.length} transaction${carried.length === 1 ? '' : 's'}; ${b.chain ? 'the indexer has not read the block yet, or every one had already been executed in an earlier chain block' : 'they execute in the chain block that merges this one'}.</p>` : '<div class="empty" style="margin-top:8px">No EVM transactions in this block.</div>');
|
||||
if (j.evm && !j.evm.error) $('txs').insertAdjacentHTML('beforeend', `<p class="note">EVM view of this chain block: gas used ${int(j.evm.gas_used)}, state root <span class="hash">${esc(shortHash(j.evm.state_root, 10, 6))}</span>${j.evm.igneum ? `, rewards ${j.evm.igneum.rewards.map(r => ignFromWei(r.wei) + ' IGN to ' + shortHash(r.miner, 6, 4)).join(', ')}, proving pool credit ${ignFromWei(j.evm.igneum.proving_pool_credit)} IGN` : ''}.</p>`);
|
||||
const sh = j.shards;
|
||||
$('proof-eyebrow').textContent = b.chain ? `${sh.length} shard${sh.length === 1 ? '' : 's'} planned for this chain block` : 'not a chain block';
|
||||
$('proofs').innerHTML = `<p style="font-size:14px;color:var(--ink-2)">This block carries <b>${b.proof_records ?? 0}</b> proof record${b.proof_records === 1 ? '' : 's'} for earlier chain blocks (274 bytes each, in the coinbase extra data).</p>` +
|
||||
(sh.length ? `<div class="tbl"><table><thead><tr><th class="n">Shard</th><th>State</th><th>Prover</th><th class="n">pgas</th><th class="n">Proof lag, DAA</th><th>Carried by</th><th class="n">Payout</th></tr></thead><tbody>${sh.map(s => `<tr><td class="n">${s.i} of ${s.n}</td><td><span class="chip ${s.state === 'paid' ? 'ok' : s.state === 'verified' ? 'hot' : ''}">${esc(s.state)}</span></td><td class="id">${esc(s.prover || '')}</td><td class="n">${int(s.pgas)}</td><td class="n">${s.lag === null ? '' : int(s.lag)}</td><td>${s.carried_by ? link(s.carried_by) : ''}</td><td class="n">${s.payout_wei ? ignFromWei(s.payout_wei, 6) + ' IGN' : ''}</td></tr>`).join('')}</tbody></table></div>` : (b.chain ? '<div class="empty">No shard plan recorded (the proving feed was not reading this block).</div>' : ''));
|
||||
$('proofs').innerHTML = `<p style="font-size:14px;color:var(--ink-2)">This block carries <b>${b.proof_records ?? 0}</b> proof record${b.proof_records === 1 ? '' : 's'} for earlier chain blocks (274 bytes each, in the coinbase extra data).${b.chain ? ` Its own execution is split into <b>${sum.total}</b> shard${sum.total === 1 ? '' : 's'}: ${sum.paid} paid, ${sum.verified} verified, ${sum.proving} proving, ${sum.planned} planned.` : ''}</p>` +
|
||||
(sh.length ? `<div class="tbl"><table><thead><tr><th class="n">Shard</th><th>State</th><th>Prover</th><th class="n">pgas</th><th class="n">Proof lag, DAA</th><th>Carried by</th><th class="n">Payout</th><th>Node verdict</th></tr></thead><tbody>${sh.map(s => `<tr><td class="n">${s.i} of ${s.n}</td><td><span class="chip ${s.state === 'paid' ? 'ok' : s.state === 'verified' || s.state === 'proving' ? 'hot' : ''}">${esc(s.state)}</span></td><td class="id">${esc(s.prover || '')}</td><td class="n">${int(s.pgas)}</td><td class="n">${s.lag === null ? '' : int(s.lag)}</td><td>${s.carried_by ? link(s.carried_by) : ''}</td><td class="n">${s.payout_wei ? ignFromWei(s.payout_wei, 6) + ' IGN' : ''}</td><td>${s.node_verified === true ? 'proof verified by the node' : s.node_verified === false ? 'not verified' : '<span class="kv">no record yet</span>'}</td></tr>`).join('')}</tbody></table></div><p class="note">State follows the record: planned when the node published the shard plan, proving when a prover's record is in the pool, verified when the node's SP1 verifier accepted the proof, paid when a later coinbase carried and paid it. The explorer does not re-run the STARK itself; the node's verdict is shown as the node's. Every state, number and record on this page is read from the observer node, the chain the finality certificates follow.</p>` : (b.chain ? '<div class="empty">No shard plan recorded (the proving feed has not read this block).</div>' : ''));
|
||||
const cp = j.checkpoint, certs = j.certificates || [];
|
||||
$('fin-eyebrow').textContent = cp ? `checkpoint ${cp.index}` : (certs.length ? 'carries a certificate' : 'not a checkpoint');
|
||||
$('finality').innerHTML = (cp ? `<dl>${dl([['State', `<span class="chip ${cp.state === 'locked' ? 'ok' : ''}">${esc(cp.state)}</span>`], ['Signed weight', `${int(cp.signed)} of ${int(cp.total)} (${(cp.fraction_total * 100).toFixed(1)}% of all weight, ${(cp.fraction_active * 100).toFixed(1)}% of active)`], ['Votes', `${int(cp.votes)} of ${int(cp.voters)} voters`], cp.locked_at ? ['Locked', utc(new Date(cp.locked_at).getTime())] : null])}</dl>` : '') +
|
||||
certs.map(c => `<dl style="margin-top:14px">${dl([[c.role === 'checkpoint' ? 'Certificate' : 'Carries certificate', `index ${c.index}, ${c.voter_count} voters, aggregator <span class="id">${esc(shortHash(c.aggregator, 8, 0))}</span>, ${c.role === 'checkpoint' ? 'carried by ' + link(c.carrier) : 'for checkpoint ' + link(c.checkpoint)}`], ['Weight', `${int(c.total_weight)} total, ${int(c.active_weight)} active at the lock`], c.previous ? ['Previous lock', `index ${c.previous.index}, ${link(c.previous.hash)}${c.headers_complete ? ', header chain stored' : ''}`] : null, ['Verify', `<a href="/api/checkpoint">/api/checkpoint</a> serves the newest certificate for the in-browser verifier`]])}</dl>`).join('') +
|
||||
(!cp && !certs.length ? '<div class="empty">This block is neither a checkpoint nor a certificate carrier. Checkpoints are every 30 blue score; a lock needs two thirds of all 30-day weight.</div>' : '');
|
||||
certs.map(c => `<dl style="margin-top:14px">${dl([[c.role === 'checkpoint' ? 'Certificate' : 'Carries certificate', `index ${c.index}, ${c.voter_count} voters, aggregator <span class="id">${esc(shortHash(c.aggregator, 8, 0))}</span>, ${c.role === 'checkpoint' ? 'carried by ' + link(c.carrier) : 'for checkpoint ' + link(c.checkpoint)}`], ['Weight', `${int(c.total_weight)} total, ${int(c.active_weight)} active at the lock`], c.previous ? ['Previous lock', `index ${c.previous.index}, ${link(c.previous.hash)}${c.headers_complete ? ', header chain stored' : ''}`] : null, ['Data', `<a href="/api/checkpoint?index=${c.index}">/api/checkpoint?index=${c.index}</a> serves this certificate for the in-browser verifier`]])}</dl>`).join('') +
|
||||
(!cp && !certs.length ? `<div class="empty">This block is neither a checkpoint nor a certificate carrier. Checkpoints are every 30 blue score; a lock needs two thirds of all 30-day weight.${b.lock && b.lock.final ? ` This block is under lock ${int(b.lock.index)}.` : ''}</div>` : '');
|
||||
$('body').hidden = false;
|
||||
const ownCert = certs.find(c => c.role === 'checkpoint');
|
||||
if (ownCert) recheck(ownCert.index, b.hash, true);
|
||||
else if (b.lock && b.lock.final && b.lock.index !== undefined) recheck(b.lock.index, b.hash, false);
|
||||
}
|
||||
load().catch(e => { $('msg').textContent = 'The API did not answer: ' + e.message; });
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -406,8 +406,9 @@ function renderEvidence(html) {
|
|||
}
|
||||
|
||||
const PAGES = [['index.html', ''], ['download.html', 'download'], ['income.html', 'income'], ['litepaper.html', 'litepaper'], ['live.html', 'live'], ['evidence.html', 'evidence'], ['miner.html', 'miner'], ['app.html', 'app'], ['wallet.html', 'wallet'], ['ledger.html', 'ledger'], ['metamask.html', 'metamask'], ['faucet.html', 'faucet'], ['swap.html', 'swap'], ['404.html', ''],
|
||||
// the DAG explorer (5 Oct 2026): /explorer, /block/<hash> and /address/<addr> (vercel.json rewrites the last two)
|
||||
['explorer.html', 'live'], ['block.html', 'live'], ['address.html', 'live'],
|
||||
// the Devnet 3 explorer (5 Oct 2026, extended 8 Oct 2026): /explorer, /block/<hash>, /address/<addr>, /tx/<hash> (vercel.json rewrites
|
||||
// the three) and /proving; the Explorer entry of the Network panel is the current one on all five
|
||||
['explorer.html', 'explorer'], ['block.html', 'explorer'], ['address.html', 'explorer'], ['tx.html', 'explorer'], ['proving.html', 'explorer'],
|
||||
// the scenes lab (6 Oct 2026): three animated views of the live feed, unlinked, noindex
|
||||
['scenes.html', '']];
|
||||
for (const [file, active] of PAGES) {
|
||||
|
|
|
|||
|
|
@ -3,22 +3,22 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Igneum explorer</title>
|
||||
<meta name="description" content="The Igneum block DAG, block by block: hash, DAA score, blue score, miner, transactions and proof records, with the network numbers and the supply.">
|
||||
<title>Igneum Devnet 3 explorer</title>
|
||||
<meta name="description" content="Devnet 3, block by block: the DAG, every transaction, every account, the shard plan and proof of each block, the finality lock that covers it. A test network: coins have no value.">
|
||||
<link rel="canonical" href="https://igneum.network/explorer">
|
||||
<meta name="theme-color" content="#0C0C0E">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Igneum">
|
||||
<meta property="og:title" content="Igneum explorer">
|
||||
<meta property="og:description" content="The Igneum block DAG, block by block: hash, DAA score, blue score, miner, transactions and proof records, with the network numbers and the supply.">
|
||||
<meta property="og:title" content="Igneum Devnet 3 explorer">
|
||||
<meta property="og:description" content="Devnet 3, block by block: the DAG, every transaction, every account, the shard plan and proof of each block, the finality lock that covers it. A test network: coins have no value.">
|
||||
<meta property="og:url" content="https://igneum.network/explorer">
|
||||
<meta property="og:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta property="og:image:width" content="256">
|
||||
<meta property="og:image:height" content="256">
|
||||
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Igneum explorer">
|
||||
<meta name="twitter:description" content="The Igneum block DAG, block by block, with the network numbers and the supply.">
|
||||
<meta name="twitter:title" content="Igneum Devnet 3 explorer">
|
||||
<meta name="twitter:description" content="Devnet 3, block by block: the DAG, transactions, accounts, proofs and finality. A test network.">
|
||||
<meta name="twitter:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
|
|
@ -91,6 +91,12 @@ dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
|
|||
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}.two .card{margin-top:0}}
|
||||
.two-wrap{margin-top:var(--gap)}
|
||||
main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--line);border-radius:16px;padding:25px;margin-top:24px}.card .viz-head h2{font-size:19px}.card .viz-head .eyebrow{margin:0}
|
||||
.badge{display:inline-flex;align-items:center;gap:6px;font-family:var(--f-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;padding:4px 9px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2);margin:0 6px 6px 0}
|
||||
.badge.ok{border-color:var(--green,#5CC48A);color:var(--green,#5CC48A)}.badge.hot{border-color:var(--molten);color:var(--molten)}.badge.bad{border-color:var(--excluded);color:var(--excluded)}.badge.wait{color:var(--ash)}
|
||||
.bar{display:inline-block;width:72px;height:7px;border-radius:4px;background:var(--line);vertical-align:middle;margin-right:7px;overflow:hidden}.bar i{display:block;height:100%;background:var(--green,#5CC48A)}.bar i.part{background:var(--molten)}
|
||||
.dagbox{position:relative;width:100%;height:320px;border:1px solid var(--line);border-radius:12px;background:var(--obsidian);overflow:hidden}.dagbox canvas{width:100%;height:100%;display:block;cursor:pointer}
|
||||
.dagtip{position:absolute;pointer-events:none;font:11px/1.5 var(--mono);color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:8px;padding:8px 10px;max-width:300px;display:none}
|
||||
.kv{color:var(--ash)}
|
||||
</style>
|
||||
</head>
|
||||
<body class="off">
|
||||
|
|
@ -130,8 +136,8 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
</div>
|
||||
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/live" data-nav="live" aria-current="page"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
|
|
@ -169,8 +175,8 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Network</div>
|
||||
<a href="/live" data-nav="live" aria-current="page"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
|
|
@ -245,11 +251,11 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<main id="main" class="wrap">
|
||||
<div class="head">
|
||||
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><span>Explorer</span></div>
|
||||
<div class="eyebrow"><span class="line"></span>devnet, last 24 hours</div>
|
||||
<h1>Explorer<span class="accent">.</span></h1>
|
||||
<p>Every block the devnet made in the last day, read from a node by the observer. Paste a block hash, a transaction hash, a chain block number or an address, or click any row.</p>
|
||||
<div class="eyebrow"><span class="line"></span>Devnet 3, chain id 4463</div>
|
||||
<h1>Devnet 3 explorer<span class="accent">.</span></h1>
|
||||
<p>Every block Devnet 3 made in the last day and every transaction of the last seven days, read from a node by the observer and the indexer. Paste a block hash, a transaction hash, a chain block number or an address, or click any row. Devnet 3 is a test network: its coins have no value and the chain may reset. Every number here comes from one node, the observer node on the build box, the chain the finality certificates follow; the indexer reads that node's execution RPC and nothing else.</p>
|
||||
<form class="search" id="search" role="search">
|
||||
<input id="q" type="search" autocomplete="off" spellcheck="false" placeholder="block hash, transaction hash, chain block number, 0x address or igneumdev:… address" aria-label="Search the explorer">
|
||||
<input id="q" type="search" autocomplete="off" spellcheck="false" placeholder="block hash, transaction hash, chain block number, 0x address or igneumdev:… address" aria-label="Search the Devnet 3 explorer">
|
||||
<button class="btn primary" type="submit">Search</button>
|
||||
</form>
|
||||
<div class="msg" id="msg" aria-live="polite"></div>
|
||||
|
|
@ -258,32 +264,53 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
<div class="cell status"><div class="k">Status</div><div class="v"><span class="dot"></span><span id="st-status">OFFLINE</span></div><div class="s" id="st-age">no observer update yet</div></div>
|
||||
<div class="cell"><div class="k">Height</div><div class="v" id="st-height">0</div><div class="s" id="st-count">chain block number, 0 blocks in the DAG</div></div>
|
||||
<div class="cell"><div class="k">DAA score</div><div class="v" id="st-daa">0</div><div class="s" id="st-blue">blue score 0</div></div>
|
||||
<div class="cell"><div class="k">Difficulty</div><div class="v" id="st-diff">0</div><div class="s" id="st-bt">target per block</div></div>
|
||||
<div class="cell"><div class="k">Hash rate</div><div class="v" id="st-hps">0</div><div class="s">network estimate</div></div>
|
||||
<div class="cell"><div class="k">Block reward</div><div class="v" id="st-reward">0<small>IGN</small></div><div class="s" id="st-split">80% miner, 20% proving pool</div></div>
|
||||
<div class="cell"><div class="k">Circulating</div><div class="v" id="st-supply">0<small>IGN</small></div><div class="s" id="st-cap">of 4,000,000,000 by the rule</div></div>
|
||||
<div class="cell"><div class="k">Next halving</div><div class="v" id="st-halving">0<small>d</small></div><div class="s" id="st-ramp">ramp</div></div>
|
||||
<div class="cell"><div class="k">Hash rate</div><div class="v" id="st-hps">0</div><div class="s" id="st-diff">difficulty 0</div></div>
|
||||
<div class="cell"><div class="k">Chain rate, 10 min</div><div class="v" id="st-rate">0<small>blocks/s</small></div><div class="s" id="st-rate-s">DAG blocks per second; chain blocks per second</div></div>
|
||||
<div class="cell"><div class="k">Finality</div><div class="v" id="st-fin">n/a</div><div class="s" id="st-fin-s">voters and weight</div></div>
|
||||
<div class="cell"><div class="k">Program class</div><div class="v" id="st-class">v4</div><div class="s" id="st-class-s">the hash program family at the tip</div></div>
|
||||
<div class="cell"><div class="k">Proving</div><div class="v" id="st-prov">0<small>paid</small></div><div class="s" id="st-prov-s">shards paid in 10 min</div></div>
|
||||
</div>
|
||||
<div class="card" aria-label="The DAG">
|
||||
<div class="viz-head"><h2>The DAG</h2><div class="eyebrow" id="dag-eyebrow">the newest 150 blocks, one lane per miner</div></div>
|
||||
<div class="dagbox"><canvas id="dag" aria-label="The last 150 blocks of Devnet 3 as a graph: time left to right, one lane per miner, lines to parents"></canvas><div class="dagtip" id="dag-tip"></div></div>
|
||||
<div class="legend" style="margin-top:12px"><span><i class="sw chain"></i>chain block</span><span><i class="sw blue"></i>blue, merged</span><span><i class="sw red"></i>red, excluded</span><span><i class="sw"></i>pending</span><span>◆ under a lock</span><span>● every shard paid</span></div>
|
||||
<p class="note">Left to right is arrival order at the observer; a line joins a block to each parent in the picture. Blue blocks are merged and paid; red blocks are excluded from the order. A diamond outline marks a block under a finality lock, a filled dot a block whose every planned shard has been paid. Click a block for its page.</p>
|
||||
</div>
|
||||
<div class="card" aria-label="Latest blocks">
|
||||
<div class="viz-head"><h2>Latest blocks</h2><div class="eyebrow" id="blk-eyebrow">newest first, every 5 s</div></div>
|
||||
<div class="tbl">
|
||||
<table>
|
||||
<thead><tr><th>Block</th><th class="n">Number</th><th class="n">DAA</th><th class="n">Blue score</th><th>Miner</th><th class="n">Txs</th><th class="n">Proof records</th><th>Time</th></tr></thead>
|
||||
<tbody id="blocks"><tr><td colspan="8" class="empty">Loading.</td></tr></tbody>
|
||||
<thead><tr><th>Block</th><th class="n">Number</th><th class="n">DAA</th><th class="n">Blue score</th><th>Class</th><th>Miner</th><th class="n">Txs</th><th>Shards paid</th><th>Final</th><th>Time</th></tr></thead>
|
||||
<tbody id="blocks"><tr><td colspan="10" class="empty">Loading.</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="legend"><span><i class="sw chain"></i>chain block</span><span><i class="sw blue"></i>blue, merged and paid</span><span><i class="sw red"></i>red, excluded</span><span><i class="sw"></i>pending, not merged yet</span><span>★ locked checkpoint</span></div>
|
||||
<p class="note">Number is the chain block number, the same number the EVM reports; a block off the selected chain has none. Miner is the payout address the coinbase names. Proof records is how many shard proofs the block carries for earlier blocks. Click a row for the block.</p>
|
||||
<p class="note">Number is the chain block number, the same number the EVM reports; a block off the selected chain has none. Class is the hash program family the block was mined under. Txs is what the block carries; the EVM executes each transaction once, in the first chain block that includes it. Shards paid counts the block's proof shards a later coinbase has paid. Final means a finality lock covers the block.</p>
|
||||
<button class="btn" id="more" type="button" style="margin-top:14px">Older blocks</button>
|
||||
</div>
|
||||
<div class="card" aria-label="Public API">
|
||||
<div class="viz-head"><h2>Public API</h2><div class="eyebrow">JSON, cached 10 s</div></div>
|
||||
<dl>
|
||||
<dt>/api/stats</dt><dd><a href="/api/stats" class="hash">network hash rate, difficulty, block time, block reward now, blocks per day, DAA, blue score, height, last block, chain id, version</a></dd>
|
||||
<dt>/api/supply</dt><dd><a href="/api/supply" class="hash">circulating by the emission rule, the 4,000,000,000 cap, the halving table, the 30-day ramp, the observer's coinbase check</a></dd>
|
||||
<dt>/api/explorer</dt><dd><a href="/api/explorer?blocks=20" class="hash">?blocks=N, ?block=hash, ?height=N, ?address=addr, ?search=q</a></dd>
|
||||
</dl>
|
||||
<p class="note">Reward and supply come from the emission rule of the specification (section 2.5) at the node's DAA score; the observer checks the chain's coinbase sums against it every hour and the result is in /api/supply under check.</p>
|
||||
<div class="card" aria-label="Latest transactions">
|
||||
<div class="viz-head"><h2>Latest transactions</h2><div class="eyebrow" id="tx-eyebrow">newest first, every 5 s</div></div>
|
||||
<div class="tbl">
|
||||
<table>
|
||||
<thead><tr><th>Transaction</th><th class="n">Block</th><th>From</th><th>To</th><th class="n">Value</th><th>Status</th><th>Time</th></tr></thead>
|
||||
<tbody id="txs"><tr><td colspan="7" class="empty">Loading.</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="note">EVM transactions as the indexer read them from the node's receipts: seven days are kept. Value is in IGN (18 decimals on the EVM side).</p>
|
||||
<button class="btn" id="more-tx" type="button" style="margin-top:14px">Older transactions</button>
|
||||
</div>
|
||||
<div class="two two-wrap">
|
||||
<div class="card" aria-label="Proving"><div class="viz-head"><h2>Proving</h2><div class="eyebrow" id="pv-eyebrow">shards, provers, latency</div></div><dl id="pv"></dl><p class="note"><a href="/proving">The proving view</a>: every chain block's shard plan, who proved what, and the block-to-paid latency.</p></div>
|
||||
<div class="card" aria-label="Public API"><div class="viz-head"><h2>Public API</h2><div class="eyebrow">JSON, cached 5 to 10 s</div></div>
|
||||
<dl>
|
||||
<dt>/api/stats</dt><dd><a href="/api/stats" class="hash">hash rate, difficulty, block time, reward, DAA, blue score, height</a></dd>
|
||||
<dt>/api/supply</dt><dd><a href="/api/supply" class="hash">circulating by the emission rule, the cap, the halving table</a></dd>
|
||||
<dt>/api/explorer</dt><dd><a href="/api/explorer?blocks=20" class="hash">?blocks=N, ?block=hash, ?height=N, ?tx=hash, ?txs=N, ?address=addr, ?proving=1, ?stats=1, ?dag=N, ?search=q</a></dd>
|
||||
<dt>/api/checkpoint</dt><dd><a href="/api/checkpoint" class="hash">the newest certified checkpoint with its voter table and headers; ?index=N for one</a></dd>
|
||||
<dt>/api/live</dt><dd><a href="/api/live" class="hash">the observer's feed: blocks of the last 90 s, miners, events, finality, proving</a></dd>
|
||||
</dl>
|
||||
<p class="note">Every answer names the network (Devnet 3, chain id 4463). The RPC itself is at rpc.devnet.igneum.network (read methods and eth_sendRawTransaction, 20 requests per second per address).</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- footer:start -->
|
||||
|
|
@ -357,11 +384,12 @@ main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--lin
|
|||
</script>
|
||||
<!-- footer:end -->
|
||||
<script type="module">
|
||||
import { classify, hrefFor, ign, shortHash, hashrate, rel, int } from '/lib/explorer.mjs';
|
||||
import { classify, hrefFor, ign, ignFromWei, shortHash, hashrate, rel, int } from '/lib/explorer.mjs';
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
let oldest = 0, serverNow = 0, serverAt = 0;
|
||||
const esc = s => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
let oldest = 0, oldestTx = 0, serverNow = 0, serverAt = 0;
|
||||
const vnow = () => serverNow + (performance.now() - serverAt);
|
||||
const stamp = iso => iso ? new Date(iso).toISOString().slice(11, 19) + ' UTC' : '';
|
||||
$('search').addEventListener('submit', async e => {
|
||||
e.preventDefault();
|
||||
const c = classify($('q').value);
|
||||
|
|
@ -374,46 +402,109 @@ $('search').addEventListener('submit', async e => {
|
|||
if (j.ok && j.href) location.href = j.href; else $('msg').textContent = j.error || 'Not found.';
|
||||
} catch { $('msg').textContent = 'The API did not answer.'; }
|
||||
});
|
||||
const minerCell = b => (b.evm_miner ? `<a class="hash" href="/address/${b.evm_miner}" title="${esc(b.evm_miner)}">${esc(shortHash(b.evm_miner, 6, 4))}</a>` : (b.miner ? `<a class="hash" href="/address/${esc(b.miner)}">${esc(shortHash(b.miner, 14, 4))}</a>` : '<span class="id">' + esc(b.miner_id || '?') + '</span>')) + `<span class="id" style="margin-left:8px;font-size:12px">${esc(b.miner_id || '')}</span>`;
|
||||
function shardsCell(b) {
|
||||
const s = b.shards; if (!s || !s.total) return b.chain ? '<span class="kv">no plan yet</span>' : '<span class="kv">not a chain block</span>';
|
||||
const pct = Math.round(s.paid / s.total * 100);
|
||||
return `<span class="bar" title="${s.paid} paid, ${s.verified} verified, ${s.proving} proving, ${s.planned} planned"><i class="${s.paid === s.total ? '' : 'part'}" style="width:${pct}%"></i></span>${s.paid} of ${s.total}`;
|
||||
}
|
||||
function rowHtml(b) {
|
||||
const sw = b.chain ? 'chain' : b.color;
|
||||
const miner = b.evm_miner ? `<a class="hash" href="/address/${b.evm_miner}" title="${esc(b.evm_miner)}">${esc(shortHash(b.evm_miner, 6, 4))}</a>` : (b.miner ? `<a class="hash" href="/address/${esc(b.miner)}">${esc(shortHash(b.miner, 14, 4))}</a>` : '<span class="id">' + esc(b.miner_id || '?') + '</span>');
|
||||
return `<tr data-href="/block/${b.hash}"><td><i class="sw ${sw}" title="${b.chain ? 'chain block' : b.color}"></i><a class="hash" href="/block/${b.hash}">${esc(shortHash(b.hash, 10, 6))}</a>${b.locked ? ' ★' : ''}</td><td class="n">${b.number === null ? '<span style="color:var(--ash)">off chain</span>' : int(b.number)}</td><td class="n">${int(b.daa)}</td><td class="n">${int(b.blue_score)}</td><td>${miner}<span class="id" style="margin-left:8px;font-size:12px">${esc(b.miner_id || '')}</span></td><td class="n">${b.tx_count === null ? 'n/a' : b.tx_count}</td><td class="n">${b.proof_records === null ? 'n/a' : b.proof_records}</td><td title="${new Date(b.ts).toISOString()}">${rel(vnow() - b.ts)}</td></tr>`;
|
||||
const replaced = b.chain && b.chain_now === false, sw = replaced ? '' : (b.chain ? 'chain' : b.color);
|
||||
return `<tr data-href="/block/${b.hash}"><td><i class="sw ${sw}" title="${replaced ? 'was the chain block of its number, replaced by a tip reorg' : (b.chain ? 'chain block' : b.color)}"></i><a class="hash" href="/block/${b.hash}">${esc(shortHash(b.hash, 10, 6))}</a>${b.locked ? ' ★' : ''}</td><td class="n">${b.number === null ? '<span class="kv">off chain</span>' : (replaced ? `<span class="kv" title="a tip reorg gave this number to another block">${int(b.number)}, replaced</span>` : int(b.number))}</td><td class="n">${int(b.daa)}</td><td class="n">${int(b.blue_score)}</td><td><span class="chip">${esc(b.class || 'v4')}</span></td><td>${minerCell(b)}</td><td class="n">${b.tx_count === null ? 'n/a' : b.tx_count}</td><td>${shardsCell(b)}</td><td>${b.final ? '<span class="chip ok">final</span>' : '<span class="kv">not yet</span>'}</td><td title="${new Date(b.ts).toISOString()}">${rel(vnow() - b.ts)}</td></tr>`;
|
||||
}
|
||||
async function loadBlocks(before) {
|
||||
const r = await fetch('/api/explorer?blocks=50' + (before ? '&before=' + before : ''));
|
||||
const j = await r.json(); if (!j.ok) throw new Error(j.error);
|
||||
serverNow = new Date(j.now).getTime(); serverAt = performance.now();
|
||||
const html = j.blocks.map(rowHtml).join('');
|
||||
if (before) $('blocks').insertAdjacentHTML('beforeend', html); else $('blocks').innerHTML = html || '<tr><td colspan="8" class="empty">No blocks in the last 24 hours.</td></tr>';
|
||||
if (before) $('blocks').insertAdjacentHTML('beforeend', html); else $('blocks').innerHTML = html || '<tr><td colspan="10" class="empty">No blocks in the last 24 hours.</td></tr>';
|
||||
if (j.blocks.length) oldest = j.blocks[j.blocks.length - 1].rx;
|
||||
$('blk-eyebrow').textContent = (j.stale ? 'observer offline, last known' : 'newest first, every 5 s') + (j.network ? ', ' + j.network : '');
|
||||
$('blk-eyebrow').textContent = (j.stale ? 'observer offline, last known' : 'newest first, every 5 s') + (j.lock ? `, lock ${int(j.lock.index)} at blue score ${int(j.lock.blue_score)}` : ', no lock yet');
|
||||
}
|
||||
$('blocks').addEventListener('click', e => { const tr = e.target.closest('tr[data-href]'); if (tr && !e.target.closest('a')) location.href = tr.dataset.href; });
|
||||
function txHtml(t) {
|
||||
return `<tr data-href="/tx/${t.hash}"><td><a class="hash" href="/tx/${t.hash}">${esc(shortHash(t.hash, 10, 6))}</a></td><td class="n"><a href="/block/${t.block_hash}">${int(t.number)}</a></td><td><a class="hash" href="/address/${t.from}" title="${esc(t.from)}">${esc(shortHash(t.from, 6, 4))}</a></td><td>${t.to ? `<a class="hash" href="/address/${t.to}" title="${esc(t.to)}">${esc(shortHash(t.to, 6, 4))}</a>` : (t.contract_address ? `<span class="kv">created </span><a class="hash" href="/address/${t.contract_address}">${esc(shortHash(t.contract_address, 6, 4))}</a>` : '<span class="kv">none</span>')}</td><td class="n">${ignFromWei(t.value_wei, 4)} IGN</td><td>${t.status === 'success' ? '<span class="chip ok">ok</span>' : t.status === 'failed' ? '<span class="chip" style="border-color:var(--excluded);color:var(--excluded)">failed</span>' : '<span class="kv">no receipt</span>'}</td><td title="${t.ts ? new Date(t.ts).toISOString() : ''}">${t.ts ? rel(vnow() - t.ts) : 'n/a'}</td></tr>`;
|
||||
}
|
||||
async function loadTxs(before) {
|
||||
const r = await fetch('/api/explorer?txs=30' + (before ? '&before=' + before : ''));
|
||||
const j = await r.json(); if (!j.ok) throw new Error(j.error);
|
||||
const html = j.txs.map(txHtml).join('');
|
||||
if (before) $('txs').insertAdjacentHTML('beforeend', html); else $('txs').innerHTML = html || '<tr><td colspan="7" class="empty">No transactions indexed yet.</td></tr>';
|
||||
if (j.txs.length) oldestTx = j.txs[j.txs.length - 1].number;
|
||||
const ix = j.indexer || {};
|
||||
$('tx-eyebrow').textContent = ix.stale ? 'indexer stale, last known' : `newest first, every 5 s; ${int(ix.txs_total)} transactions and ${int(ix.accounts_total)} accounts in seven days, indexed to chain block ${int(ix.indexed_number)}`;
|
||||
}
|
||||
for (const [tb] of [['blocks'], ['txs']]) $(tb).addEventListener('click', e => { const tr = e.target.closest('tr[data-href]'); if (tr && !e.target.closest('a')) location.href = tr.dataset.href; });
|
||||
$('more').addEventListener('click', () => { $('more').disabled = true; loadBlocks(oldest).catch(() => {}).finally(() => { $('more').disabled = false; clearInterval(timer); }); });
|
||||
$('more-tx').addEventListener('click', () => { $('more-tx').disabled = true; loadTxs(oldestTx).catch(() => {}).finally(() => { $('more-tx').disabled = false; clearInterval(timerTx); }); });
|
||||
async function loadStats() {
|
||||
const [s, p] = await Promise.all([fetch('/api/stats').then(r => r.json()), fetch('/api/supply').then(r => r.json())]);
|
||||
const s = await fetch('/api/explorer?stats=1').then(r => r.json()); if (!s.ok) throw new Error(s.error);
|
||||
document.body.classList.toggle('on', !s.stale); document.body.classList.toggle('off', !!s.stale);
|
||||
$('st-status').textContent = s.stale ? 'OFFLINE' : 'LIVE';
|
||||
$('st-age').textContent = s.age_s === null ? 'no observer update yet' : (s.stale ? `last update ${rel(s.age_s * 1000)}` : `${s.network || ''}, node ${s.node_version || '?'}`);
|
||||
$('st-age').textContent = s.stale ? 'observer stale' : `${s.network || 'Devnet 3'}, node ${s.node_version || '?'}, read ${stamp(s.now)}`;
|
||||
$('st-height').textContent = s.height === null ? 'n/a' : int(s.height);
|
||||
$('st-count').textContent = `chain block number, ${int(s.block_count)} blocks in the DAG`;
|
||||
$('st-daa').textContent = int(s.daa); $('st-blue').textContent = 'blue score ' + int(s.blue_score);
|
||||
$('st-diff').textContent = s.difficulty === null ? 'n/a' : (s.difficulty >= 1e6 ? (s.difficulty / 1e6).toFixed(2) + ' M' : int(s.difficulty));
|
||||
$('st-bt').textContent = s.block_time_measured_s === null ? 'target 1 s per block' : `${s.block_time_measured_s} s per block over 60 s, target 1 s`;
|
||||
$('st-hps').textContent = hashrate(s.hashrate);
|
||||
if (s.block_reward) { $('st-reward').innerHTML = esc(Number(s.block_reward.ign).toFixed(4)) + '<small>IGN</small>'; $('st-split').textContent = `${Number(s.block_reward.miner_ign).toFixed(4)} miner, ${Number(s.block_reward.proving_pool_ign).toFixed(4)} proving pool`; }
|
||||
if (p.ok) {
|
||||
$('st-supply').innerHTML = esc(int(Number(p.circulating_ign))) + '<small>IGN</small>';
|
||||
$('st-cap').textContent = `of ${int(Number(p.max_supply_ign))}, ${(Number(p.circulating_ign) / Number(p.max_supply_ign) * 100).toFixed(3)}% by the rule`;
|
||||
const d = p.halving.next_halving_in_s / 86400;
|
||||
$('st-halving').innerHTML = esc(d >= 1 ? Math.floor(d).toLocaleString('en-GB') : (p.halving.next_halving_in_s / 3600).toFixed(1)) + '<small>' + (d >= 1 ? 'days' : 'h') + '</small>';
|
||||
$('st-ramp').textContent = p.ramp.complete ? 'launch ramp complete' : `launch ramp at ${(p.ramp.factor_now * 100).toFixed(1)}%, full at DAA ${int(p.ramp.length_daa_s)}`;
|
||||
}
|
||||
$('st-hps').textContent = hashrate(s.hashrate); $('st-diff').textContent = 'difficulty ' + (s.difficulty === null ? 'n/a' : (s.difficulty >= 1e6 ? (s.difficulty / 1e6).toFixed(2) + ' M' : int(s.difficulty)));
|
||||
$('st-rate').innerHTML = esc(s.blocks_per_s_10m === null ? 'n/a' : s.blocks_per_s_10m.toFixed(2)) + '<small>blocks/s</small>';
|
||||
$('st-rate-s').textContent = `${s.chain_blocks_per_s_10m === null ? 'n/a' : s.chain_blocks_per_s_10m.toFixed(2)} chain blocks per second, ${int(s.miners_10m)} miners in 10 min`;
|
||||
const f = s.finality;
|
||||
if (f) { $('st-fin').innerHTML = f.latest_locked_index === null ? 'no lock' : `lock ${int(f.latest_locked_index)}`; $('st-fin-s').textContent = `${int(f.voters)} voters, ${int(f.total_weight)} blocks of weight${s.lock ? `, ${(s.lock.fraction_total * 100).toFixed(1)}% signed the newest lock` : ''}${f.active ? '' : ', finality paused'}`; }
|
||||
$('st-class').textContent = s.class ? s.class.class : 'v4';
|
||||
const next = s.class && s.class.floors ? s.class.floors.find(x => x.daa > (s.daa || 0)) : null;
|
||||
$('st-class-s').textContent = next ? `${next.class} from DAA ${int(next.daa)}` : (s.class && s.class.floors && s.class.floors.length ? `since DAA ${int(s.class.floors[s.class.floors.length - 1].daa)}` : 'the hash program family at the tip');
|
||||
const p = s.proving || {};
|
||||
$('st-prov').innerHTML = esc(int(p.shards_paid_10m ?? 0)) + '<small>paid</small>';
|
||||
$('st-prov-s').textContent = p.supported === false ? 'the node reports no proving layer' : `shards paid in 10 min, ${int(p.provers_10m ?? 0)} provers, ${int(p.open_blocks ?? 0)} open blocks`;
|
||||
}
|
||||
async function loadProving() {
|
||||
const j = await fetch('/api/explorer?proving=1').then(r => r.json()); if (!j.ok) throw new Error(j.error);
|
||||
const p = j.proving || {}, l = j.latency_1h || {}, t = j.totals_24h || {};
|
||||
const dl = pairs => pairs.map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join('');
|
||||
$('pv-eyebrow').textContent = `read ${stamp(j.now)}`;
|
||||
$('pv').innerHTML = dl([
|
||||
['Paid, 24 h', `${int(t.paid ? t.paid.shards : 0)} shards, ${ignFromWei(t.paid ? t.paid.payout_wei : '0', 2)} IGN to provers`],
|
||||
['Open now', `${int(t.planned ? t.planned.shards : 0)} planned, ${int(t.proving ? t.proving.shards : 0)} proving, ${int(t.verified ? t.verified.shards : 0)} verified and unpaid`],
|
||||
['Provers, 24 h', `${int(j.provers.length)} keys paid`],
|
||||
['Latency, 1 h', l.paid ? `${int(l.paid)} paid; p50 ${int(l.p50_daa)} DAA, p90 ${int(l.p90_daa)} DAA from the block to its payer` : 'no shard paid in the last hour'],
|
||||
['Verifier', p.verifier ? 'the observer node checks every record it sees' : 'n/a'],
|
||||
]);
|
||||
}
|
||||
// the DAG picture: arrival order left to right, one lane per miner, links to parents in the picture
|
||||
const dagTip = $('dag-tip'); let dagBlocks = [], dagPos = new Map();
|
||||
function drawDag() {
|
||||
const cv = $('dag'); if (!cv || !dagBlocks.length) return;
|
||||
const dpr = Math.min(devicePixelRatio || 1, 2), W = cv.clientWidth, H = cv.clientHeight; cv.width = W * dpr; cv.height = H * dpr;
|
||||
const ctx = cv.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H);
|
||||
const cs = getComputedStyle(document.documentElement), col = k => cs.getPropertyValue(k).trim();
|
||||
const lanes = []; const laneOf = new Map();
|
||||
for (const b of dagBlocks) { const m = b.miner_id || '?'; if (!laneOf.has(m)) { laneOf.set(m, lanes.length); lanes.push(m); } }
|
||||
const n = dagBlocks.length, padX = 18, padY = 16, lh = Math.max(14, (H - 2 * padY) / Math.max(1, lanes.length)), xs = (W - 2 * padX) / Math.max(1, n - 1);
|
||||
dagPos = new Map(); dagBlocks.forEach((b, i) => dagPos.set(b.hash, { x: padX + i * xs, y: padY + laneOf.get(b.miner_id || '?') * lh + lh / 2, b }));
|
||||
ctx.lineWidth = 1;
|
||||
for (const b of dagBlocks) { const p = dagPos.get(b.hash); for (const ph of b.parent_hashes || []) { const q = dagPos.get(ph); if (!q) continue; ctx.strokeStyle = b.chain && ph === b.selected_parent ? col('--ember') : 'rgba(154,154,158,.35)'; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke(); } }
|
||||
for (const b of dagBlocks) {
|
||||
const p = dagPos.get(b.hash), r = b.chain ? 5 : 3.5;
|
||||
ctx.fillStyle = b.chain && b.chain_now !== false ? col('--ember') : b.color === 'blue' ? col('--included') : b.color === 'red' ? col('--excluded') : col('--ash');
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, r, 0, Math.PI * 2); ctx.fill();
|
||||
if (b.final) { ctx.strokeStyle = col('--molten'); ctx.beginPath(); ctx.moveTo(p.x, p.y - r - 4); ctx.lineTo(p.x + r + 4, p.y); ctx.lineTo(p.x, p.y + r + 4); ctx.lineTo(p.x - r - 4, p.y); ctx.closePath(); ctx.stroke(); }
|
||||
if (b.shards && b.shards.total && b.shards.paid === b.shards.total) { ctx.fillStyle = col('--bone'); ctx.beginPath(); ctx.arc(p.x, p.y, 1.6, 0, Math.PI * 2); ctx.fill(); }
|
||||
}
|
||||
ctx.font = '10px ' + (col('--mono') || 'monospace'); ctx.fillStyle = col('--ash');
|
||||
lanes.forEach((m, i) => ctx.fillText(m, 4, padY + i * lh + lh / 2 - 7));
|
||||
}
|
||||
function nearest(ev) { const cv = $('dag'), r = cv.getBoundingClientRect(), x = ev.clientX - r.left, y = ev.clientY - r.top; let best = null, bd = 144; for (const p of dagPos.values()) { const d = (p.x - x) ** 2 + (p.y - y) ** 2; if (d < bd) { bd = d; best = p; } } return best; }
|
||||
$('dag').addEventListener('mousemove', ev => { const p = nearest(ev); if (!p) { dagTip.style.display = 'none'; return; } const b = p.b, s = b.shards; dagTip.style.display = 'block'; dagTip.style.left = Math.min(p.x + 12, $('dag').clientWidth - 310) + 'px'; dagTip.style.top = (p.y + 12) + 'px'; dagTip.innerHTML = `${esc(shortHash(b.hash, 10, 6))}<br>${b.chain ? (b.chain_now === false ? 'replaced chain block ' + int(b.number) : 'chain block ' + int(b.number)) : esc(b.color)}, blue score ${int(b.blue_score)}, DAA ${int(b.daa)}<br>miner ${esc(b.miner_id || '?')}, class ${esc(b.class || 'v4')}, ${b.parents} parent${b.parents === 1 ? '' : 's'}<br>${s && s.total ? `${s.paid} of ${s.total} shards paid` : (b.chain ? 'plan not read yet' : 'no shards (not a chain block)')}${b.final ? ', under a lock' : ''}`; });
|
||||
$('dag').addEventListener('mouseleave', () => { dagTip.style.display = 'none'; });
|
||||
$('dag').addEventListener('click', ev => { const p = nearest(ev); if (p) location.href = '/block/' + p.b.hash; });
|
||||
async function loadDag() { const j = await fetch('/api/explorer?dag=150').then(r => r.json()); if (!j.ok) throw new Error(j.error); dagBlocks = j.blocks; $('dag-eyebrow').textContent = `the newest ${j.blocks.length} blocks, one lane per miner, read ${stamp(j.now)}`; drawDag(); }
|
||||
window.addEventListener('resize', drawDag); document.addEventListener('igneum-theme', drawDag);
|
||||
const timer = setInterval(() => { loadBlocks().catch(() => {}); }, 5000);
|
||||
setInterval(() => { loadStats().catch(() => {}); }, 10000);
|
||||
loadBlocks().catch(e => { $('blocks').innerHTML = `<tr><td colspan="8" class="empty">${esc(e.message)}</td></tr>`; });
|
||||
loadStats().catch(() => {});
|
||||
const timerTx = setInterval(() => { loadTxs().catch(() => {}); }, 5000);
|
||||
setInterval(() => { loadStats().catch(() => {}); loadProving().catch(() => {}); }, 10000);
|
||||
setInterval(() => { loadDag().catch(() => {}); }, 4000);
|
||||
loadBlocks().catch(e => { $('blocks').innerHTML = `<tr><td colspan="10" class="empty">${esc(e.message)}</td></tr>`; });
|
||||
loadTxs().catch(e => { $('txs').innerHTML = `<tr><td colspan="7" class="empty">${esc(e.message)}</td></tr>`; });
|
||||
loadStats().catch(() => {}); loadProving().catch(() => {}); loadDag().catch(() => {});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
397
site/proving.html
Normal file
397
site/proving.html
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Igneum Devnet 3 proving</title>
|
||||
<meta name="description" content="Proving on Devnet 3: the shard plan of every chain block, who proved what, the payouts and the block-to-paid latency.">
|
||||
<link rel="canonical" href="https://igneum.network/proving">
|
||||
<meta name="theme-color" content="#0C0C0E">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Igneum">
|
||||
<meta property="og:title" content="Igneum Devnet 3 proving">
|
||||
<meta property="og:description" content="Proving on Devnet 3: the shard plan of every chain block, who proved what, the payouts and the block-to-paid latency.">
|
||||
<meta property="og:url" content="https://igneum.network/proving">
|
||||
<meta property="og:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta property="og:image:width" content="256">
|
||||
<meta property="og:image:height" content="256">
|
||||
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Igneum Devnet 3 proving">
|
||||
<meta name="twitter:description" content="Proving on Devnet 3: the shard plan of every chain block, who proved what, the payouts and the block-to-paid latency.">
|
||||
<meta name="twitter:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<!-- head:start -->
|
||||
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<link rel="stylesheet" href="/site.css">
|
||||
<style>
|
||||
/* Shared by every page: the head partial, injected by the build between the head markers. Edit the partial, not the page. */
|
||||
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. The tokens, type and components are in /site.css (docs/plans/site-ui-3.md). */
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
|
||||
</style>
|
||||
<script>
|
||||
/* theme before first paint: ?theme=light|dark for one view (the capture tool), else the stored choice under igneum-theme, else the system setting; ?motion=off marks the page data-motion=off so every .reveal is drawn at once (a headless render never scrolls) */
|
||||
(function(){var t=null;try{t=new URLSearchParams(location.search).get("theme");}catch(e){}if(t!=="light"&&t!=="dark"){try{t=localStorage.getItem("igneum-theme");}catch(e){t=null;}}if(t!=="light"&&t!=="dark")t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t);try{if(new URLSearchParams(location.search).get("motion")==="off")document.documentElement.setAttribute("data-motion","off");}catch(e){}})();
|
||||
</script>
|
||||
<!-- head:end -->
|
||||
<style>
|
||||
/* page block; tokens, type and components are in /site.css */
|
||||
/* the explorer pages share this block with block.html and address.html; same tokens as live.html */
|
||||
--card-pad:clamp(18px,3vw,28px);--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:24px;--gap-tile:12px;
|
||||
--fs-h1:clamp(32px,5.5vw,56px);--fs-h3:clamp(18px,2vw,22px);--fs-tile:clamp(20px,2.2vw,26px)}
|
||||
a:focus-visible,button:focus-visible,input:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
.mono{font-family:var(--f-mono)}
|
||||
h1,h2{font-family:var(--f-display);margin:0;line-height:1.08;text-wrap:balance}
|
||||
.head{padding:65px 0 45px;display:flex;flex-direction:column;gap:14px}
|
||||
.head p{color:var(--ink-2);max-width:67ch;font-size:18px;line-height:1.65}
|
||||
.search{display:flex;gap:10px;max-width:760px;margin-top:4px;align-items:center;padding:12px 16px;border:1px solid var(--line-2);border-radius:8px;background:var(--row)}
|
||||
.search input{flex:1;min-width:0;border:0;outline:none;background:transparent;color:var(--bone);font-size:14px}
|
||||
.search input::placeholder{color:var(--ash)}
|
||||
.btn.primary:hover{background:#FF6A2B;border-color:#FF6A2B;color:var(--ember-ink)}
|
||||
.msg{font-family:var(--f-mono);font-size:13px;color:var(--molten);min-height:18px}
|
||||
.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:24px;background:var(--row)}
|
||||
@media (max-width:700px){.strip{grid-template-columns:repeat(2,minmax(0,1fr))}.cell{border-right:1px solid var(--line)!important}.cell:nth-child(2n){border-right:0!important}.cell:nth-last-child(-n+4){border-bottom:1px solid var(--line)}.cell:nth-last-child(-n+2){border-bottom:0}}
|
||||
.cell{padding:22px 25px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:6px;min-width:0}.cell:nth-child(4n){border-right:0}.cell:nth-last-child(-n+4){border-bottom:0}
|
||||
.cell .k{font:10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);margin-bottom:5px}
|
||||
.cell .v{font:700 clamp(21px,2.2vw,29px)/1.1 var(--display);letter-spacing:-.04em;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cell .v small{font-family:var(--f-mono);font-weight:500;font-size:13px;color:var(--ash);margin-left:4px;letter-spacing:.04em}
|
||||
@media (max-width:700px){.cell .v{font-size:19px}}
|
||||
.cell .s{font:10px/1.5 var(--mono);color:var(--ash);text-wrap:balance;margin-top:4px}
|
||||
.cell.status .v{display:flex;align-items:center;gap:10px}
|
||||
.off .dot{background:var(--ash);animation:none}.off .cell.status .v{color:var(--ash)}.on .cell.status .v{color:var(--molten)}
|
||||
@media (prefers-reduced-motion:reduce){.dot{animation:none}}
|
||||
td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
|
||||
tbody tr:hover td{background:rgba(255,255,255,.025)}
|
||||
.id{font-family:var(--f-mono);color:var(--molten)}
|
||||
.hash{font-family:var(--f-mono)}
|
||||
.sw.chain{background:var(--ember);border-color:var(--blue)}.sw.blue{background:rgba(92,184,255,.7);border-color:var(--blue)}.sw.red{background:linear-gradient(45deg,transparent 44%,var(--ash) 44%,var(--ash) 56%,transparent 56%) center/8px 8px no-repeat}
|
||||
.chip{display:inline-block;font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:3px 8px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2);margin-right:6px}
|
||||
.chip.hot{border-color:var(--molten);color:var(--molten)}.chip.ok{border-color:var(--green);color:var(--green)}
|
||||
dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:8px 20px;margin:12px 0 0;font-size:14px}
|
||||
dt{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);padding-top:3px}
|
||||
dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
|
||||
@media (max-width:640px){dl{grid-template-columns:1fr;gap:2px 0}dd{margin-bottom:8px}}
|
||||
.two{display:grid;gap:var(--gap);grid-template-columns:1fr}
|
||||
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}.two .card{margin-top:0}}
|
||||
.two-wrap{margin-top:var(--gap)}
|
||||
main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--line);border-radius:16px;padding:25px;margin-top:24px}.card .viz-head h2{font-size:19px}.card .viz-head .eyebrow{margin:0}
|
||||
.badge{display:inline-flex;align-items:center;gap:6px;font-family:var(--f-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;padding:4px 9px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2);margin:0 6px 6px 0}
|
||||
.badge.ok{border-color:var(--green,#5CC48A);color:var(--green,#5CC48A)}.badge.hot{border-color:var(--molten);color:var(--molten)}.badge.bad{border-color:var(--excluded);color:var(--excluded)}.badge.wait{color:var(--ash)}
|
||||
.bar{display:inline-block;width:72px;height:7px;border-radius:4px;background:var(--line);vertical-align:middle;margin-right:7px;overflow:hidden}.bar i{display:block;height:100%;background:var(--green,#5CC48A)}.bar i.part{background:var(--molten)}
|
||||
.dagbox{position:relative;width:100%;height:320px;border:1px solid var(--line);border-radius:12px;background:var(--obsidian);overflow:hidden}.dagbox canvas{width:100%;height:100%;display:block;cursor:pointer}
|
||||
.dagtip{position:absolute;pointer-events:none;font:11px/1.5 var(--mono);color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:8px;padding:8px 10px;max-width:300px;display:none}
|
||||
.kv{color:var(--ash)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- nav:start -->
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<nav class="nav" aria-label="Main" data-nav-version="5">
|
||||
<div class="container nav-row">
|
||||
<a href="/" class="logo" aria-label="Igneum home">
|
||||
<svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
|
||||
<span class="word">IGNEUM</span>
|
||||
</a>
|
||||
<span class="devnet" id="nav-devnet" title="igneum-devnet-3, chain id 4463. Coins have no value. The chain may reset."><span class="dot off" id="foot-dot"></span><span id="foot-state">devnet</span></span>
|
||||
<div class="nav-groups" id="nav-groups" role="list">
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" data-active aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-build" data-group="build" aria-expanded="false" aria-controls="nav-panel-build" aria-haspopup="true">Build<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
</div>
|
||||
<div class="nav-controls">
|
||||
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
|
||||
<a href="/download" class="btn primary small cta" data-nav="download">Download</a>
|
||||
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-sheet" aria-label="Open the menu" aria-haspopup="true"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path class="bars" d="M4 6h16M4 12h16M4 18h16"/><path class="cross" d="m6 6 12 12M6 18 18 6"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panels" id="nav-panels">
|
||||
<div class="panel-wrap container">
|
||||
<div class="nav-panel" id="nav-panel-mine" data-group="mine" role="region" aria-labelledby="nav-btn-mine" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
|
||||
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
|
||||
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
|
||||
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
|
||||
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
|
||||
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
|
||||
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
|
||||
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
|
||||
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
|
||||
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
|
||||
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
|
||||
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-panel" id="nav-panel-build" data-group="build" role="region" aria-labelledby="nav-btn-build" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-sheet" id="nav-sheet" role="dialog" aria-modal="true" aria-label="Menu" hidden>
|
||||
<div class="sheet-scroll">
|
||||
<div class="sheet-group"><div class="sheet-head">Mine</div>
|
||||
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
|
||||
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
|
||||
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
|
||||
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
|
||||
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
|
||||
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
|
||||
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Network</div>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Learn</div>
|
||||
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
|
||||
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
|
||||
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
|
||||
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
|
||||
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
|
||||
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Build</div>
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</span></a>
|
||||
</div>
|
||||
<div class="sheet-foot">
|
||||
<a href="/download" class="btn primary big" data-nav="download">Download</a>
|
||||
<div class="sheet-social"><a href="https://discord.gg/igneum" target="_blank" rel="noopener">Discord</a><a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener">Source</a><a href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener">Reddit</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<script>
|
||||
(function(){
|
||||
// the slim bar (site-ui-5, 7 Oct 2026): three group buttons open one panel each; the burger opens the sheet on phones.
|
||||
// Keyboard: Enter or Space opens a group, Escape closes it and returns focus to its button, arrows move between the
|
||||
// group buttons, Tab walks the open panel's links, a click outside closes. The theme toggle flips data-theme and stores it.
|
||||
var n=document.querySelector('.nav');if(!n)return;
|
||||
var btns=[].slice.call(n.querySelectorAll('.group-btn')),panels=[].slice.call(n.querySelectorAll('.nav-panel')),wrap=n.querySelector('.panels');
|
||||
var burger=document.getElementById('nav-burger'),sheet=document.getElementById('nav-sheet');
|
||||
function panelOf(g){return panels.filter(function(p){return p.getAttribute('data-group')===g;})[0];}
|
||||
function closeAll(focusBack){var was=null;btns.forEach(function(b){if(b.getAttribute('aria-expanded')==='true')was=b;b.setAttribute('aria-expanded','false');});panels.forEach(function(p){p.hidden=true;});n.classList.remove('panel-open');if(focusBack&&was)was.focus();}
|
||||
function open(g){closeAll(false);var b=btns.filter(function(x){return x.getAttribute('data-group')===g;})[0],p=panelOf(g);if(!b||!p)return;b.setAttribute('aria-expanded','true');p.hidden=false;n.classList.add('panel-open');anchor(b,p);}
|
||||
// the panel sits under its button; a panel that would run past the container's right edge aligns its right edge to the button's right edge instead (never past the viewport)
|
||||
function anchor(b,p){var pw=n.querySelector('.panel-wrap');if(!pw)return;var cr=pw.getBoundingClientRect(),br=b.getBoundingClientRect(),w=p.offsetWidth,left=br.left-cr.left;if(left+w>cr.width)left=(br.right-cr.left)-w;if(left<0)left=0;p.style.setProperty('--panel-left',Math.round(left)+'px');}
|
||||
window.addEventListener('resize',function(){var o=btns.filter(function(x){return x.getAttribute('aria-expanded')==='true';})[0];if(o)anchor(o,panelOf(o.getAttribute('data-group')));}); btns.forEach(function(b,i){
|
||||
b.addEventListener('click',function(){var on=b.getAttribute('aria-expanded')==='true';if(on)closeAll(false);else open(b.getAttribute('data-group'));});
|
||||
b.addEventListener('keydown',function(e){
|
||||
var k=e.key;
|
||||
if(k==='ArrowRight'||k==='ArrowLeft'){e.preventDefault();var j=(i+(k==='ArrowRight'?1:btns.length-1))%btns.length;btns[j].focus();if(n.classList.contains('panel-open'))open(btns[j].getAttribute('data-group'));}
|
||||
else if(k==='ArrowDown'){e.preventDefault();open(b.getAttribute('data-group'));var f=panelOf(b.getAttribute('data-group')).querySelector('a');if(f)f.focus();}
|
||||
else if(k==='Escape'){closeAll(true);}
|
||||
});
|
||||
});
|
||||
if(wrap)wrap.addEventListener('keydown',function(e){if(e.key==='Escape'){e.stopPropagation();closeAll(true);}});
|
||||
document.addEventListener('click',function(e){if(!n.classList.contains('panel-open'))return;if(e.target.closest('.group-btn')||e.target.closest('.nav-panel'))return;closeAll(false);});
|
||||
document.addEventListener('focusin',function(e){if(!n.classList.contains('panel-open'))return;if(n.contains(e.target))return;closeAll(false);});
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&n.classList.contains('panel-open'))closeAll(true);});
|
||||
// the sheet on phones
|
||||
function setSheet(o){if(!burger||!sheet)return;n.classList.toggle('open',o);sheet.hidden=!o;burger.setAttribute('aria-expanded',o?'true':'false');burger.setAttribute('aria-label',o?'Close the menu':'Open the menu');document.documentElement.classList.toggle('nav-locked',o);if(o){var f=sheet.querySelector('a');if(f)f.focus();}}
|
||||
if(burger&&sheet){
|
||||
burger.addEventListener('click',function(){setSheet(!n.classList.contains('open'));});
|
||||
sheet.addEventListener('click',function(e){if(e.target.closest('a'))setSheet(false);});
|
||||
sheet.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'){setSheet(false);burger.focus();return;}
|
||||
if(e.key!=='Tab')return;var f=sheet.querySelectorAll('a,button');if(!f.length)return;var first=f[0],last=f[f.length-1];
|
||||
if(e.shiftKey&&document.activeElement===first){e.preventDefault();burger.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();burger.focus();}
|
||||
});
|
||||
burger.addEventListener('keydown',function(e){if(e.key==='Tab'&&!e.shiftKey&&n.classList.contains('open')){e.preventDefault();var f=sheet.querySelector('a');if(f)f.focus();}});
|
||||
var mq=window.matchMedia('(min-width:861px)');if(mq.addEventListener)mq.addEventListener('change',function(){setSheet(false);});
|
||||
}
|
||||
// the theme toggle
|
||||
var t=n.querySelector('[data-theme-toggle]');
|
||||
function paintToggle(){var dark=document.documentElement.getAttribute('data-theme')!=='light';t.setAttribute('aria-label',dark?'Switch to light theme':'Switch to dark theme');t.innerHTML=dark?'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg>':'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.7 13.3A8.5 8.5 0 0 1 10.7 3.3 8.5 8.5 0 1 0 20.7 13.3Z"/></svg>';}
|
||||
if(t){t.addEventListener('click',function(){var v=document.documentElement.getAttribute('data-theme')==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',v);try{localStorage.setItem('igneum-theme',v);}catch(e){}document.dispatchEvent(new CustomEvent('igneum-theme'));});paintToggle();document.addEventListener('igneum-theme',paintToggle);}
|
||||
})();
|
||||
</script>
|
||||
<!-- nav:end -->
|
||||
<main id="main" class="wrap">
|
||||
<div class="head">
|
||||
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><a href="/explorer">Explorer</a><span>/</span><span>Proving</span></div>
|
||||
<div class="eyebrow"><a href="/explorer">Devnet 3 explorer</a> · proving</div>
|
||||
<h1>Proving on Devnet 3<span class="accent">.</span></h1>
|
||||
<p>Every chain block's execution is split into shards. Miners prove them, the node verifies the proofs, and a later coinbase pays the prover. This page reads the observer's record of that cycle: the plan per block, who proved what, and how long a block waits to be paid.</p>
|
||||
<div class="msg" id="msg" aria-live="polite"></div>
|
||||
</div>
|
||||
<div class="strip" aria-live="polite">
|
||||
<div class="cell status"><div class="k">Proving</div><div class="v"><span class="dot"></span><span id="p-status">OFFLINE</span></div><div class="s" id="p-status-s">the node's proving layer</div></div>
|
||||
<div class="cell"><div class="k">Paid, 10 min</div><div class="v" id="p-paid10">0</div><div class="s" id="p-paid10-s">shards</div></div>
|
||||
<div class="cell"><div class="k">Paid, 24 h</div><div class="v" id="p-paid24">0</div><div class="s" id="p-paid24-s">shards, IGN to provers</div></div>
|
||||
<div class="cell"><div class="k">Open blocks</div><div class="v" id="p-open">0</div><div class="s" id="p-open-s">chain blocks with unpaid shards</div></div>
|
||||
<div class="cell"><div class="k">Provers, 24 h</div><div class="v" id="p-provers">0</div><div class="s" id="p-provers-s">vote keys paid for a shard</div></div>
|
||||
<div class="cell"><div class="k">Latency, 1 h</div><div class="v" id="p-lat">n/a</div><div class="s" id="p-lat-s">median DAA from block to payment</div></div>
|
||||
<div class="cell"><div class="k">Pool</div><div class="v" id="p-pool">0</div><div class="s" id="p-pool-s">records in the node's pool</div></div>
|
||||
<div class="cell"><div class="k">Shard budget</div><div class="v" id="p-budget">0<small>pgas</small></div><div class="s">proving gas per shard</div></div>
|
||||
</div>
|
||||
<div class="card" aria-label="Chain blocks and their shards">
|
||||
<div class="viz-head"><h2>Chain blocks</h2><div class="eyebrow" id="b-eyebrow">newest 60, every 5 s</div></div>
|
||||
<div class="tbl">
|
||||
<table>
|
||||
<thead><tr><th class="n">Number</th><th>Block</th><th class="n">DAA</th><th>Class</th><th>Miner</th><th class="n">Txs</th><th class="n">pgas</th><th>Shards paid</th><th>States</th><th class="n">Median lag, DAA</th><th>Time</th></tr></thead>
|
||||
<tbody id="blocks"><tr><td colspan="11" class="empty">Loading.</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="note">States count the block's shards as planned (the node published the plan), proving (a record is in the pool), verified (the node's verifier accepted the proof) and paid (a later coinbase carried the record). Lag is the DAA score of the paying block minus the block's own; Devnet 3 targets one DAA step per second.</p>
|
||||
</div>
|
||||
<div class="two two-wrap">
|
||||
<div class="card" aria-label="Provers">
|
||||
<div class="viz-head"><h2>Provers, 24 h</h2><div class="eyebrow" id="pr-eyebrow">by shards paid</div></div>
|
||||
<div class="tbl"><table><thead><tr><th>Prover</th><th class="n">Shards</th><th class="n">Paid, IGN</th><th class="n">Median lag</th><th class="n">Last hour</th><th>Last paid</th></tr></thead><tbody id="provers"></tbody></table></div>
|
||||
<p class="note">A prover is a vote key (its first 8 hex characters). A card runs several keys, so one machine can be several rows.</p>
|
||||
</div>
|
||||
<div class="card" aria-label="Latency">
|
||||
<div class="viz-head"><h2>Block to paid</h2><div class="eyebrow" id="l-eyebrow">the last hour</div></div>
|
||||
<dl id="latency"></dl>
|
||||
<p class="note">Measured on the shards paid in the window, from the proven block's DAA score to the paying block's. The 24-hour totals by state are below the hour.</p>
|
||||
<dl id="totals"></dl>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<div class="footer-grid">
|
||||
<div class="footer-brand">
|
||||
<a href="/" class="logo" aria-label="Igneum home"><svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span><span class="stop">.</span></a>
|
||||
<p>Mined by GPUs. Proven by fire.</p>
|
||||
<div class="social" aria-label="Igneum elsewhere">
|
||||
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" aria-label="Igneum source: the spec, the vectors and the issues"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M8.6 17.6 3 12l5.6-5.6 1.4 1.4L5.8 12l4.2 4.2zm6.8 0-1.4-1.4L18.2 12 14 7.8l1.4-1.4L21 12z"></path></svg></a>
|
||||
<a href="https://discord.gg/igneum" target="_blank" rel="noopener" aria-label="The Igneum Discord"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.6 5.3A17 17 0 0 0 15.4 4l-.2.4a15.6 15.6 0 0 1 3.9 1.9 13.6 13.6 0 0 0-14.2 0A15.6 15.6 0 0 1 8.8 4.4L8.6 4a17 17 0 0 0-4.2 1.3C1.8 9.2 1.1 13 1.4 16.7a17.1 17.1 0 0 0 5.2 2.6l1.1-1.8a10.8 10.8 0 0 1-1.7-.8l.4-.3a12.2 12.2 0 0 0 11.2 0l.4.3-1.7.8 1.1 1.8a17 17 0 0 0 5.2-2.6c.4-4.3-.7-8-3-11.4zM8.7 14.4c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1zm6.6 0c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1z"></path></svg></a>
|
||||
<a data-social="reddit" href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener" aria-label="Igneum on Reddit"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12.1a2.2 2.2 0 0 0-3.7-1.6 10.8 10.8 0 0 0-5.8-1.8l1-4.6 3.2.7a1.5 1.5 0 1 0 .2-.9l-3.6-.8a.5.5 0 0 0-.5.4l-1.1 5.2a10.8 10.8 0 0 0-5.9 1.8A2.2 2.2 0 1 0 3.4 14a4.3 4.3 0 0 0 0 .7c0 3.4 3.9 6.1 8.6 6.1s8.6-2.7 8.6-6.1a4.3 4.3 0 0 0 0-.7 2.2 2.2 0 0 0 1.4-1.9zM7 13.6a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 7 13.6zm8.6 4.1a5.7 5.7 0 0 1-3.6 1.1 5.7 5.7 0 0 1-3.6-1.1.4.4 0 0 1 .6-.6 4.9 4.9 0 0 0 3 .9 4.9 4.9 0 0 0 3-.9.4.4 0 0 1 .6.6zm-.3-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5z"></path></svg></a>
|
||||
</div>
|
||||
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</a><a href="/download#mac"><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</a><a href="/download#linux"><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</a><a href="/download#hive"><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</a></div>
|
||||
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" class="text-link">Specification and test vectors<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18 18 6M6 6h12v12"/></svg></a>
|
||||
</div>
|
||||
<div class="footer-column"><h4>Run</h4><div>
|
||||
<a href="/download">Download Ember</a>
|
||||
<a href="/miner">The miner</a>
|
||||
<a href="/app">The app</a>
|
||||
<a href="/wallet">The wallet</a>
|
||||
<a href="/miners">GPU bench table</a>
|
||||
<a href="/dev-fee">The dev fee</a>
|
||||
<a href="/faucet">Devnet faucet</a>
|
||||
<a href="/metamask">Add Igneum to MetaMask</a>
|
||||
</div></div>
|
||||
<div class="footer-column"><h4>Read</h4><div>
|
||||
<a href="/litepaper">Litepaper</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/bench">Engineering log</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
<a href="/claims">What Igneum does not claim</a>
|
||||
<a href="/randomx">Igneum vs RandomX</a>
|
||||
<a href="/provenance">Built on the shoulders</a>
|
||||
</div></div>
|
||||
<div class="footer-column"><h4>Explore</h4><div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/explorer">Explorer</a>
|
||||
<a href="/build">Build on Igneum</a>
|
||||
<a href="/swap">Swap</a>
|
||||
<a href="/grants">Grants</a>
|
||||
<a href="/journey">Journey</a>
|
||||
<a href="/download">Downloads: devnet build</a>
|
||||
<a href="https://discord.gg/igneum" target="_blank" rel="noopener">Community Discord</a>
|
||||
<a href="mailto:hello@igneum.network">hello@igneum.network</a>
|
||||
<a href="https://git.igneum.network/igneum-network/spec/issues" rel="noopener">An issue on the spec</a>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>Igneum Labs LTD · Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. Experimental software. Nothing on this site is an offer to sell anything.<br>Report a flaw by email or by an issue on the spec. Nobody from Igneum will ask for your seed. © 2026 Igneum · not an offer to sell anything</p>
|
||||
<div class="theme-group" role="group" aria-label="Theme"><button type="button" data-theme-set="system" aria-pressed="true">System</button><button type="button" data-theme-set="light" aria-pressed="false">Light</button><button type="button" data-theme-set="dark" aria-pressed="false">Dark</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// theme control: stores light or dark under igneum-theme; system clears it and follows the OS. The head applied the value before paint.
|
||||
var bs=document.querySelectorAll('[data-theme-set]');if(!bs.length)return;
|
||||
var media=window.matchMedia('(prefers-color-scheme: light)');
|
||||
function stored(){try{var t=localStorage.getItem('igneum-theme');return t==='light'||t==='dark'?t:'system';}catch(e){return 'system';}}
|
||||
function apply(){var c=stored();document.documentElement.setAttribute('data-theme',c==='system'?(media.matches?'light':'dark'):c);bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});document.dispatchEvent(new CustomEvent('igneum-theme'));}
|
||||
bs.forEach(function(b){b.addEventListener('click',function(){var v=b.getAttribute('data-theme-set');try{if(v==='system')localStorage.removeItem('igneum-theme');else localStorage.setItem('igneum-theme',v);}catch(e){}apply();});});
|
||||
if(media.addEventListener)media.addEventListener('change',function(){if(stored()==='system')apply();});
|
||||
document.addEventListener('igneum-theme',function(){var c=stored();bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});});
|
||||
bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===stored()?'true':'false');});
|
||||
// the network state word in the status bar: one read of the observer per page view
|
||||
var fs=document.getElementById('foot-state'),fd=document.getElementById('foot-dot');if(fs&&fd){fetch('/api/live?window=30',{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){var on=d&&d.ok&&d.state&&!d.state.stale;fs.textContent=on?'devnet live':'devnet, observer stale';fd.className='dot '+(on?'live':'off');}).catch(function(){fs.textContent='devnet';});}
|
||||
})();
|
||||
</script>
|
||||
<!-- footer:end -->
|
||||
<script type="module">
|
||||
import { ignFromWei, shortHash, rel, int } from '/lib/explorer.mjs';
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const dl = pairs => pairs.filter(p => p).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join('');
|
||||
const stamp = iso => iso ? new Date(iso).toISOString().slice(11, 19) + ' UTC' : '';
|
||||
function bar(s) { if (!s || !s.total) return '<span class="kv">no plan yet</span>'; const pct = Math.round(s.paid / s.total * 100); return `<span class="bar"><i class="${s.paid === s.total ? '' : 'part'}" style="width:${pct}%"></i></span>${s.paid} of ${s.total}`; }
|
||||
async function load() {
|
||||
const j = await fetch('/api/explorer?proving=1').then(r => r.json()); if (!j.ok) throw new Error(j.error);
|
||||
const now = new Date(j.now).getTime(), p = j.proving || {}, l = j.latency_1h || {}, t = j.totals_24h || {};
|
||||
const on = p.supported !== false && !j.stale;
|
||||
document.body.classList.toggle('on', on); document.body.classList.toggle('off', !on);
|
||||
$('p-status').textContent = j.stale ? 'OFFLINE' : (p.supported === false ? 'NO LAYER' : (p.active ? 'ACTIVE' : 'INACTIVE'));
|
||||
$('p-status-s').textContent = j.stale ? 'observer stale' : `read ${stamp(j.now)}, tip DAA ${int(p.tip_daa)}${p.paused_since_ms ? ', paused' : ''}`;
|
||||
$('p-paid10').textContent = int(p.shards_paid_10m ?? 0); $('p-paid10-s').textContent = `shards; ${int(p.blocks_fully_proven_10m ?? 0)} of ${int(p.blocks_10m ?? 0)} blocks of the window fully proven`;
|
||||
$('p-paid24').textContent = int(t.paid ? t.paid.shards : 0); $('p-paid24-s').textContent = `shards, ${ignFromWei(t.paid ? t.paid.payout_wei : '0', 2)} IGN to provers`;
|
||||
$('p-open').textContent = int(p.open_blocks ?? 0); $('p-open-s').textContent = `chain blocks with unpaid shards, ${int(p.pending_plans ?? 0)} plans not read yet`;
|
||||
$('p-provers').textContent = int(j.provers.length); $('p-provers-s').textContent = `vote keys paid for a shard; ${int(p.provers_10m ?? 0)} in the last 10 min`;
|
||||
$('p-lat').textContent = l.paid ? int(l.p50_daa) : 'n/a'; $('p-lat-s').textContent = l.paid ? `median DAA from block to payment over ${int(l.paid)} shards, p90 ${int(l.p90_daa)}` : 'no shard paid in the last hour';
|
||||
const pool = p.pool || {}; $('p-pool').textContent = int(pool.entries ?? 0); $('p-pool-s').textContent = `records in the node's pool: ${int(pool.pending ?? 0)} pending, ${int(pool.verified ?? 0)} verified, ${int(pool.failed ?? 0)} failed`;
|
||||
$('p-budget').innerHTML = esc(int(p.shard_budget_pgas ?? 0)) + '<small>pgas</small>';
|
||||
$('b-eyebrow').textContent = `newest ${j.blocks.length}, every 5 s, read ${stamp(j.now)}`;
|
||||
$('blocks').innerHTML = j.blocks.length ? j.blocks.map(b => `<tr data-href="/block/${b.hash}"><td class="n">${int(b.number)}</td><td><a class="hash" href="/block/${b.hash}">${esc(shortHash(b.hash, 10, 6))}</a></td><td class="n">${int(b.daa)}</td><td><span class="chip">${esc(b.class)}</span></td><td>${b.evm_miner ? `<a class="hash" href="/address/${b.evm_miner}">${esc(shortHash(b.evm_miner, 6, 4))}</a>` : ''} <span class="id" style="font-size:12px">${esc(b.miner_id || '')}</span></td><td class="n">${int(b.tx_count)}</td><td class="n">${int(b.pgas)}</td><td>${bar(b.shards)}</td><td class="kv" style="font-size:12px">${b.shards && b.shards.total ? `${b.shards.planned} planned, ${b.shards.proving} proving, ${b.shards.verified} verified, ${b.shards.paid} paid` : ''}</td><td class="n">${b.shards && b.shards.median_lag !== null && b.shards.median_lag !== undefined ? int(b.shards.median_lag) : ''}</td><td title="${new Date(b.ts).toISOString()}">${rel(now - b.ts)}</td></tr>`).join('') : '<tr><td colspan="11" class="empty">No chain block in the last 24 hours.</td></tr>';
|
||||
$('pr-eyebrow').textContent = `${int(j.provers.length)} keys, by shards paid`;
|
||||
$('provers').innerHTML = j.provers.length ? j.provers.map(x => `<tr><td class="id">${esc(x.id)}</td><td class="n">${int(x.shards_24h)}</td><td class="n">${ignFromWei(x.payout_wei_24h, 2)}</td><td class="n">${x.median_lag_24h === null ? '' : int(x.median_lag_24h)}</td><td class="n">${int(x.shards_1h)}</td><td title="${x.last_paid ? new Date(x.last_paid).toISOString() : ''}">${x.last_paid ? rel(now - new Date(x.last_paid).getTime()) : ''}</td></tr>`).join('') : '<tr><td colspan="6" class="empty">No shard paid in the last 24 hours.</td></tr>';
|
||||
$('l-eyebrow').textContent = `the last hour, read ${stamp(j.now)}`;
|
||||
$('latency').innerHTML = l.paid ? dl([['Shards paid', int(l.paid)], ['Median', `${int(l.p50_daa)} DAA`], ['p90', `${int(l.p90_daa)} DAA`], ['Fastest', `${int(l.min_daa)} DAA`], ['Slowest', `${int(l.max_daa)} DAA`]]) : '<div class="empty">No shard paid in the last hour.</div>';
|
||||
$('totals').innerHTML = dl(Object.entries(t).sort().map(([k, v]) => [`${k}, 24 h`, `${int(v.shards)} shards${k === 'paid' ? `, ${ignFromWei(v.payout_wei, 2)} IGN` : ''}`]));
|
||||
}
|
||||
$('blocks').addEventListener('click', e => { const tr = e.target.closest('tr[data-href]'); if (tr && !e.target.closest('a')) location.href = tr.dataset.href; });
|
||||
setInterval(() => { load().catch(() => {}); }, 5000);
|
||||
load().catch(e => { $('msg').textContent = 'The API did not answer: ' + e.message; });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -6,7 +6,8 @@
|
|||
<url><loc>https://igneum.network/app</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.8</priority></url>
|
||||
<url><loc>https://igneum.network/wallet</loc><lastmod>2026-10-07</lastmod><changefreq>monthly</changefreq><priority>0.6</priority></url>
|
||||
<url><loc>https://igneum.network/live</loc><lastmod>2026-10-07</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
|
||||
<url><loc>https://igneum.network/explorer</loc><lastmod>2026-10-07</lastmod><changefreq>daily</changefreq><priority>0.5</priority></url>
|
||||
<url><loc>https://igneum.network/explorer</loc><lastmod>2026-10-08</lastmod><changefreq>daily</changefreq><priority>0.5</priority></url>
|
||||
<url><loc>https://igneum.network/proving</loc><lastmod>2026-10-08</lastmod><changefreq>daily</changefreq><priority>0.4</priority></url>
|
||||
<url><loc>https://igneum.network/ledger</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
|
||||
<url><loc>https://igneum.network/bench</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
|
||||
<url><loc>https://igneum.network/evidence</loc><lastmod>2026-10-07</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>
|
||||
|
|
|
|||
400
site/tx.html
Normal file
400
site/tx.html
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Igneum Devnet 3 transaction</title>
|
||||
<meta name="description" content="One Devnet 3 transaction: status, value, fees and their split, the block that executed it, its proof state and finality, its logs.">
|
||||
<link rel="canonical" href="https://igneum.network/tx">
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="theme-color" content="#0C0C0E">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:site_name" content="Igneum">
|
||||
<meta property="og:title" content="Igneum Devnet 3 transaction">
|
||||
<meta property="og:description" content="One Devnet 3 transaction: status, value, fees and their split, the block that executed it, its proof state and finality, its logs.">
|
||||
<meta property="og:url" content="https://igneum.network/tx">
|
||||
<meta property="og:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta property="og:image:width" content="256">
|
||||
<meta property="og:image:height" content="256">
|
||||
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="Igneum Devnet 3 transaction">
|
||||
<meta name="twitter:description" content="One Devnet 3 transaction: status, value, fees and their split, the block that executed it, its proof state and finality, its logs.">
|
||||
<meta name="twitter:image" content="https://igneum.network/og-small.png?v=3">
|
||||
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
||||
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
||||
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<!-- head:start -->
|
||||
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<link rel="stylesheet" href="/site.css">
|
||||
<style>
|
||||
/* Shared by every page: the head partial, injected by the build between the head markers. Edit the partial, not the page. */
|
||||
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. The tokens, type and components are in /site.css (docs/plans/site-ui-3.md). */
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
||||
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
|
||||
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
|
||||
</style>
|
||||
<script>
|
||||
/* theme before first paint: ?theme=light|dark for one view (the capture tool), else the stored choice under igneum-theme, else the system setting; ?motion=off marks the page data-motion=off so every .reveal is drawn at once (a headless render never scrolls) */
|
||||
(function(){var t=null;try{t=new URLSearchParams(location.search).get("theme");}catch(e){}if(t!=="light"&&t!=="dark"){try{t=localStorage.getItem("igneum-theme");}catch(e){t=null;}}if(t!=="light"&&t!=="dark")t=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches?"light":"dark";document.documentElement.setAttribute("data-theme",t);try{if(new URLSearchParams(location.search).get("motion")==="off")document.documentElement.setAttribute("data-motion","off");}catch(e){}})();
|
||||
</script>
|
||||
<!-- head:end -->
|
||||
<style>
|
||||
/* page block; tokens, type and components are in /site.css */
|
||||
/* the explorer pages share this block with block.html and address.html; same tokens as live.html */
|
||||
--card-pad:clamp(18px,3vw,28px);--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:24px;--gap-tile:12px;
|
||||
--fs-h1:clamp(32px,5.5vw,56px);--fs-h3:clamp(18px,2vw,22px);--fs-tile:clamp(20px,2.2vw,26px)}
|
||||
a:focus-visible,button:focus-visible,input:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
.mono{font-family:var(--f-mono)}
|
||||
h1,h2{font-family:var(--f-display);margin:0;line-height:1.08;text-wrap:balance}
|
||||
.head{padding:65px 0 45px;display:flex;flex-direction:column;gap:14px}
|
||||
.head p{color:var(--ink-2);max-width:67ch;font-size:18px;line-height:1.65}
|
||||
.search{display:flex;gap:10px;max-width:760px;margin-top:4px;align-items:center;padding:12px 16px;border:1px solid var(--line-2);border-radius:8px;background:var(--row)}
|
||||
.search input{flex:1;min-width:0;border:0;outline:none;background:transparent;color:var(--bone);font-size:14px}
|
||||
.search input::placeholder{color:var(--ash)}
|
||||
.btn.primary:hover{background:#FF6A2B;border-color:#FF6A2B;color:var(--ember-ink)}
|
||||
.msg{font-family:var(--f-mono);font-size:13px;color:var(--molten);min-height:18px}
|
||||
.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:24px;background:var(--row)}
|
||||
@media (max-width:700px){.strip{grid-template-columns:repeat(2,minmax(0,1fr))}.cell{border-right:1px solid var(--line)!important}.cell:nth-child(2n){border-right:0!important}.cell:nth-last-child(-n+4){border-bottom:1px solid var(--line)}.cell:nth-last-child(-n+2){border-bottom:0}}
|
||||
.cell{padding:22px 25px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:6px;min-width:0}.cell:nth-child(4n){border-right:0}.cell:nth-last-child(-n+4){border-bottom:0}
|
||||
.cell .k{font:10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ash);margin-bottom:5px}
|
||||
.cell .v{font:700 clamp(21px,2.2vw,29px)/1.1 var(--display);letter-spacing:-.04em;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.cell .v small{font-family:var(--f-mono);font-weight:500;font-size:13px;color:var(--ash);margin-left:4px;letter-spacing:.04em}
|
||||
@media (max-width:700px){.cell .v{font-size:19px}}
|
||||
.cell .s{font:10px/1.5 var(--mono);color:var(--ash);text-wrap:balance;margin-top:4px}
|
||||
.cell.status .v{display:flex;align-items:center;gap:10px}
|
||||
.off .dot{background:var(--ash);animation:none}.off .cell.status .v{color:var(--ash)}.on .cell.status .v{color:var(--molten)}
|
||||
@media (prefers-reduced-motion:reduce){.dot{animation:none}}
|
||||
td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
|
||||
tbody tr:hover td{background:rgba(255,255,255,.025)}
|
||||
.id{font-family:var(--f-mono);color:var(--molten)}
|
||||
.hash{font-family:var(--f-mono)}
|
||||
.sw.chain{background:var(--ember);border-color:var(--blue)}.sw.blue{background:rgba(92,184,255,.7);border-color:var(--blue)}.sw.red{background:linear-gradient(45deg,transparent 44%,var(--ash) 44%,var(--ash) 56%,transparent 56%) center/8px 8px no-repeat}
|
||||
.chip{display:inline-block;font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:3px 8px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2);margin-right:6px}
|
||||
.chip.hot{border-color:var(--molten);color:var(--molten)}.chip.ok{border-color:var(--green);color:var(--green)}
|
||||
dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:8px 20px;margin:12px 0 0;font-size:14px}
|
||||
dt{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);padding-top:3px}
|
||||
dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
|
||||
@media (max-width:640px){dl{grid-template-columns:1fr;gap:2px 0}dd{margin-bottom:8px}}
|
||||
.two{display:grid;gap:var(--gap);grid-template-columns:1fr}
|
||||
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}.two .card{margin-top:0}}
|
||||
.two-wrap{margin-top:var(--gap)}
|
||||
main{padding-bottom:100px}.card{background:var(--row);border:1px solid var(--line);border-radius:16px;padding:25px;margin-top:24px}.card .viz-head h2{font-size:19px}.card .viz-head .eyebrow{margin:0}
|
||||
.badge{display:inline-flex;align-items:center;gap:6px;font-family:var(--f-mono);font-size:11px;letter-spacing:.06em;text-transform:uppercase;padding:4px 9px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2);margin:0 6px 6px 0}
|
||||
.badge.ok{border-color:var(--green,#5CC48A);color:var(--green,#5CC48A)}.badge.hot{border-color:var(--molten);color:var(--molten)}.badge.bad{border-color:var(--excluded);color:var(--excluded)}.badge.wait{color:var(--ash)}
|
||||
.bar{display:inline-block;width:72px;height:7px;border-radius:4px;background:var(--line);vertical-align:middle;margin-right:7px;overflow:hidden}.bar i{display:block;height:100%;background:var(--green,#5CC48A)}.bar i.part{background:var(--molten)}
|
||||
.dagbox{position:relative;width:100%;height:320px;border:1px solid var(--line);border-radius:12px;background:var(--obsidian);overflow:hidden}.dagbox canvas{width:100%;height:100%;display:block;cursor:pointer}
|
||||
.dagtip{position:absolute;pointer-events:none;font:11px/1.5 var(--mono);color:var(--bone);background:var(--graphite);border:1px solid var(--line-2);border-radius:8px;padding:8px 10px;max-width:300px;display:none}
|
||||
.kv{color:var(--ash)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- nav:start -->
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<nav class="nav" aria-label="Main" data-nav-version="5">
|
||||
<div class="container nav-row">
|
||||
<a href="/" class="logo" aria-label="Igneum home">
|
||||
<svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg>
|
||||
<span class="word">IGNEUM</span>
|
||||
</a>
|
||||
<span class="devnet" id="nav-devnet" title="igneum-devnet-3, chain id 4463. Coins have no value. The chain may reset."><span class="dot off" id="foot-dot"></span><span id="foot-state">devnet</span></span>
|
||||
<div class="nav-groups" id="nav-groups" role="list">
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-mine" data-group="mine" aria-expanded="false" aria-controls="nav-panel-mine" aria-haspopup="true">Mine<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-network" data-group="network" data-active aria-expanded="false" aria-controls="nav-panel-network" aria-haspopup="true">Network<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-learn" data-group="learn" aria-expanded="false" aria-controls="nav-panel-learn" aria-haspopup="true">Learn<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
<div class="nav-group" role="listitem"><button type="button" class="group-btn" id="nav-btn-build" data-group="build" aria-expanded="false" aria-controls="nav-panel-build" aria-haspopup="true">Build<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m7 10 5 5 5-5"/></svg></button></div>
|
||||
</div>
|
||||
<div class="nav-controls">
|
||||
<button class="btn icon-btn" type="button" data-theme-toggle aria-label="Switch to light theme"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg></button>
|
||||
<a href="/download" class="btn primary small cta" data-nav="download">Download</a>
|
||||
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-sheet" aria-label="Open the menu" aria-haspopup="true"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path class="bars" d="M4 6h16M4 12h16M4 18h16"/><path class="cross" d="m6 6 12 12M6 18 18 6"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panels" id="nav-panels">
|
||||
<div class="panel-wrap container">
|
||||
<div class="nav-panel" id="nav-panel-mine" data-group="mine" role="region" aria-labelledby="nav-btn-mine" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
|
||||
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
|
||||
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
|
||||
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
|
||||
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
|
||||
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
|
||||
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-panel" id="nav-panel-network" data-group="network" role="region" aria-labelledby="nav-btn-network" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-panel" id="nav-panel-learn" data-group="learn" role="region" aria-labelledby="nav-btn-learn" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
|
||||
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
|
||||
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
|
||||
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
|
||||
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
|
||||
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-panel" id="nav-panel-build" data-group="build" role="region" aria-labelledby="nav-btn-build" hidden>
|
||||
<div class="panel-list">
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-sheet" id="nav-sheet" role="dialog" aria-modal="true" aria-label="Menu" hidden>
|
||||
<div class="sheet-scroll">
|
||||
<div class="sheet-group"><div class="sheet-head">Mine</div>
|
||||
<a href="/miner" data-nav="miner"><b>Ember, the miner</b><span>One click installs the node, the miner and the prover.</span></a>
|
||||
<a href="/download" data-nav="download"><b>Download</b><span>Windows, macOS, Linux and HiveOS.</span></a>
|
||||
<a href="/app" data-nav="app"><b>The app</b><span>What you see while the card mines.</span></a>
|
||||
<a href="/wallet" data-nav="wallet"><b>The wallet</b><span>Your coins. Final means final.</span></a>
|
||||
<a href="/miners" data-nav="miners"><b>GPU bench table</b><span>Measured rates, card by card.</span></a>
|
||||
<a href="/dev-fee" data-nav="dev-fee"><b>The dev fee</b><span>The optional one, in full view, off with one flag.</span></a>
|
||||
<a href="/metamask" data-nav="metamask"><b>Add to MetaMask</b><span>Igneum as a network in your wallet.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Network</div>
|
||||
<a href="/live" data-nav="live"><b>Live devnet</b><span>The chain as it grows, one lane per miner.</span></a>
|
||||
<a href="/explorer" data-nav="explorer" aria-current="page"><b>Explorer</b><span>Look a block or an address up.</span></a>
|
||||
<a href="/journey" data-nav="journey"><b>The journey</b><span>Six phases, four gates, the log behind them.</span></a>
|
||||
<a href="/bench" data-nav="bench"><b>Engineering log</b><span>Every measurement, with the commands that made it.</span></a>
|
||||
<a href="/evidence" data-nav="evidence"><b>Evidence</b><span>Every claim and how far it is tested.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Learn</div>
|
||||
<a href="/litepaper" data-nav="litepaper"><b>Litepaper</b><span>The design, as published.</span></a>
|
||||
<a href="/income" data-nav="income"><b>Income per tier</b><span>IGN a day per card at three network sizes, and the electricity.</span></a>
|
||||
<a href="/ledger" data-nav="ledger"><b>Ledger</b><span>Every criticism, answered or conceded.</span></a>
|
||||
<a href="/claims" data-nav="claims"><b>What Igneum does not claim</b><span>The limits, stated first.</span></a>
|
||||
<a href="/randomx" data-nav="randomx"><b>Igneum vs RandomX</b><span>What was kept and what was rebuilt for GPUs.</span></a>
|
||||
<a href="/provenance" data-nav="provenance"><b>Built on the shoulders</b><span>Every borrowed part, credited.</span></a>
|
||||
</div>
|
||||
<div class="sheet-group"><div class="sheet-head">Build</div>
|
||||
<a href="/build" data-nav="build"><b>Build on Igneum</b><span>Chain ids, RPC, a contract in five minutes.</span></a>
|
||||
<a href="/faucet" data-nav="faucet"><b>Devnet faucet</b><span>10 IGN of devnet coin, once a day.</span></a>
|
||||
<a href="/swap" data-nav="swap"><b>Swap</b><span>Test tokens on Devnet 3; every swap is a proven block.</span></a>
|
||||
<a href="/grants" data-nav="grants"><b>Grants</b><span>Tooling, apps, infrastructure, research.</span></a>
|
||||
</div>
|
||||
<div class="sheet-foot">
|
||||
<a href="/download" class="btn primary big" data-nav="download">Download</a>
|
||||
<div class="sheet-social"><a href="https://discord.gg/igneum" target="_blank" rel="noopener">Discord</a><a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener">Source</a><a href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener">Reddit</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<script>
|
||||
(function(){
|
||||
// the slim bar (site-ui-5, 7 Oct 2026): three group buttons open one panel each; the burger opens the sheet on phones.
|
||||
// Keyboard: Enter or Space opens a group, Escape closes it and returns focus to its button, arrows move between the
|
||||
// group buttons, Tab walks the open panel's links, a click outside closes. The theme toggle flips data-theme and stores it.
|
||||
var n=document.querySelector('.nav');if(!n)return;
|
||||
var btns=[].slice.call(n.querySelectorAll('.group-btn')),panels=[].slice.call(n.querySelectorAll('.nav-panel')),wrap=n.querySelector('.panels');
|
||||
var burger=document.getElementById('nav-burger'),sheet=document.getElementById('nav-sheet');
|
||||
function panelOf(g){return panels.filter(function(p){return p.getAttribute('data-group')===g;})[0];}
|
||||
function closeAll(focusBack){var was=null;btns.forEach(function(b){if(b.getAttribute('aria-expanded')==='true')was=b;b.setAttribute('aria-expanded','false');});panels.forEach(function(p){p.hidden=true;});n.classList.remove('panel-open');if(focusBack&&was)was.focus();}
|
||||
function open(g){closeAll(false);var b=btns.filter(function(x){return x.getAttribute('data-group')===g;})[0],p=panelOf(g);if(!b||!p)return;b.setAttribute('aria-expanded','true');p.hidden=false;n.classList.add('panel-open');anchor(b,p);}
|
||||
// the panel sits under its button; a panel that would run past the container's right edge aligns its right edge to the button's right edge instead (never past the viewport)
|
||||
function anchor(b,p){var pw=n.querySelector('.panel-wrap');if(!pw)return;var cr=pw.getBoundingClientRect(),br=b.getBoundingClientRect(),w=p.offsetWidth,left=br.left-cr.left;if(left+w>cr.width)left=(br.right-cr.left)-w;if(left<0)left=0;p.style.setProperty('--panel-left',Math.round(left)+'px');}
|
||||
window.addEventListener('resize',function(){var o=btns.filter(function(x){return x.getAttribute('aria-expanded')==='true';})[0];if(o)anchor(o,panelOf(o.getAttribute('data-group')));}); btns.forEach(function(b,i){
|
||||
b.addEventListener('click',function(){var on=b.getAttribute('aria-expanded')==='true';if(on)closeAll(false);else open(b.getAttribute('data-group'));});
|
||||
b.addEventListener('keydown',function(e){
|
||||
var k=e.key;
|
||||
if(k==='ArrowRight'||k==='ArrowLeft'){e.preventDefault();var j=(i+(k==='ArrowRight'?1:btns.length-1))%btns.length;btns[j].focus();if(n.classList.contains('panel-open'))open(btns[j].getAttribute('data-group'));}
|
||||
else if(k==='ArrowDown'){e.preventDefault();open(b.getAttribute('data-group'));var f=panelOf(b.getAttribute('data-group')).querySelector('a');if(f)f.focus();}
|
||||
else if(k==='Escape'){closeAll(true);}
|
||||
});
|
||||
});
|
||||
if(wrap)wrap.addEventListener('keydown',function(e){if(e.key==='Escape'){e.stopPropagation();closeAll(true);}});
|
||||
document.addEventListener('click',function(e){if(!n.classList.contains('panel-open'))return;if(e.target.closest('.group-btn')||e.target.closest('.nav-panel'))return;closeAll(false);});
|
||||
document.addEventListener('focusin',function(e){if(!n.classList.contains('panel-open'))return;if(n.contains(e.target))return;closeAll(false);});
|
||||
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&n.classList.contains('panel-open'))closeAll(true);});
|
||||
// the sheet on phones
|
||||
function setSheet(o){if(!burger||!sheet)return;n.classList.toggle('open',o);sheet.hidden=!o;burger.setAttribute('aria-expanded',o?'true':'false');burger.setAttribute('aria-label',o?'Close the menu':'Open the menu');document.documentElement.classList.toggle('nav-locked',o);if(o){var f=sheet.querySelector('a');if(f)f.focus();}}
|
||||
if(burger&&sheet){
|
||||
burger.addEventListener('click',function(){setSheet(!n.classList.contains('open'));});
|
||||
sheet.addEventListener('click',function(e){if(e.target.closest('a'))setSheet(false);});
|
||||
sheet.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'){setSheet(false);burger.focus();return;}
|
||||
if(e.key!=='Tab')return;var f=sheet.querySelectorAll('a,button');if(!f.length)return;var first=f[0],last=f[f.length-1];
|
||||
if(e.shiftKey&&document.activeElement===first){e.preventDefault();burger.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();burger.focus();}
|
||||
});
|
||||
burger.addEventListener('keydown',function(e){if(e.key==='Tab'&&!e.shiftKey&&n.classList.contains('open')){e.preventDefault();var f=sheet.querySelector('a');if(f)f.focus();}});
|
||||
var mq=window.matchMedia('(min-width:861px)');if(mq.addEventListener)mq.addEventListener('change',function(){setSheet(false);});
|
||||
}
|
||||
// the theme toggle
|
||||
var t=n.querySelector('[data-theme-toggle]');
|
||||
function paintToggle(){var dark=document.documentElement.getAttribute('data-theme')!=='light';t.setAttribute('aria-label',dark?'Switch to light theme':'Switch to dark theme');t.innerHTML=dark?'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1 1m12 12 1 1M5 19l1-1M18 6l1-1"/></svg>':'<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.7 13.3A8.5 8.5 0 0 1 10.7 3.3 8.5 8.5 0 1 0 20.7 13.3Z"/></svg>';}
|
||||
if(t){t.addEventListener('click',function(){var v=document.documentElement.getAttribute('data-theme')==='light'?'dark':'light';document.documentElement.setAttribute('data-theme',v);try{localStorage.setItem('igneum-theme',v);}catch(e){}document.dispatchEvent(new CustomEvent('igneum-theme'));});paintToggle();document.addEventListener('igneum-theme',paintToggle);}
|
||||
})();
|
||||
</script>
|
||||
<!-- nav:end -->
|
||||
<main id="main" class="wrap">
|
||||
<div class="head">
|
||||
<div class="breadcrumb"><a href="/">Igneum</a><span>/</span><a href="/explorer">Explorer</a><span>/</span><span>Transaction</span></div>
|
||||
<div class="eyebrow"><a href="/explorer">Devnet 3 explorer</a> · transaction</div>
|
||||
<h1 class="hash" id="title">Transaction</h1>
|
||||
<div id="chips"></div>
|
||||
<div class="msg" id="msg" aria-live="polite"></div>
|
||||
</div>
|
||||
<div id="body" hidden>
|
||||
<div class="two two-wrap">
|
||||
<div class="card"><div class="viz-head"><h2>Summary</h2></div><dl id="summary"></dl></div>
|
||||
<div class="card"><div class="viz-head"><h2>Fees and proving</h2><div class="eyebrow">the receipt's Igneum section</div></div><dl id="fees"></dl><p class="note">Read from the observer node, the chain the finality certificates follow. Both base fees burn in full. The priority fee splits 80/20 between the including miner and the proving pool. Proving gas (pgas) is what the shard that proves this transaction costs; the proving base fee prices it.</p></div>
|
||||
</div>
|
||||
<div class="card"><div class="viz-head"><h2>Block and proof</h2><div class="eyebrow" id="blk-eyebrow"></div></div><dl id="blk"></dl></div>
|
||||
<div class="card"><div class="viz-head"><h2>Logs</h2><div class="eyebrow" id="log-n"></div></div><div class="tbl" id="logs"></div></div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<div class="footer-grid">
|
||||
<div class="footer-brand">
|
||||
<a href="/" class="logo" aria-label="Igneum home"><svg class="brand-mark" viewBox="0 0 1024 1024" aria-hidden="true"><rect width="1024" height="1024" rx="160" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></g></svg><span class="word">IGNEUM</span><span class="stop">.</span></a>
|
||||
<p>Mined by GPUs. Proven by fire.</p>
|
||||
<div class="social" aria-label="Igneum elsewhere">
|
||||
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" aria-label="Igneum source: the spec, the vectors and the issues"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M8.6 17.6 3 12l5.6-5.6 1.4 1.4L5.8 12l4.2 4.2zm6.8 0-1.4-1.4L18.2 12 14 7.8l1.4-1.4L21 12z"></path></svg></a>
|
||||
<a href="https://discord.gg/igneum" target="_blank" rel="noopener" aria-label="The Igneum Discord"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M19.6 5.3A17 17 0 0 0 15.4 4l-.2.4a15.6 15.6 0 0 1 3.9 1.9 13.6 13.6 0 0 0-14.2 0A15.6 15.6 0 0 1 8.8 4.4L8.6 4a17 17 0 0 0-4.2 1.3C1.8 9.2 1.1 13 1.4 16.7a17.1 17.1 0 0 0 5.2 2.6l1.1-1.8a10.8 10.8 0 0 1-1.7-.8l.4-.3a12.2 12.2 0 0 0 11.2 0l.4.3-1.7.8 1.1 1.8a17 17 0 0 0 5.2-2.6c.4-4.3-.7-8-3-11.4zM8.7 14.4c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1zm6.6 0c-1 0-1.8-.9-1.8-2.1s.8-2.1 1.8-2.1 1.9 1 1.8 2.1c0 1.2-.8 2.1-1.8 2.1z"></path></svg></a>
|
||||
<a data-social="reddit" href="https://www.reddit.com/user/Igneum_network/" target="_blank" rel="noopener" aria-label="Igneum on Reddit"><svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M22 12.1a2.2 2.2 0 0 0-3.7-1.6 10.8 10.8 0 0 0-5.8-1.8l1-4.6 3.2.7a1.5 1.5 0 1 0 .2-.9l-3.6-.8a.5.5 0 0 0-.5.4l-1.1 5.2a10.8 10.8 0 0 0-5.9 1.8A2.2 2.2 0 1 0 3.4 14a4.3 4.3 0 0 0 0 .7c0 3.4 3.9 6.1 8.6 6.1s8.6-2.7 8.6-6.1a4.3 4.3 0 0 0 0-.7 2.2 2.2 0 0 0 1.4-1.9zM7 13.6a1.5 1.5 0 1 1 1.5 1.5A1.5 1.5 0 0 1 7 13.6zm8.6 4.1a5.7 5.7 0 0 1-3.6 1.1 5.7 5.7 0 0 1-3.6-1.1.4.4 0 0 1 .6-.6 4.9 4.9 0 0 0 3 .9 4.9 4.9 0 0 0 3-.9.4.4 0 0 1 .6.6zm-.3-2.6a1.5 1.5 0 1 1 1.5-1.5 1.5 1.5 0 0 1-1.5 1.5z"></path></svg></a>
|
||||
</div>
|
||||
<div class="footer-dl" aria-label="Download Ember"><a href="/download#windows"><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</a><a href="/download#mac"><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</a><a href="/download#linux"><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</a><a href="/download#hive"><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</a></div>
|
||||
<a href="https://git.igneum.network/igneum-network/spec" target="_blank" rel="noopener" class="text-link">Specification and test vectors<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 18 18 6M6 6h12v12"/></svg></a>
|
||||
</div>
|
||||
<div class="footer-column"><h4>Run</h4><div>
|
||||
<a href="/download">Download Ember</a>
|
||||
<a href="/miner">The miner</a>
|
||||
<a href="/app">The app</a>
|
||||
<a href="/wallet">The wallet</a>
|
||||
<a href="/miners">GPU bench table</a>
|
||||
<a href="/dev-fee">The dev fee</a>
|
||||
<a href="/faucet">Devnet faucet</a>
|
||||
<a href="/metamask">Add Igneum to MetaMask</a>
|
||||
</div></div>
|
||||
<div class="footer-column"><h4>Read</h4><div>
|
||||
<a href="/litepaper">Litepaper</a>
|
||||
<a href="/evidence">Evidence</a>
|
||||
<a href="/bench">Engineering log</a>
|
||||
<a href="/ledger">Ledger: every criticism</a>
|
||||
<a href="/claims">What Igneum does not claim</a>
|
||||
<a href="/randomx">Igneum vs RandomX</a>
|
||||
<a href="/provenance">Built on the shoulders</a>
|
||||
</div></div>
|
||||
<div class="footer-column"><h4>Explore</h4><div>
|
||||
<a href="/live">Live devnet</a>
|
||||
<a href="/explorer">Explorer</a>
|
||||
<a href="/build">Build on Igneum</a>
|
||||
<a href="/swap">Swap</a>
|
||||
<a href="/grants">Grants</a>
|
||||
<a href="/journey">Journey</a>
|
||||
<a href="/download">Downloads: devnet build</a>
|
||||
<a href="https://discord.gg/igneum" target="_blank" rel="noopener">Community Discord</a>
|
||||
<a href="mailto:hello@igneum.network">hello@igneum.network</a>
|
||||
<a href="https://git.igneum.network/igneum-network/spec/issues" rel="noopener">An issue on the spec</a>
|
||||
</div></div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>Igneum Labs LTD · Unit IH-00-01-01-OF-01, Level 01, Innovation One, Dubai International Financial Centre. Experimental software. Nothing on this site is an offer to sell anything.<br>Report a flaw by email or by an issue on the spec. Nobody from Igneum will ask for your seed. © 2026 Igneum · not an offer to sell anything</p>
|
||||
<div class="theme-group" role="group" aria-label="Theme"><button type="button" data-theme-set="system" aria-pressed="true">System</button><button type="button" data-theme-set="light" aria-pressed="false">Light</button><button type="button" data-theme-set="dark" aria-pressed="false">Dark</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// theme control: stores light or dark under igneum-theme; system clears it and follows the OS. The head applied the value before paint.
|
||||
var bs=document.querySelectorAll('[data-theme-set]');if(!bs.length)return;
|
||||
var media=window.matchMedia('(prefers-color-scheme: light)');
|
||||
function stored(){try{var t=localStorage.getItem('igneum-theme');return t==='light'||t==='dark'?t:'system';}catch(e){return 'system';}}
|
||||
function apply(){var c=stored();document.documentElement.setAttribute('data-theme',c==='system'?(media.matches?'light':'dark'):c);bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});document.dispatchEvent(new CustomEvent('igneum-theme'));}
|
||||
bs.forEach(function(b){b.addEventListener('click',function(){var v=b.getAttribute('data-theme-set');try{if(v==='system')localStorage.removeItem('igneum-theme');else localStorage.setItem('igneum-theme',v);}catch(e){}apply();});});
|
||||
if(media.addEventListener)media.addEventListener('change',function(){if(stored()==='system')apply();});
|
||||
document.addEventListener('igneum-theme',function(){var c=stored();bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===c?'true':'false');});});
|
||||
bs.forEach(function(b){b.setAttribute('aria-pressed',b.getAttribute('data-theme-set')===stored()?'true':'false');});
|
||||
// the network state word in the status bar: one read of the observer per page view
|
||||
var fs=document.getElementById('foot-state'),fd=document.getElementById('foot-dot');if(fs&&fd){fetch('/api/live?window=30',{cache:'no-store'}).then(function(r){return r.json();}).then(function(d){var on=d&&d.ok&&d.state&&!d.state.stale;fs.textContent=on?'devnet live':'devnet, observer stale';fd.className='dot '+(on?'live':'off');}).catch(function(){fs.textContent='devnet';});}
|
||||
})();
|
||||
</script>
|
||||
<!-- footer:end -->
|
||||
<script type="module">
|
||||
import { ignFromWei, shortHash, rel, int, utc } from '/lib/explorer.mjs';
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const addr = a => a ? `<a class="hash" href="/address/${esc(a)}">${esc(a)}</a>` : '<span class="kv">none</span>';
|
||||
const dl = pairs => pairs.filter(p => p).map(([k, v]) => `<dt>${esc(k)}</dt><dd>${v}</dd>`).join('');
|
||||
const wei = h => (h === null || h === undefined ? null : BigInt(h).toString());
|
||||
const id = decodeURIComponent(location.pathname.split('/').pop() || '');
|
||||
async function load() {
|
||||
const r = await fetch('/api/explorer?tx=' + encodeURIComponent(id)); const j = await r.json();
|
||||
$('title').textContent = id;
|
||||
if (!j.ok) { $('msg').textContent = j.error || 'Not found.'; return; }
|
||||
const t = j.tx, b = j.block, now = new Date(j.now).getTime(), g = t.igneum || {};
|
||||
document.title = `Devnet 3 transaction ${shortHash(t.hash, 8, 6)}`;
|
||||
$('title').textContent = t.hash;
|
||||
const chips = [t.status === 'success' ? '<span class="chip ok">success</span>' : t.status === 'failed' ? '<span class="chip" style="border-color:var(--excluded);color:var(--excluded)">failed</span>' : '<span class="chip">no receipt</span>'];
|
||||
chips.push(`<span class="chip">chain block ${int(t.number)}</span>`);
|
||||
if (b && b.in_observer) { chips.push(b.lock && b.lock.final ? `<span class="chip ok">final under lock ${int(b.lock.index)}</span>` : '<span class="chip">not under a lock yet</span>'); if (b.shards && b.shards.total) chips.push(`<span class="chip ${b.shards.paid === b.shards.total ? 'ok' : ''}">${b.shards.paid} of ${b.shards.total} shards paid</span>`); chips.push(`<span class="chip">class ${esc(b.class || 'v4')}</span>`); }
|
||||
if (t.contract_address) chips.push('<span class="chip hot">contract creation</span>');
|
||||
$('chips').innerHTML = chips.join('');
|
||||
const fee = t.gas_used !== null && t.effective_gas_price !== null ? (BigInt(t.gas_used) * BigInt(t.effective_gas_price)).toString() : null;
|
||||
$('summary').innerHTML = dl([
|
||||
['Network', 'Devnet 3, chain id ' + esc(j.chain_id ?? 4463)],
|
||||
['Hash', `<span class="hash">${esc(t.hash)}</span>`],
|
||||
['Status', t.status || 'no receipt'],
|
||||
['Block', `<a class="hash" href="/block/${esc(t.block_hash)}">${esc(shortHash(t.block_hash, 12, 8))}</a> <span class="kv">chain block ${int(t.number)}, index ${int(t.index)}</span>`],
|
||||
['Time', t.ts ? `${utc(t.ts)} <span class="kv">(${rel(now - t.ts)})</span>` : 'n/a'],
|
||||
['From', addr(t.from)], ['To', t.to ? addr(t.to) + (t.contracts && t.contracts[t.to] !== undefined ? ' <span class="chip">contract</span>' : '') : (t.contract_address ? `<span class="kv">created</span> ${addr(t.contract_address)}` : '<span class="kv">none</span>')],
|
||||
['Value', `${ignFromWei(t.value_wei, 8)} IGN`],
|
||||
['Fee', fee ? `${ignFromWei(fee, 8)} IGN <span class="kv">${int(t.gas_used)} gas at ${ignFromWei(t.effective_gas_price, 10)} IGN per gas</span>` : 'n/a'],
|
||||
['Gas', `${int(t.gas_used)} used of ${int(t.gas)} limit`],
|
||||
['Nonce', int(t.nonce)], ['Type', `${t.type} <span class="kv">${t.type === 2 ? 'EIP-1559' : t.type === 1 ? 'access list' : 'legacy'}</span>`],
|
||||
['Input', `${int(t.input_bytes)} bytes${t.selector ? `, selector <span class="hash">${esc(t.selector)}</span>` : ''}`],
|
||||
]);
|
||||
$('fees').innerHTML = t.igneum ? dl([
|
||||
['Miner tip', g.minerTip ? `${ignFromWei(wei(g.minerTip), 8)} IGN <span class="kv">to the including miner</span>` : 'n/a'],
|
||||
['Execution base fee burned', g.burnedExecutionBaseFee ? `${ignFromWei(wei(g.burnedExecutionBaseFee), 8)} IGN` : 'n/a'],
|
||||
['Proving fee burned', g.burnedProvingFee ? `${ignFromWei(wei(g.burnedProvingFee), 8)} IGN` : 'n/a'],
|
||||
['Proving gas', g.pgasUsed ? `${int(Number(BigInt(g.pgasUsed)))} pgas${g.provingBaseFeePerGas ? ` at ${ignFromWei(wei(g.provingBaseFeePerGas), 10)} IGN per pgas` : ''}${g.overBudget ? ', over budget' : ''}${g.pgasAborted ? ', aborted' : ''}` : 'n/a'],
|
||||
g.developerShares && g.developerShares.length ? ['App share', g.developerShares.map(s => `${ignFromWei(wei(s.wei), 8)} IGN ${s.payee ? 'to ' + addr(s.payee) : '<span class="kv">unregistered, burned</span>'}`).join('<br>')] : null,
|
||||
['Including block', g.includingBlock ? `<a class="hash" href="/block/${esc(String(g.includingBlock).replace(/^0x/, ''))}">${esc(shortHash(String(g.includingBlock).replace(/^0x/, ''), 12, 8))}</a> <span class="kv">the first block that carried it; executed under chain block ${int(t.number)}</span>` : 'n/a'],
|
||||
['Including miner', g.includingMiner ? addr(g.includingMiner) : 'n/a'],
|
||||
]) : '<div class="empty">No receipt yet.</div>';
|
||||
if (b && b.in_observer) {
|
||||
$('blk-eyebrow').textContent = `chain block ${int(b.number)}`;
|
||||
$('blk').innerHTML = dl([
|
||||
['Block', `<a class="hash" href="/block/${esc(b.hash)}">${esc(b.hash)}</a>`],
|
||||
['Blue score', int(b.blue_score)], ['DAA score', int(b.daa)], ['Class', esc(b.class || 'v4')],
|
||||
['Miner', addr(b.evm_miner)],
|
||||
['Lock state', b.lock ? `${b.lock.final ? 'final' : 'not final'} <span class="kv">${esc(b.lock.reason)}</span>` : 'n/a'],
|
||||
['Proof', b.shards && b.shards.total ? `${b.shards.paid} of ${b.shards.total} shards paid${b.shards.median_lag !== null ? `, median lag ${int(b.shards.median_lag)} DAA` : ''} <span class="kv">${b.shards.proving} proving, ${b.shards.verified} verified, ${b.shards.planned} planned</span>` : '<span class="kv">plan not read yet</span>'],
|
||||
]);
|
||||
} else { $('blk-eyebrow').textContent = 'outside the observer window'; $('blk').innerHTML = dl([['Block', `<a class="hash" href="/block/${esc(t.block_hash)}">${esc(t.block_hash)}</a>`], ['Note', esc(b && b.note ? b.note : 'the observer keeps one day of blocks')]]); }
|
||||
const logs = t.logs || [];
|
||||
$('log-n').textContent = `${logs.length} log${logs.length === 1 ? '' : 's'}`;
|
||||
$('logs').innerHTML = logs.length ? `<table><thead><tr><th class="n">Index</th><th>Address</th><th>Topics</th><th>Data</th></tr></thead><tbody>${logs.map(l => `<tr><td class="n">${int(l.i)}</td><td>${addr(l.address)}</td><td class="hash" style="font-size:12px">${(l.topics || []).map(x => esc(x)).join('<br>')}</td><td class="hash" style="font-size:12px;overflow-wrap:anywhere">${esc(l.data)}</td></tr>`).join('')}</tbody></table>` : '<div class="empty">This transaction emitted no logs.</div>';
|
||||
$('body').hidden = false;
|
||||
}
|
||||
load().catch(e => { $('msg').textContent = 'The API did not answer: ' + e.message; });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
144
site/vercel.json
144
site/vercel.json
|
|
@ -1,17 +1,139 @@
|
|||
{
|
||||
"cleanUrls": true,
|
||||
"trailingSlash": false,
|
||||
"redirects": [{"source": "/connect", "destination": "/metamask", "permanent": true}, {"source": "/gpu-benchmarks", "destination": "/miners", "permanent": true}],
|
||||
"rewrites": [{"source": "/block/:id", "destination": "/block"}, {"source": "/address/:addr", "destination": "/address"}, {"source": "/benchmarks", "destination": "/miners"}],
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/connect",
|
||||
"destination": "/metamask",
|
||||
"permanent": true
|
||||
},
|
||||
{
|
||||
"source": "/gpu-benchmarks",
|
||||
"destination": "/miners",
|
||||
"permanent": true
|
||||
}
|
||||
],
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/",
|
||||
"has": [
|
||||
{
|
||||
"type": "host",
|
||||
"value": "explorer.igneum.network"
|
||||
}
|
||||
],
|
||||
"destination": "/explorer"
|
||||
},
|
||||
{
|
||||
"source": "/tx/:hash",
|
||||
"destination": "/tx"
|
||||
},
|
||||
{
|
||||
"source": "/block/:id",
|
||||
"destination": "/block"
|
||||
},
|
||||
{
|
||||
"source": "/address/:addr",
|
||||
"destination": "/address"
|
||||
},
|
||||
{
|
||||
"source": "/benchmarks",
|
||||
"destination": "/miners"
|
||||
}
|
||||
],
|
||||
"headers": [
|
||||
{"source": "/(.*)", "headers": [{"key": "X-Content-Type-Options", "value": "nosniff"}, {"key": "X-Frame-Options", "value": "DENY"}, {"key": "Referrer-Policy", "value": "strict-origin-when-cross-origin"}, {"key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload"}]},
|
||||
{"source": "/fonts/(.*)", "headers": [{"key": "Cache-Control", "value": "public, max-age=31536000, immutable"}]},
|
||||
{"source": "/(.*)\\.(png|ico)", "headers": [{"key": "Cache-Control", "value": "public, max-age=86400, stale-while-revalidate=604800"}]},
|
||||
{"source": "/site.webmanifest", "headers": [{"key": "Cache-Control", "value": "public, max-age=86400, stale-while-revalidate=604800"}]},
|
||||
{"source": "/journey.json", "headers": [{"key": "Cache-Control", "value": "public, max-age=300"}]},
|
||||
{"source": "/api/live", "headers": [{"key": "Cache-Control", "value": "public, max-age=1"}]},
|
||||
{"source": "/api/stats", "headers": [{"key": "Cache-Control", "value": "public, max-age=10, s-maxage=10"}]},
|
||||
{"source": "/api/supply", "headers": [{"key": "Cache-Control", "value": "public, max-age=10, s-maxage=10"}]},
|
||||
{"source": "/api/explorer", "headers": [{"key": "Cache-Control", "value": "public, max-age=5, s-maxage=5"}]}
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "X-Content-Type-Options",
|
||||
"value": "nosniff"
|
||||
},
|
||||
{
|
||||
"key": "X-Frame-Options",
|
||||
"value": "DENY"
|
||||
},
|
||||
{
|
||||
"key": "Referrer-Policy",
|
||||
"value": "strict-origin-when-cross-origin"
|
||||
},
|
||||
{
|
||||
"key": "Strict-Transport-Security",
|
||||
"value": "max-age=63072000; includeSubDomains; preload"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/fonts/(.*)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=31536000, immutable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/(.*)\\.(png|ico)",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=86400, stale-while-revalidate=604800"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/site.webmanifest",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=86400, stale-while-revalidate=604800"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/journey.json",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=300"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/api/live",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/api/stats",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=10, s-maxage=10"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/api/supply",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=10, s-maxage=10"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/api/explorer",
|
||||
"headers": [
|
||||
{
|
||||
"key": "Cache-Control",
|
||||
"value": "public, max-age=5, s-maxage=5"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ const GROUPS = {
|
|||
build: ['/build', '/faucet', '/swap', '/grants'], // the builder programme (8 October 2026): the developer entry page, the Devnet 3 faucet, the swap, the grants
|
||||
};
|
||||
const ROUTES = Object.values(GROUPS).flat();
|
||||
// pages served without their own panel entry: the explorer's two rewrites, the 404 and the unlinked scenes lab
|
||||
const UNLISTED = new Set(['block.html', 'address.html', '404.html', 'scenes.html']);
|
||||
// pages served without their own panel entry: the explorer's three rewrites (block, address, tx), its proving view, the 404 and the unlinked scenes lab
|
||||
const UNLISTED = new Set(['block.html', 'address.html', 'tx.html', 'proving.html', '404.html', 'scenes.html']);
|
||||
|
||||
export function check(site) {
|
||||
const pages = readdirSync(site).filter(f => f.endsWith('.html'));
|
||||
|
|
|
|||
|
|
@ -118,3 +118,7 @@ Three readings the coordinator raised, reproduced read-only and answered:
|
|||
Under the null (independent residuals, n = 6 epochs) one pair reads r over 0.8 with p about 0.028 (t = 2.67 on 4 degrees of freedom) and over 0.9 with p about 0.007; chance 3-cliques per window are about C(m, 3) p^3: 0.09 at m = 30 ids, about 100 at m = 300. So the clique alone is never the alert; the candidate also needs a design flag on a member (chance per id: `nonce` about 0.002, `late_start` about 1e-6, `spread` unknown until the per-model baseline exists; the devnet's maximum excess is 5.2% against the 10% line) and 6 net windows. At n = 12 (`DETECTOR_WINDOW_EPOCHS=12`) p(r over 0.8) is about 0.001 and chance 3-cliques at m = 300 are about 0.005 per window: set 12 once the testnet has over 100 ids. The first week must add: (1) the excess spread per card model over 12 or more epochs (the devnet has three models over 6); (2) the nonce layout of third-party miners and pools (a stratum extranonce in the high word is honest and non-uniform, so until each software is baselined a `nonce` flag is evidence, not an alert); (3) bands for cards the project does not own, which needs the card model in the coinbase tag; (4) the `machine_group` count, to see what an 8-key card looks like at scale.
|
||||
|
||||
Consequences per tier: the detector costs a miner nothing (it runs on the observer; one `UPDATE` a minute, two epochs re-read a minute, two regexp scans of the intake every 30 minutes). A home card under 8 keys appears as a `machine_group`, never an alert by itself. A pool's key trips `band_high`, which is informational; a pool that publishes its card mix clears it. The alert's action is the signal of epoch-length.md section 11, whose cost per tier is that document's section 7.
|
||||
|
||||
## Explorer indexer (8 October 2026)
|
||||
|
||||
`tools/observer/explorer-indexer.mjs` runs beside the observer against the same node's EVM RPC and writes the EVM side the explorer needs: `<prefix>live_txs` (every transaction with its receipt, logs and the Igneum fee split), `<prefix>live_accounts` (balance, nonce, code, sent and received counts, miner flag), `<prefix>explorer_blocks` (number to hash, for the three-deep reorg re-read) and `<prefix>explorer_state` (progress, `igneum_getNodeInfo`). It never writes the observer's tables. Env: `DATABASE_URL`, `LIVE_TABLE_PREFIX` (default `dn3_`), `IGNEUM_EVM_RPC` (default `http://127.0.0.1:26850`), `EXPLORER_NETWORK` (default `igneum-devnet-3`; a node reporting another network is refused), `EXPLORER_RETAIN_HOURS` (168), `EXPLORER_BACKFILL` (0 = from the first block), `EXPLORER_BATCH` (25). `--once` runs one tick. Tests: `node --test tools/observer/explorer-indexer.test.mjs`. On build-1 it is the unit `igneum-explorer-dn3` (docs/plans/explorer.md section 8).
|
||||
|
|
|
|||
282
tools/observer/explorer-indexer.mjs
Normal file
282
tools/observer/explorer-indexer.mjs
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
// Igneum explorer indexer: the sibling of tools/observer/observer.mjs for the EVM side (8 October 2026, the explorer lane).
|
||||
// The observer writes every DAG block, the shard plans, the checkpoints and the certificates. This process reads the
|
||||
// execution layer of the same node (eth_getBlockByNumber with the transactions, eth_getBlockReceipts, eth_getBalance,
|
||||
// eth_getTransactionCount, eth_getCode, igneum_getNodeInfo) and writes what /tx/<hash>, /address/<addr> and the
|
||||
// transaction lists need. It never touches the observer's tables; it creates its own three next to them under the same
|
||||
// prefix, so one LIVE_TABLE_PREFIX names one chain on both sides. Node 22 or newer, no dependencies.
|
||||
//
|
||||
// node tools/observer/explorer-indexer.mjs run (env below)
|
||||
// node tools/observer/explorer-indexer.mjs --once one tick, then exit (the smoke run)
|
||||
//
|
||||
// Environment, every value optional:
|
||||
// DATABASE_URL Neon connection string (read from ~/.config/igneum/env when unset; never in the repo)
|
||||
// LIVE_TABLE_PREFIX table prefix, default dn3_ (Devnet 3, the same prefix the observer unit on the build box writes)
|
||||
// IGNEUM_EVM_RPC the node's execution JSON-RPC, default http://127.0.0.1:26850 (the Devnet 3 observer node's)
|
||||
// EXPLORER_NETWORK the network the node must report in igneum_getNodeInfo, default igneum-devnet-3; another name refuses
|
||||
// to index (8 October 2026, 10:09 UTC: a unit pointed at the first devnet's node for 26 s wrote 4,797 of
|
||||
// that chain's blocks into the dn3_ tables; the tables were wiped and re-filled)
|
||||
// EXPLORER_BACKFILL chain blocks to index behind the tip on an empty table, default 0 = from the first block
|
||||
// EXPLORER_RETAIN_HOURS hours of transactions kept, default 168 (7 days); older rows are deleted once an hour
|
||||
// EXPLORER_BATCH chain blocks per round while catching up, default 25
|
||||
// EXPLORER_TICK_MS the idle tick, default 2000
|
||||
//
|
||||
// Tables (created on start if missing; the prefix is written as T below):
|
||||
// T live_txs one row per EVM transaction: hash (0x), block_hash (the DAG block's hash, no 0x, as live_blocks), block_number, tx_index, from_addr, to_addr, value_wei,
|
||||
// nonce, gas, gas_price, gas_used, effective_gas_price, status, contract_address, input_bytes, selector,
|
||||
// logs_count, logs (jsonb), igneum (jsonb: the receipt's igneum section), tx_type, ts_ms, received_at
|
||||
// T live_accounts one row per address seen: balance_wei, nonce, code_bytes, is_contract, sent, received, first_seen_number,
|
||||
// last_seen_number, miner (mined a block in the observer's table), refreshed_at
|
||||
// T explorer_blocks one row per indexed chain block: number, hash, tx_count, gas_used, ts_ms, indexed_at (the reorg check
|
||||
// re-reads the newest three numbers every tick and re-indexes a number whose hash moved)
|
||||
// T explorer_state one row: indexed_number, tip_number, chain_id, node_info (igneum_getNodeInfo: params, digest, genesis),
|
||||
// txs_total, accounts_total, backlog, blocks_per_s, last_error, started_at, updated_at
|
||||
//
|
||||
// Reorgs: Devnet 3's rule is no reorg over depth 3 (release-0.3.22 gates), so three numbers are re-read each tick; a deeper
|
||||
// one shows as a block hash in live_txs that the observer's live_blocks no longer calls a chain block, which the API reports.
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const ONCE = process.argv.includes('--once');
|
||||
const T = (process.env.LIVE_TABLE_PREFIX === undefined ? 'dn3_' : process.env.LIVE_TABLE_PREFIX).replace(/[^a-z0-9_]/gi, '');
|
||||
const EVM = process.env.IGNEUM_EVM_RPC || 'http://127.0.0.1:26850';
|
||||
const BACKFILL = Math.max(0, Number(process.env.EXPLORER_BACKFILL) || 0);
|
||||
const NETWORK = process.env.EXPLORER_NETWORK || 'igneum-devnet-3';
|
||||
const RETAIN_H = Math.max(1, Number(process.env.EXPLORER_RETAIN_HOURS) || 168);
|
||||
const BATCH = Math.max(1, Math.min(100, Number(process.env.EXPLORER_BATCH) || 25));
|
||||
const TICK_MS = Math.max(500, Number(process.env.EXPLORER_TICK_MS) || 2000);
|
||||
const ACCOUNTS_PER_TICK = 20;
|
||||
const RPC_CONCURRENCY = 6;
|
||||
|
||||
// ---- pure helpers (tested by explorer-indexer.test.mjs) --------------------------------------------------------------
|
||||
export const hexInt = h => (h === null || h === undefined ? null : Number(BigInt(h)));
|
||||
export const hexBig = h => (h === null || h === undefined ? null : BigInt(h).toString());
|
||||
export const lower = a => (a ? String(a).toLowerCase() : null);
|
||||
|
||||
/** One live_txs row from the block's transaction object and its receipt (both as the node's JSON-RPC returns them). */
|
||||
export function txRow(tx, rc, block) {
|
||||
const input = String(tx.input || '0x');
|
||||
return {
|
||||
hash: lower(tx.hash), block_hash: lower(block.hash).replace(/^0x/, ''), block_number: hexInt(block.number), tx_index: hexInt(tx.transactionIndex) ?? 0,
|
||||
from_addr: lower(tx.from), to_addr: lower(tx.to), value_wei: hexBig(tx.value) || '0', nonce: hexInt(tx.nonce), gas: hexInt(tx.gas),
|
||||
gas_price: hexBig(tx.gasPrice ?? tx.maxFeePerGas) , gas_used: rc ? hexInt(rc.gasUsed) : null, effective_gas_price: rc ? hexBig(rc.effectiveGasPrice) : null,
|
||||
status: rc && rc.status !== undefined && rc.status !== null ? hexInt(rc.status) : null, contract_address: rc ? lower(rc.contractAddress) : null,
|
||||
input_bytes: Math.max(0, (input.length - 2) / 2), selector: input.length >= 10 ? input.slice(0, 10).toLowerCase() : null,
|
||||
logs_count: rc && rc.logs ? rc.logs.length : 0,
|
||||
logs: rc && rc.logs ? rc.logs.map(l => ({ i: hexInt(l.logIndex), address: lower(l.address), topics: l.topics || [], data: l.data || '0x' })) : [],
|
||||
igneum: rc && rc.igneum ? rc.igneum : null, tx_type: hexInt(tx.type) ?? 0, ts_ms: hexInt(block.timestamp) === null ? null : hexInt(block.timestamp) * 1000,
|
||||
};
|
||||
}
|
||||
|
||||
/** The addresses a block touches, each with why (sender, receiver, contract, miner). */
|
||||
export function touched(rows, block) {
|
||||
const m = new Map();
|
||||
const add = (a, kind) => { if (!a) return; const k = lower(a); if (!m.has(k)) m.set(k, new Set()); m.get(k).add(kind); };
|
||||
for (const r of rows) { add(r.from_addr, 'sender'); add(r.to_addr, 'receiver'); add(r.contract_address, 'contract'); }
|
||||
if (block && block.igneum && Array.isArray(block.igneum.rewards)) for (const r of block.igneum.rewards) add(r.miner, 'miner');
|
||||
if (block && block.miner) add(block.miner, 'miner');
|
||||
return m;
|
||||
}
|
||||
|
||||
// ---- Neon over HTTP, the same pattern as the observer ---------------------------------------------------------------
|
||||
function envFile() {
|
||||
try { return Object.fromEntries(readFileSync(`${homedir()}/.config/igneum/env`, 'utf8').split('\n').filter(l => /^[A-Z_]+=/.test(l)).map(l => { const i = l.indexOf('='); return [l.slice(0, i), l.slice(i + 1).replace(/^"|"$/g, '')]; })); } catch { return {}; }
|
||||
}
|
||||
function neon(url) {
|
||||
if (!url) throw new Error('DATABASE_URL is not set');
|
||||
const host = new URL(url).hostname.replace('-pooler', '');
|
||||
return async (query, params = []) => {
|
||||
const r = await fetch(`https://${host}/sql`, { method: 'POST', headers: { 'Neon-Connection-String': url, 'Content-Type': 'application/json' }, body: JSON.stringify({ query, params }), signal: AbortSignal.timeout(30000) });
|
||||
const j = await r.json();
|
||||
if (!r.ok) throw new Error(j.message || JSON.stringify(j));
|
||||
return j.rows || [];
|
||||
};
|
||||
}
|
||||
|
||||
// ---- JSON-RPC with a small concurrency cap ----------------------------------------------------------------------------
|
||||
let inFlight = 0; const waiters = [];
|
||||
async function rpc(method, params = []) {
|
||||
if (inFlight >= RPC_CONCURRENCY) await new Promise(r => waiters.push(r));
|
||||
inFlight++;
|
||||
try {
|
||||
const r = await fetch(EVM, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(15000) });
|
||||
const j = await r.json();
|
||||
if (j.error) throw new Error(`${method}: ${j.error.message || JSON.stringify(j.error)}`);
|
||||
return j.result;
|
||||
} finally { inFlight--; const w = waiters.shift(); if (w) w(); }
|
||||
}
|
||||
const hex = n => '0x' + Number(n).toString(16);
|
||||
|
||||
// ---- schema ----------------------------------------------------------------------------------------------------------
|
||||
async function schema(sql) {
|
||||
await sql(`CREATE TABLE IF NOT EXISTS ${T}live_txs (
|
||||
hash text PRIMARY KEY, block_hash text NOT NULL, block_number bigint NOT NULL, tx_index integer NOT NULL DEFAULT 0,
|
||||
from_addr text, to_addr text, value_wei text NOT NULL DEFAULT '0', nonce bigint, gas bigint, gas_price text, gas_used bigint,
|
||||
effective_gas_price text, status smallint, contract_address text, input_bytes integer NOT NULL DEFAULT 0, selector text,
|
||||
logs_count integer NOT NULL DEFAULT 0, logs jsonb NOT NULL DEFAULT '[]'::jsonb, igneum jsonb, tx_type smallint NOT NULL DEFAULT 0,
|
||||
ts_ms bigint, received_at timestamptz NOT NULL DEFAULT now())`);
|
||||
await sql(`CREATE INDEX IF NOT EXISTS ${T}live_txs_number ON ${T}live_txs (block_number DESC, tx_index)`);
|
||||
await sql(`CREATE INDEX IF NOT EXISTS ${T}live_txs_from ON ${T}live_txs (from_addr, block_number DESC)`);
|
||||
await sql(`CREATE INDEX IF NOT EXISTS ${T}live_txs_to ON ${T}live_txs (to_addr, block_number DESC)`);
|
||||
await sql(`CREATE INDEX IF NOT EXISTS ${T}live_txs_block ON ${T}live_txs (block_hash)`);
|
||||
await sql(`CREATE INDEX IF NOT EXISTS ${T}live_txs_ts ON ${T}live_txs (ts_ms)`);
|
||||
await sql(`CREATE TABLE IF NOT EXISTS ${T}live_accounts (
|
||||
address text PRIMARY KEY, balance_wei text, nonce bigint, code_bytes integer, is_contract boolean, sent integer NOT NULL DEFAULT 0,
|
||||
received integer NOT NULL DEFAULT 0, first_seen_number bigint, last_seen_number bigint, miner boolean NOT NULL DEFAULT false,
|
||||
refreshed_at timestamptz, updated_at timestamptz NOT NULL DEFAULT now())`);
|
||||
await sql(`CREATE INDEX IF NOT EXISTS ${T}live_accounts_seen ON ${T}live_accounts (last_seen_number DESC)`);
|
||||
await sql(`CREATE TABLE IF NOT EXISTS ${T}explorer_blocks (number bigint PRIMARY KEY, hash text NOT NULL, tx_count integer NOT NULL DEFAULT 0,
|
||||
gas_used bigint, ts_ms bigint, indexed_at timestamptz NOT NULL DEFAULT now())`);
|
||||
await sql(`CREATE TABLE IF NOT EXISTS ${T}explorer_state (id integer PRIMARY KEY, indexed_number bigint, tip_number bigint, chain_id integer,
|
||||
node_info jsonb, txs_total bigint, accounts_total bigint, backlog integer, blocks_per_s double precision, last_error text,
|
||||
rpc text, started_at timestamptz, updated_at timestamptz)`);
|
||||
await sql(`INSERT INTO ${T}explorer_state (id, started_at, updated_at, rpc) VALUES (1, now(), now(), $1) ON CONFLICT (id) DO UPDATE SET started_at = now(), rpc = $1`, [EVM]);
|
||||
}
|
||||
|
||||
// ---- the work ----------------------------------------------------------------------------------------------------------
|
||||
const log = (...a) => console.log(new Date().toISOString(), ...a);
|
||||
const accountQueue = new Map(); // address -> {kinds:Set, number}
|
||||
const contractKnown = new Map(); // address -> is_contract (code fetched once)
|
||||
let txsPerMin = 0, blocksPerMin = 0, accountsPerMin = 0, lastError = null;
|
||||
|
||||
async function indexNumbers(sql, numbers) {
|
||||
const blocks = await Promise.all(numbers.map(n => rpc('eth_getBlockByNumber', [hex(n), true])));
|
||||
const receipts = await Promise.all(numbers.map((n, i) => (blocks[i] && blocks[i].transactions && blocks[i].transactions.length ? rpc('eth_getBlockReceipts', [hex(n)]).catch(() => null) : Promise.resolve([]))));
|
||||
const rows = []; const blockRows = [];
|
||||
for (let i = 0; i < numbers.length; i++) {
|
||||
const b = blocks[i]; if (!b) continue;
|
||||
const byHash = new Map((receipts[i] || []).map(r => [lower(r.transactionHash), r]));
|
||||
const txs = (b.transactions || []).map(t => txRow(t, byHash.get(lower(t.hash)) || null, b));
|
||||
rows.push(...txs);
|
||||
blockRows.push({ number: hexInt(b.number), hash: lower(b.hash).replace(/^0x/, ''), tx_count: txs.length, gas_used: hexInt(b.gasUsed), ts_ms: hexInt(b.timestamp) * 1000 });
|
||||
for (const [a, kinds] of touched(txs, b)) { const q = accountQueue.get(a) || { kinds: new Set(), number: 0 }; for (const k of kinds) q.kinds.add(k); q.number = Math.max(q.number, hexInt(b.number)); accountQueue.set(a, q); }
|
||||
}
|
||||
// a number re-indexed after a reorg drops its old rows first
|
||||
await sql(`DELETE FROM ${T}live_txs WHERE block_number = ANY($1::bigint[])`, [numbers]);
|
||||
for (let off = 0; off < rows.length; off += 200) {
|
||||
const chunk = rows.slice(off, off + 200); const vals = []; const params = [];
|
||||
chunk.forEach((r, k) => {
|
||||
const base = k * 22;
|
||||
vals.push(`(${Array.from({ length: 22 }, (_, j) => '$' + (base + j + 1)).join(',')})`);
|
||||
params.push(r.hash, r.block_hash, r.block_number, r.tx_index, r.from_addr, r.to_addr, r.value_wei, r.nonce, r.gas, r.gas_price, r.gas_used, r.effective_gas_price, r.status, r.contract_address, r.input_bytes, r.selector, r.logs_count, JSON.stringify(r.logs), r.igneum ? JSON.stringify(r.igneum) : null, r.tx_type, r.ts_ms, new Date().toISOString());
|
||||
});
|
||||
await sql(`INSERT INTO ${T}live_txs (hash, block_hash, block_number, tx_index, from_addr, to_addr, value_wei, nonce, gas, gas_price, gas_used, effective_gas_price, status, contract_address, input_bytes, selector, logs_count, logs, igneum, tx_type, ts_ms, received_at)
|
||||
VALUES ${vals.join(',')} ON CONFLICT (hash) DO UPDATE SET block_hash = EXCLUDED.block_hash, block_number = EXCLUDED.block_number, tx_index = EXCLUDED.tx_index, gas_used = EXCLUDED.gas_used,
|
||||
effective_gas_price = EXCLUDED.effective_gas_price, status = EXCLUDED.status, contract_address = EXCLUDED.contract_address, logs_count = EXCLUDED.logs_count, logs = EXCLUDED.logs, igneum = EXCLUDED.igneum, ts_ms = EXCLUDED.ts_ms`, params);
|
||||
}
|
||||
if (blockRows.length) {
|
||||
const vals = []; const params = [];
|
||||
blockRows.forEach((b, k) => { vals.push(`($${k * 5 + 1},$${k * 5 + 2},$${k * 5 + 3},$${k * 5 + 4},$${k * 5 + 5})`); params.push(b.number, b.hash, b.tx_count, b.gas_used, b.ts_ms); });
|
||||
await sql(`INSERT INTO ${T}explorer_blocks (number, hash, tx_count, gas_used, ts_ms) VALUES ${vals.join(',')} ON CONFLICT (number) DO UPDATE SET hash = EXCLUDED.hash, tx_count = EXCLUDED.tx_count, gas_used = EXCLUDED.gas_used, ts_ms = EXCLUDED.ts_ms, indexed_at = now()`, params);
|
||||
}
|
||||
// sent and received counts and first/last seen, in one statement per batch
|
||||
const seen = new Map();
|
||||
for (const r of rows) {
|
||||
if (r.from_addr) { const s = seen.get(r.from_addr) || { sent: 0, received: 0, min: r.block_number, max: r.block_number }; s.sent++; s.min = Math.min(s.min, r.block_number); s.max = Math.max(s.max, r.block_number); seen.set(r.from_addr, s); }
|
||||
if (r.to_addr) { const s = seen.get(r.to_addr) || { sent: 0, received: 0, min: r.block_number, max: r.block_number }; s.received++; s.min = Math.min(s.min, r.block_number); s.max = Math.max(s.max, r.block_number); seen.set(r.to_addr, s); }
|
||||
}
|
||||
const ents = [...seen.entries()];
|
||||
for (let off = 0; off < ents.length; off += 300) {
|
||||
const chunk = ents.slice(off, off + 300); const vals = []; const params = [];
|
||||
chunk.forEach(([a, s], k) => { vals.push(`($${k * 5 + 1},$${k * 5 + 2},$${k * 5 + 3},$${k * 5 + 4},$${k * 5 + 5})`); params.push(a, s.sent, s.received, s.min, s.max); });
|
||||
await sql(`INSERT INTO ${T}live_accounts (address, sent, received, first_seen_number, last_seen_number) VALUES ${vals.join(',')}
|
||||
ON CONFLICT (address) DO UPDATE SET sent = ${T}live_accounts.sent + EXCLUDED.sent, received = ${T}live_accounts.received + EXCLUDED.received,
|
||||
first_seen_number = LEAST(${T}live_accounts.first_seen_number, EXCLUDED.first_seen_number), last_seen_number = GREATEST(${T}live_accounts.last_seen_number, EXCLUDED.last_seen_number), updated_at = now()`, params);
|
||||
}
|
||||
txsPerMin += rows.length; blocksPerMin += blockRows.length;
|
||||
return blockRows;
|
||||
}
|
||||
|
||||
async function refreshAccounts(sql, max) {
|
||||
if (!accountQueue.size) return 0;
|
||||
const picks = [...accountQueue.entries()].sort((a, b) => b[1].number - a[1].number).slice(0, max);
|
||||
for (const [a] of picks) accountQueue.delete(a);
|
||||
const reads = await Promise.all(picks.map(async ([a, q]) => {
|
||||
try {
|
||||
const [bal, nonce] = await Promise.all([rpc('eth_getBalance', [a, 'latest']), rpc('eth_getTransactionCount', [a, 'latest'])]);
|
||||
let code = contractKnown.get(a);
|
||||
if (code === undefined || q.kinds.has('contract')) { const c = await rpc('eth_getCode', [a, 'latest']); code = Math.max(0, (String(c || '0x').length - 2) / 2); contractKnown.set(a, code); }
|
||||
return { a, bal: hexBig(bal), nonce: hexInt(nonce), code, miner: q.kinds.has('miner') };
|
||||
} catch (e) { lastError = String(e.message || e).slice(0, 200); accountQueue.set(a, q); return null; }
|
||||
}));
|
||||
const ok = reads.filter(Boolean);
|
||||
if (ok.length) {
|
||||
const vals = []; const params = [];
|
||||
ok.forEach((r, k) => { vals.push(`($${k * 6 + 1},$${k * 6 + 2},$${k * 6 + 3},$${k * 6 + 4},$${k * 6 + 5},$${k * 6 + 6})`); params.push(r.a, r.bal, r.nonce, r.code, r.code > 0, r.miner); });
|
||||
await sql(`INSERT INTO ${T}live_accounts (address, balance_wei, nonce, code_bytes, is_contract, miner, refreshed_at) VALUES ${vals.map(v => v.replace(/\)$/, ', now())')).join(',')}
|
||||
ON CONFLICT (address) DO UPDATE SET balance_wei = EXCLUDED.balance_wei, nonce = EXCLUDED.nonce, code_bytes = EXCLUDED.code_bytes, is_contract = EXCLUDED.is_contract,
|
||||
miner = ${T}live_accounts.miner OR EXCLUDED.miner, refreshed_at = now(), updated_at = now()`, params);
|
||||
}
|
||||
accountsPerMin += ok.length;
|
||||
return ok.length;
|
||||
}
|
||||
|
||||
async function queueMiners(sql) {
|
||||
// the observer's miners of the last 10 minutes get a balance read every minute, so a miner page is never older than that
|
||||
const rows = await sql(`SELECT DISTINCT evm_miner AS a FROM ${T}live_blocks WHERE received_at > now() - interval '10 minutes' AND evm_miner IS NOT NULL`).catch(() => []);
|
||||
for (const r of rows) { const q = accountQueue.get(lower(r.a)) || { kinds: new Set(), number: 0 }; q.kinds.add('miner'); accountQueue.set(lower(r.a), q); }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const env = envFile();
|
||||
const sql = neon(process.env.DATABASE_URL || env.DATABASE_URL);
|
||||
await schema(sql);
|
||||
for (const r of await sql(`SELECT address, code_bytes FROM ${T}live_accounts WHERE code_bytes IS NOT NULL`)) contractKnown.set(r.address, Number(r.code_bytes));
|
||||
const st = (await sql(`SELECT indexed_number FROM ${T}explorer_state WHERE id = 1`))[0] || {};
|
||||
let indexed = st.indexed_number === null || st.indexed_number === undefined ? null : Number(st.indexed_number);
|
||||
const chainId = hexInt(await rpc('eth_chainId'));
|
||||
const info0 = await rpc('igneum_getNodeInfo').catch(() => null);
|
||||
if (!info0 || info0.network !== NETWORK) throw new Error(`the node at ${EVM} reports network ${info0 ? info0.network : 'unknown'}, this indexer writes ${NETWORK} (EXPLORER_NETWORK); refusing to index`);
|
||||
log(`explorer indexer: prefix ${T}, rpc ${EVM} (${info0.network}, digest ${String(info0.digest).slice(0, 8)}), chain id ${chainId}, indexed ${indexed === null ? 'nothing yet' : indexed}, batch ${BATCH}, retain ${RETAIN_H} h`);
|
||||
let lastInfo = 0, lastMiners = 0, lastRetain = 0, lastLog = Date.now(), lastRate = Date.now(), rateBlocks = 0;
|
||||
for (;;) {
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const tip = hexInt(await rpc('eth_blockNumber'));
|
||||
if (indexed === null) { indexed = BACKFILL > 0 ? Math.max(-1, tip - BACKFILL) : -1; log(`starting at chain block ${indexed + 1}, tip ${tip}`); }
|
||||
// the reorg check: the newest three indexed numbers must still carry the hash we stored
|
||||
if (indexed >= 0) {
|
||||
const lo = Math.max(0, indexed - 2);
|
||||
const stored = await sql(`SELECT number, hash FROM ${T}explorer_blocks WHERE number BETWEEN $1 AND $2`, [lo, indexed]);
|
||||
const live = await Promise.all(stored.map(s => rpc('eth_getBlockByNumber', [hex(Number(s.number)), false]).catch(() => null)));
|
||||
const moved = stored.filter((s, i) => live[i] && lower(live[i].hash).replace(/^0x/, '') !== s.hash).map(s => Number(s.number));
|
||||
if (moved.length) { const from = Math.min(...moved); log(`reorg: chain block ${from} moved, re-indexing from it`); indexed = from - 1; }
|
||||
}
|
||||
const behind = tip - indexed;
|
||||
if (behind > 0) {
|
||||
const n = Math.min(BATCH, behind);
|
||||
const numbers = Array.from({ length: n }, (_, i) => indexed + 1 + i);
|
||||
await indexNumbers(sql, numbers);
|
||||
indexed += n; rateBlocks += n;
|
||||
}
|
||||
if (Date.now() - lastMiners > 60000) { await queueMiners(sql); lastMiners = Date.now(); }
|
||||
// while catching up, only the newest addresses are refreshed; the rest wait for the tip
|
||||
await refreshAccounts(sql, behind > BATCH ? 4 : ACCOUNTS_PER_TICK);
|
||||
let info = null;
|
||||
if (Date.now() - lastInfo > 300000) { info = await rpc('igneum_getNodeInfo').catch(() => null); lastInfo = Date.now(); if (info && info.network !== NETWORK) throw new Error(`the node now reports network ${info.network}, not ${NETWORK}; stopping`); }
|
||||
if (indexed > tip + 100) { log(`the node's tip ${tip} is behind the indexed number ${indexed}: a node that moved back; re-indexing from ${tip - 3}`); indexed = Math.max(-1, tip - 3); }
|
||||
if (Date.now() - lastRetain > 3600000) {
|
||||
const cut = Date.now() - RETAIN_H * 3600000;
|
||||
const d = await sql(`WITH d AS (DELETE FROM ${T}live_txs WHERE ts_ms < $1 RETURNING 1) SELECT count(*)::int AS n FROM d`, [cut]);
|
||||
await sql(`DELETE FROM ${T}explorer_blocks WHERE ts_ms < $1`, [cut]);
|
||||
if (d[0] && d[0].n) log(`retention: ${d[0].n} transactions older than ${RETAIN_H} h removed`);
|
||||
lastRetain = Date.now();
|
||||
}
|
||||
const dt = (Date.now() - lastRate) / 1000; let bps = null; if (dt >= 10) { bps = rateBlocks / dt; rateBlocks = 0; lastRate = Date.now(); }
|
||||
await sql(`UPDATE ${T}explorer_state SET indexed_number = $1, tip_number = $2, chain_id = $3, backlog = $4, blocks_per_s = COALESCE($5, blocks_per_s), last_error = $6, updated_at = now(),
|
||||
node_info = COALESCE($7::jsonb, node_info), txs_total = CASE WHEN $8 THEN (SELECT count(*) FROM ${T}live_txs) ELSE txs_total END,
|
||||
accounts_total = CASE WHEN $8 THEN (SELECT count(*) FROM ${T}live_accounts) ELSE accounts_total END WHERE id = 1`,
|
||||
[indexed, tip, chainId, Math.max(0, tip - indexed), bps, lastError, info ? JSON.stringify(info) : null, Date.now() - lastLog > 60000 || ONCE]);
|
||||
if (Date.now() - lastLog > 60000) { log(`indexed ${indexed} of tip ${tip} (behind ${tip - indexed}); last minute: ${blocksPerMin} blocks, ${txsPerMin} txs, ${accountsPerMin} account reads, queue ${accountQueue.size}${lastError ? `, last error: ${lastError}` : ''}`); txsPerMin = blocksPerMin = accountsPerMin = 0; lastError = null; lastLog = Date.now(); }
|
||||
if (ONCE) { log(`once: indexed ${indexed}, tip ${tip}`); return; }
|
||||
if (tip - indexed <= 0) await new Promise(r => setTimeout(r, Math.max(0, TICK_MS - (Date.now() - t0))));
|
||||
} catch (e) {
|
||||
lastError = String(e.message || e).slice(0, 200);
|
||||
log(`tick failed: ${lastError}`);
|
||||
if (ONCE) { process.exitCode = 1; return; }
|
||||
await new Promise(r => setTimeout(r, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && /explorer-indexer\.mjs$/.test(process.argv[1])) main().catch(e => { console.error(e); process.exit(1); });
|
||||
38
tools/observer/explorer-indexer.test.mjs
Normal file
38
tools/observer/explorer-indexer.test.mjs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// The explorer indexer's pure parts: a transaction row from the node's JSON, the touched-address set, the hex helpers.
|
||||
// node --test tools/observer/explorer-indexer.test.mjs
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { txRow, touched, hexInt, hexBig } from './explorer-indexer.mjs';
|
||||
|
||||
const block = { hash: '0xE61CFFB1D306AD8D3F4F9FEC6D750100517572F54A14A82B40EBFD393531320A', number: '0x654d', timestamp: '0x6acd', miner: '0x8DB0505bbb03dffb58875897eeedc4e790a68d18', igneum: { rewards: [{ miner: '0x78fb14e43d3170dcf9abfdc70f15685c8ea31678', wei: '0x1' }] } };
|
||||
const tx = { hash: '0x4E7C2A9D54208B70A557184CB35E5847CDA4E90FFCA9578C6FC05B9C0DC4EF25', from: '0xE617a7f966fc009934e943fe0453821ff0c8716f', to: '0x7fe2f74b45dd8f0a739c906817f197631fdeff85', value: '0x0', nonce: '0x1a', gas: '0x38936', gasPrice: '0x178411b200', input: '0xa9059cbb000000000000000000000000', transactionIndex: '0x3', type: '0x2' };
|
||||
const rc = { transactionHash: tx.hash, gasUsed: '0x6e5e', effectiveGasPrice: '0x178411b200', status: '0x1', contractAddress: null, logs: [{ logIndex: '0x0', address: '0x7FE2f74b45dd8f0a739c906817f197631fdeff85', topics: ['0xaa'], data: '0x01' }], igneum: { minerTip: '0x148eb7b4f000', pgasUsed: '0x56c' } };
|
||||
|
||||
test('hex helpers: null stays null, big values keep every digit', () => {
|
||||
assert.equal(hexInt(null), null); assert.equal(hexInt('0x654d'), 25933);
|
||||
assert.equal(hexBig('0x18c55612df9b9f9a3600'), '116977006436888600000000'); assert.equal(hexBig(undefined), null);
|
||||
});
|
||||
|
||||
test('txRow: lower-case addresses and hashes, numbers decoded, the receipt folded in, the selector and the log list', () => {
|
||||
const r = txRow(tx, rc, block);
|
||||
assert.equal(r.hash, tx.hash.toLowerCase()); assert.equal(r.block_hash, block.hash.toLowerCase().slice(2)); assert.equal(r.block_number, 25933); assert.equal(r.tx_index, 3);
|
||||
assert.equal(r.from_addr, '0xe617a7f966fc009934e943fe0453821ff0c8716f'); assert.equal(r.to_addr, '0x7fe2f74b45dd8f0a739c906817f197631fdeff85');
|
||||
assert.equal(r.value_wei, '0'); assert.equal(r.nonce, 26); assert.equal(r.gas, 231734); assert.equal(r.gas_used, 28254); assert.equal(r.status, 1);
|
||||
assert.equal(r.selector, '0xa9059cbb'); assert.equal(r.input_bytes, 16); assert.equal(r.logs_count, 1); assert.equal(r.logs[0].address, '0x7fe2f74b45dd8f0a739c906817f197631fdeff85');
|
||||
assert.equal(r.igneum.minerTip, '0x148eb7b4f000'); assert.equal(r.tx_type, 2); assert.equal(r.ts_ms, 0x6acd * 1000);
|
||||
});
|
||||
|
||||
test('txRow without a receipt: the receipt fields are null, nothing throws', () => {
|
||||
const r = txRow({ ...tx, input: '0x', to: null }, null, block);
|
||||
assert.equal(r.gas_used, null); assert.equal(r.status, null); assert.equal(r.selector, null); assert.equal(r.to_addr, null); assert.deepEqual(r.logs, []); assert.equal(r.igneum, null);
|
||||
});
|
||||
|
||||
test('touched: senders, receivers, the created contract and the rewarded miners, each lower case with its reasons', () => {
|
||||
const rows = [txRow(tx, { ...rc, contractAddress: '0xABCDEF0000000000000000000000000000000001' }, block)];
|
||||
const m = touched(rows, block);
|
||||
assert.deepEqual([...m.get('0xe617a7f966fc009934e943fe0453821ff0c8716f')], ['sender']);
|
||||
assert.deepEqual([...m.get('0x7fe2f74b45dd8f0a739c906817f197631fdeff85')], ['receiver']);
|
||||
assert.deepEqual([...m.get('0xabcdef0000000000000000000000000000000001')], ['contract']);
|
||||
assert.deepEqual([...m.get('0x78fb14e43d3170dcf9abfdc70f15685c8ea31678')], ['miner']);
|
||||
assert.deepEqual([...m.get('0x8db0505bbb03dffb58875897eeedc4e790a68d18')], ['miner']);
|
||||
});
|
||||
|
|
@ -16,7 +16,10 @@ if (!process.env.DATABASE_URL) {
|
|||
try { const m = /^DATABASE_URL=(.*)$/m.exec(readFileSync(`${homedir()}/.config/igneum/env`, 'utf8')); if (m) process.env.DATABASE_URL = m[1].trim().replace(/^['"]|['"]$/g, ''); } catch { }
|
||||
}
|
||||
const cfg = JSON.parse(readFileSync(join(site, 'vercel.json'), 'utf8'));
|
||||
const rewrites = (cfg.rewrites || []).map(r => ({ re: new RegExp('^' + r.source.replace(/:[a-z]+/g, '[^/]+') + '$'), to: r.destination }));
|
||||
// a rewrite with a host condition (vercel.json `has: [{type: 'host', value}]`, the explorer host's root since 8 October 2026) applies only
|
||||
// when the request's Host matches, as on Vercel; without this the explorer's host rewrite turned the local "/" into /explorer and the
|
||||
// scene parity gate found no scene on the home fold
|
||||
const rewrites = (cfg.rewrites || []).map(r => ({ re: new RegExp('^' + r.source.replace(/:[a-z]+/g, '[^/]+') + '$'), to: r.destination, host: ((r.has || []).find(h => h.type === 'host') || {}).value || null }));
|
||||
const TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.png': 'image/png', '.ico': 'image/x-icon', '.svg': 'image/svg+xml', '.woff2': 'font/woff2', '.webp': 'image/webp', '.txt': 'text/plain', '.xml': 'application/xml', '.webmanifest': 'application/manifest+json' };
|
||||
const handlers = new Map();
|
||||
async function api(name) {
|
||||
|
|
@ -29,7 +32,8 @@ function shim(res) {
|
|||
}
|
||||
createServer(async (req, res) => {
|
||||
const url = new URL(req.url, 'http://x'); let p = decodeURIComponent(url.pathname);
|
||||
for (const rw of rewrites) if (rw.re.test(p)) { p = rw.to; break; }
|
||||
const host = String(req.headers.host || '').replace(/:\d+$/, '');
|
||||
for (const rw of rewrites) { if (rw.host && rw.host !== host) continue; if (rw.re.test(p)) { p = rw.to; break; } }
|
||||
if (p.startsWith('/api/')) {
|
||||
const name = p.slice(5).replace(/[^a-z0-9_-]/gi, '');
|
||||
if (!existsSync(join(site, 'api', name + '.mjs'))) { res.statusCode = 404; return res.end('no such function'); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue