igneum/pool/web/index.html
igneum-labs 633e211c91 Pool finished (mission item 11): pool-0's fee published beside the dev fee, TLS 1.3 on the member port with the authorize binding (spec 9.3, O-9.7 closed), the page rows Q68 to Q73, and the open pool: a share sidechain with no operator (spec 9.12)
Pool-0: welcome carries software_dev_fee_percent beside the pool fee, the page's Fees row and site/miner.html say pool-0 charges the same 1 percent as the solo dev fee; jobs and seeds carry the latency ladder's rung for 0.3.19.

TLS (pool/src/tls.rs): --tls-cert/--tls-key or --tls-self-signed (a P-256 pair under the data dir, the certificate pin printed at start); the binding is the member's BLS signature over this connection's exporter (label EXPORTER-igneum-pool-binding, context the chain id), refused on any other connection; testnet and mainnet refuse the clear without --allow-plain. HiveOS: pools:// and POOL_PIN.

Page rows: node state in words (Q68), one formatter set and luck in MiningPoolStats' convention (Q69), samples and check costs persisted (Q70), payments under the lookup (Q71), hourly history with a sparkline, --alert-webhook, /metrics, /health on the node (Q72), the network's finality state beside "payouts follow blue confirmation, not finality" (Q73).

The open pool (pool/src/sidechain.rs, open.rs, p2p.rs; igneum-pool --open): the member's own node and daemon, no payout key, no balance; every coinbase carries the member's own address, the share chain's parent (IGNS) and the window's split (IGNP); templates re-stamped on a new chain tip without a node call; shares gossiped and checked by every member (structure, the node's seeds, the PoW); heaviest work wins, a fork's loser is stale; the dev fee as one split entry; shares.log and verify-share for the drop proof; the gate harness pool/tools/open-gate.mjs. The node side (the executor's split from pool_split_activation_daa) is on the fork branch pool-finish-node.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 3e5271ba6a)
2026-10-07 14:03:05 +00:00

272 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>__POOL_NAME__</title>
<meta name="description" content="An Igneum mining pool: hash rate, miners, blocks, payouts, finality, and how to connect igneum-miner.">
<meta name="theme-color" content="#0C0C0E">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml">
<style>
/* the site's tokens (site/partials/head.html and site/explorer.html), self-contained: the pool runs on its own host */
:root{--f-sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--f-display:'Unbounded','Arial Black',sans-serif;--f-mono:'IBM Plex Mono',ui-monospace,Menlo,monospace;
--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E;--green:#3FD39A;
--max:1200px;--gutter:clamp(16px,4vw,32px);--gap:24px;--gap-tile:12px;--card-pad:clamp(18px,3vw,28px);--card-r:18px;--tile-r:14px;--fs-h1:clamp(32px,5.5vw,56px);--fs-h3:clamp(18px,2vw,22px);--fs-tile:clamp(20px,2.2vw,26px)}
*{box-sizing:border-box}
body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--f-sans);font-size:17px;line-height:1.55;-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:inherit;text-decoration:none}a:hover{color:var(--molten)}
.wrap{max-width:var(--max);margin:0 auto;padding-inline:var(--gutter)}
.nav{position:sticky;top:0;z-index:20;background:rgba(12,12,14,.84);backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
.nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:68px}
.brand{display:inline-flex;align-items:center;gap:10px}
.brand .word{font-family:var(--f-display);font-weight:900;font-size:20px;letter-spacing:.06em}
.nav .links{display:flex;gap:clamp(14px,2vw,26px);font-size:15px;font-weight:500;color:var(--ink-2)}
.mono{font-family:var(--f-mono)}
.eyebrow{font-family:var(--f-mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash)}
h1,h2{font-family:var(--f-display);margin:0;line-height:1.08}
h1{font-weight:900;font-size:var(--fs-h1);letter-spacing:-.01em;overflow-wrap:anywhere}
h2{font-weight:700;font-size:var(--fs-h3)}
p{margin:0}
.head{padding-block:clamp(40px,6vw,64px) var(--gap);display:flex;flex-direction:column;gap:14px}
.head p{color:var(--ink-2);max-width:64ch;font-size:16px}
.card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0;margin-top:var(--gap)}
.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap-tile)}
@media (max-width:700px){.strip{grid-template-columns:repeat(2,minmax(0,1fr))}}
.cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:18px 20px;display:flex;flex-direction:column;gap:6px;min-width:0}
.cell .k{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
.cell .v{font-family:var(--f-display);font-weight:700;font-size:var(--fs-tile);line-height:1.1;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}
.cell .s{font-family:var(--f-mono);font-size:12px;color:var(--ash)}
.viz-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px 20px;margin-bottom:12px}
.tbl{overflow-x:auto;margin-top:12px}
table{border-collapse:collapse;width:100%;font-size:14px}
th,td{padding:10px 10px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap;vertical-align:top}
th{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500}
td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
tr:last-child td{border-bottom:0}
.hash{font-family:var(--f-mono)}
.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)}
.chip.ok{border-color:var(--green);color:var(--green)}.chip.hot{border-color:var(--molten);color:var(--molten)}.chip.bad{border-color:var(--ember);color:var(--ember)}
.note{font-size:13px;color:var(--ash);margin-top:14px;max-width:80ch}
.bar{display:flex;flex-wrap:wrap;gap:8px 18px;align-items:center;font-family:var(--f-mono);font-size:13px;color:var(--ink-2);margin-top:12px}
.bar .dot{display:inline-block;width:9px;height:9px;border-radius:50%;background:var(--ash);margin-right:6px}
.bar .dot.ok{background:var(--green)}.bar .dot.bad{background:var(--ember)}.bar .dot.hot{background:var(--molten)}
pre{background:var(--obsidian);border:1px solid var(--line);border-radius:10px;padding:14px 16px;font:500 13px/1.5 var(--f-mono);overflow-x:auto;margin:12px 0 0;color:var(--bone)}
.search{display:flex;gap:10px;max-width:760px;margin-top:4px}
.search input{flex:1;min-width:0;min-height:48px;padding:10px 14px;border-radius:10px;border:1px solid var(--line-2);background:var(--graphite);color:var(--bone);font:500 15px/1.3 var(--f-mono)}
.btn{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:12px 22px;border-radius:10px;font-weight:600;font-size:16px;border:1px solid var(--ember);color:var(--ember-ink);background:var(--ember);cursor:pointer}
.btn:hover{background:#FF6A2B}
.msg{font-family:var(--f-mono);font-size:13px;color:var(--molten);min-height:18px}
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}
.two{display:grid;gap:var(--gap);grid-template-columns:1fr;margin-top:var(--gap)}
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}.two .card{margin-top:0}}
svg.spark{width:100%;height:64px;display:block;margin-top:10px}
main{padding-bottom:clamp(56px,8vw,96px)}
.foot{border-top:1px solid var(--line);padding:28px 0;font-size:13px;color:var(--ash)}
</style>
</head>
<body>
<nav class="nav" aria-label="Main">
<div class="wrap">
<a href="https://igneum.network/" class="brand" aria-label="Igneum home">
<svg viewBox="0 0 1024 1024" width="36" height="36" aria-hidden="true"><rect width="1024" height="1024" 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>
<div class="links"><a href="#connect">Connect</a><a href="#blocks">Blocks</a><a href="#payments">Payments</a><a href="#lookup">Your address</a><a href="/api/stats">API</a><a href="https://igneum.network/miner">The miner</a></div>
</div>
</nav>
<main id="main" class="wrap">
<div class="head">
<div class="eyebrow">__NETWORK__ · __MODE__ pool · fee __FEE__% · minimum payout __MIN_PAYOUT__ IGN</div>
<h1>__POOL_NAME__</h1>
<p id="lede">Your vote key stays yours: the pool names your key in every header it gives you, so your finality weight follows you (spec 09). The pool pays 80% of each block it finds across the last shares; the 20% proving share goes to provers by the chain's own rule and never passes through here.</p>
<div class="bar" aria-live="polite">
<span><span class="dot" id="d-node"></span><span id="t-node">node</span></span>
<span><span class="dot" id="d-fin"></span><span id="t-fin">finality</span></span>
<span id="t-rule">payouts follow blue confirmation, not finality</span>
<span id="t-tls"></span>
</div>
</div>
<div class="strip" aria-live="polite">
<div class="cell"><div class="k">Pool hash rate</div><div class="v" id="p-hr">0</div><div class="s" id="p-hr1h">10 min / 1 h</div></div>
<div class="cell"><div class="k">Miners / workers</div><div class="v" id="p-miners">0 / 0</div><div class="s">online now</div></div>
<div class="cell"><div class="k">Blocks 24 h</div><div class="v" id="p-blocks">0</div><div class="s" id="p-blocks-s">confirmed / orphaned</div></div>
<div class="cell"><div class="k">Last block</div><div class="v" id="p-last">none</div><div class="s" id="p-last-s"></div></div>
<div class="cell"><div class="k">Network hash rate</div><div class="v" id="n-hr">0</div><div class="s" id="n-diff">difficulty</div></div>
<div class="cell"><div class="k">Block reward</div><div class="v" id="n-reward">0<small>IGN</small></div><div class="s">80% to the miner, 20% to provers</div></div>
<div class="cell"><div class="k">Effort</div><div class="v" id="p-effort">0%</div><div class="s" id="p-luck">luck 24 h</div></div>
<div class="cell"><div class="k">Paid 24 h</div><div class="v" id="p-paid">0<small>IGN</small></div><div class="s" id="p-paid-s">pending</div></div>
</div>
<div class="card" id="open-card" hidden>
<div class="viz-head"><h2>The share chain</h2><div class="eyebrow">no operator: every block's own coinbase pays the window</div></div>
<dl>
<dt>Height</dt><dd id="o-height"></dd>
<dt>Tip</dt><dd class="hash" id="o-tip"></dd>
<dt>Target</dt><dd class="hash" id="o-target"></dd>
<dt>Shares</dt><dd id="o-shares"></dd>
<dt>Window</dt><dd id="o-window"></dd>
</dl>
<div class="tbl"><table><thead><tr><th>Address</th><th class="n">Share of the next block</th></tr></thead><tbody id="o-split"></tbody></table></div>
<p class="note">A share is a block template at the chain's target with your key in the header and your address in the coinbase. Shares form a chain between the members; a block found on it pays the last shares by the chain's rule, which every node applies from the coinbase alone. There is no balance to withhold and no key that could. The software dev fee is one entry of the split, the same 1% the solo miner and pool-0 carry.</p>
</div>
<div class="card" id="connect">
<div class="viz-head"><h2>Connect</h2><div class="eyebrow">igneum-miner 0.3.19 or later</div></div>
<p class="note" style="margin-top:0">One command per card. The payout address is an EVM address you hold the key for; the worker name labels the card on this page. Your own node is optional on an operator pool: with one, the miner checks the pool's seeds and templates against it, votes on finality with your key, and submits found blocks there too. Without one it hashes and does not vote. The open pool needs your node: the daemon beside it builds your templates from it.</p>
<pre>igneum-miner mine grpc://127.0.0.1:26610 1 100000000 rig1 --pool __POOL_URL__ __TLS__ --evm-address 0xYOURADDRESS --worker-name rig1-gpu0 \
--worker ./igneum-worker-cuda --worker-args "--device 0 --pack packs/devnet"
# without a node of your own (hashes, does not vote):
igneum-miner mine none 1 100000000 rig1 --pool __POOL_URL__ __TLS__ --evm-address 0xYOURADDRESS --worker-name rig1-gpu0 --worker ./igneum-worker-cuda</pre>
<dl>
<dt>HiveOS</dt><dd>Flight Sheet pool URL <span class="mono">pool://__POOL_URL__</span>, wallet <span class="mono">0xYOURADDRESS.%WORKER_NAME%</span>; the rest as on igneum.network/miner</dd>
<dt>Transport</dt><dd id="c-tls">TLS 1.3 on the member port (spec 9.3); the miner's <span class="mono">--pool-tls</span> uses the system roots, <span class="mono">--pool-pin</span> the certificate pin this pool prints at start</dd>
<dt>Shares</dt><dd>a share is a lane hash at or below your share target; the pool sets it so you send about one every 10 s and checks every one on the CPU exactly as a node checks a block</dd>
<dt>Fees</dt><dd>the solo miner's software dev fee is 1 block in 100; this pool's fee is __FEE__%, published here and in the welcome message every miner prints at start; in pool mode the miner software takes no dev fee of its own</dd>
<dt>Pool address</dt><dd class="hash">__POOL_ADDRESS__</dd>
</dl>
</div>
<div class="card" id="lookup">
<div class="viz-head"><h2>Your address</h2><div class="eyebrow">shares, hash rate, balance, payments, 7 days</div></div>
<form class="search" id="search"><input id="q" type="search" autocomplete="off" spellcheck="false" placeholder="0x your payout address" aria-label="Payout address"><button class="btn" type="submit">Look up</button></form>
<div class="msg" id="msg" aria-live="polite"></div>
<div id="miner" hidden>
<dl>
<dt>Hash rate</dt><dd id="m-hr"></dd>
<dt>Shares</dt><dd id="m-shares"></dd>
<dt>Balance</dt><dd id="m-balance"></dd>
<dt>Paid</dt><dd id="m-paid"></dd>
<dt>Blocks found</dt><dd id="m-blocks"></dd>
</dl>
<svg class="spark" id="m-spark" viewBox="0 0 600 64" preserveAspectRatio="none" aria-label="Hash rate per hour over 7 days"></svg>
<div class="s mono" id="m-spark-s" style="font-size:12px;color:var(--ash)"></div>
<div class="tbl"><table><thead><tr><th>Worker</th><th class="n">Hash rate</th><th class="n">Accepted</th><th class="n">Stale</th><th class="n">Rejected</th><th>Shift</th><th>Proving</th><th>Status</th></tr></thead><tbody id="m-workers"></tbody></table></div>
<div class="viz-head" style="margin-top:18px"><h2>Payments to this address</h2><div class="eyebrow">newest first</div></div>
<div class="tbl"><table><thead><tr><th>Time</th><th class="n">IGN</th><th>Tx</th><th>Status</th></tr></thead><tbody id="m-pay"></tbody></table></div>
<p class="note">A worker that also proves shards gives about 4% of its hash rate to the prover (measured on an RTX 5090, bench-log "proving v1"); proving income is paid to your own key by the chain and does not appear here. The Proving column says when the miner reports it.</p>
</div>
</div>
<div class="two">
<div class="card" id="blocks" style="margin-top:0">
<div class="viz-head"><h2>Blocks</h2><div class="eyebrow">newest first, every 5 s</div></div>
<div class="tbl"><table><thead><tr><th>Block</th><th class="n">DAA</th><th>Finder</th><th class="n">Reward</th><th class="n">Effort</th><th>Status</th><th>Time</th></tr></thead><tbody id="blocks-body"><tr><td colspan="7">Loading.</td></tr></tbody></table></div>
</div>
<div class="card" id="payments" style="margin-top:0">
<div class="viz-head"><h2>Payments</h2><div class="eyebrow" id="pay-eyebrow">EVM transfers from the pool address</div></div>
<div class="tbl"><table><thead><tr><th>Time</th><th>Address</th><th class="n">IGN</th><th>Tx</th><th>Status</th></tr></thead><tbody id="pay-body"><tr><td colspan="5">Loading.</td></tr></tbody></table></div>
</div>
</div>
<div class="card">
<div class="viz-head"><h2>API</h2><div class="eyebrow">JSON, GET, CORS open</div></div>
<dl>
<dt>/api/stats</dt><dd><a class="hash" href="/api/stats">pool hash rate, miners, workers, blocks 24 h, last block, fee, minimum payout, luck, the network's difficulty, hash rate, reward and finality</a></dd>
<dt>/api/blocks</dt><dd><a class="hash" href="/api/blocks">blocks found with DAA score, time, finder, status and the split</a></dd>
<dt>/api/miners/&lt;address&gt;</dt><dd class="hash">hash rate, shares, workers, balance, paid, payments, hourly history</dd>
<dt>/api/payments</dt><dd><a class="hash" href="/api/payments">every payout with its transaction hash</a></dd>
<dt>/api/pool-stats</dt><dd><a class="hash" href="/api/pool-stats">the flat camelCase object pool dashboards poll</a></dd>
<dt>/metrics</dt><dd><a class="hash" href="/metrics">Prometheus text</a></dd>
<dt>/health</dt><dd><a class="hash" href="/health">200 when the node is synced and answered a template in the last 30 s, else 503</a></dd>
<dt>/api/open</dt><dd><a class="hash" href="/api/open">the share chain (open pool)</a></dd>
</dl>
</div>
</main>
<footer class="foot"><div class="wrap">Igneum. Mined by GPUs. Proven by fire. · <a href="https://igneum.network/">igneum.network</a> · Nothing on this page is an offer to sell anything.</div></footer>
<script>
// Q69: one formatter set, the site's conventions (polish 4.3): hash rate 1 decimal on a kH to PH ladder, counts with
// en-GB separators, difficulty one spelling, IGN 4 decimals on tiles and 6 in tables.
const $ = id => document.getElementById(id);
const esc = s => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;');
const nf = new Intl.NumberFormat('en-GB');
const num = n => nf.format(Math.round(Number(n) || 0));
const hr = h => { h = Number(h) || 0; const u = ['H/s','kH/s','MH/s','GH/s','TH/s','PH/s']; let i = 0; while (h >= 1000 && i < u.length - 1) { h /= 1000; i++; } return (i ? h.toFixed(1) : Math.round(h)) + ' ' + u[i]; };
const diff = d => { d = Number(d) || 0; const u = ['','k','M','G','T','P']; let i = 0; while (d >= 1000 && i < u.length - 1) { d /= 1000; i++; } return (i ? d.toFixed(2) : Math.round(d)) + (i ? ' ' + u[i] : ''); };
const ign4 = v => (Number(v) || 0).toFixed(4), ign6 = v => (Number(v) || 0).toFixed(6);
const short = h => h ? h.slice(0, 10) + '…' : '';
const rel = ms => { const d = (Date.now() - ms) / 1000; return d < 60 ? Math.round(d) + ' s ago' : d < 3600 ? Math.round(d / 60) + ' min ago' : Math.round(d / 3600) + ' h ago'; };
const chip = s => `<span class="chip ${s === 'confirmed' || s === 'coinbase' ? 'ok' : s === 'orphan' || s === 'failed' ? 'bad' : 'hot'}">${esc(s)}</span>`;
let openMode = false;
async function stats() {
try {
const r = await fetch('/api/stats'); const s = await r.json(); const p = s.pool, n = s.network;
openMode = p.mode === 'open';
$('p-hr').textContent = hr(p.hashrate); $('p-hr1h').textContent = hr(p.hashrate_1h) + ' over 1 h';
$('p-miners').textContent = num(p.miners) + ' / ' + num(p.workers);
$('p-blocks').textContent = num(p.blocks_24h); $('p-blocks-s').textContent = num(p.blocks_confirmed_24h) + ' confirmed / ' + num(p.blocks_orphaned_24h) + ' orphaned, ' + num(p.blocks_total) + ' total';
if (p.last_block) { $('p-last').textContent = short(p.last_block.hash); $('p-last-s').textContent = rel(p.last_block.ts_ms) + ', ' + p.last_block.status; }
$('n-hr').textContent = n.hashrate == null ? 'pending' : hr(n.hashrate); $('n-diff').textContent = 'difficulty ' + diff(n.difficulty) + ', DAA ' + num(n.daa_score);
$('n-reward').innerHTML = esc(ign4(n.miner_reward_ign)) + '<small>IGN</small>';
// Q69, Q72: luck in MiningPoolStats' convention (expected over actual; over 100% is lucky)
$('p-effort').textContent = Math.round(p.effort_current * 100) + '%'; $('p-luck').textContent = 'luck 24 h ' + (p.luck_24h ? Math.round(p.luck_24h * 100) + '%' : 'n/a');
$('p-paid').innerHTML = esc(ign4(p.paid_24h_ign)) + '<small>IGN</small>'; $('p-paid-s').textContent = openMode ? 'paid by the coinbases' : ign4(p.pending_balance_ign) + ' IGN pending' + (p.dry_run ? ' (dry run)' : '');
// Q68, Q72, Q73: the node and the network's finality, in words
const nd = $('d-node'); nd.className = 'dot ' + (p.node_state === 'ok' ? 'ok' : p.node_state === 'syncing' ? 'hot' : 'bad');
$('t-node').textContent = p.node_state === 'ok' ? 'node ok, ' + esc(n.node_version) : p.node_state === 'unreachable' ? 'node unreachable since ' + (n.updated_ms ? rel(n.updated_ms) : 'start') : 'node ' + p.node_state;
const fd = $('d-fin'); fd.className = 'dot ' + (n.finality === 'active' ? 'ok' : n.finality === 'unknown' ? '' : 'hot');
$('t-fin').textContent = n.finality === 'active' ? 'finality active, checkpoint ' + num(n.finality_locked_index) + (n.finality_locked_age_s ? ', ' + num(n.finality_locked_age_s) + ' s ago' : '') : 'finality ' + esc(n.finality);
$('t-tls').textContent = p.tls ? 'members on TLS 1.3' : 'members in the clear';
if (openMode) {
$('lede').textContent = 'An open pool: no operator. Your daemon builds your templates from your own node with your key in the header and your address in the coinbase; shares form a chain between the members, and every block found on it pays the window from its own coinbase by the chain’s rule. Nobody holds a balance or a key for you.';
$('pay-eyebrow').textContent = 'coinbase credits, by block';
$('open-card').hidden = false;
}
} catch (e) { $('p-last-s').textContent = 'stats unavailable'; $('d-node').className = 'dot bad'; $('t-node').textContent = 'pool unreachable'; }
}
async function open() {
if (!openMode) return;
try {
const r = await fetch('/api/open'); const j = await r.json(); if (!j.ok) return; const o = j.open;
$('o-height').textContent = num(o.height) + (o.tip_age_s != null ? ' (tip ' + num(o.tip_age_s) + ' s ago)' : '');
$('o-tip').textContent = o.tip ? short(o.tip) : 'none yet';
$('o-target').textContent = (o.target64 || '') + ' (next ' + (o.next_target64 || '') + ')';
$('o-shares').textContent = num(o.accepted) + ' accepted, ' + num(o.stale) + ' stale, ' + num(o.reorgs) + ' reorgs, ' + num(o.rejected) + ' refused; ' + num(o.local_shares) + ' ours, ' + num(o.peer_shares) + ' from peers';
$('o-window').textContent = num(o.window_shares) + ' shares, ' + (o.share_ms / 1000) + ' s per share, dev fee ' + o.dev_fee_percent + '%';
$('o-split').innerHTML = o.window.length ? o.window.map(w => `<tr><td class="hash">${esc(w.address)}</td><td class="n">${(w.fraction * 100).toFixed(2)}%</td></tr>`).join('') : '<tr><td colspan="2">No share yet.</td></tr>';
} catch (e) {}
}
async function blocks() {
try {
const r = await fetch('/api/blocks?limit=25'); const j = await r.json();
$('blocks-body').innerHTML = j.blocks.length ? j.blocks.map(b => `<tr><td class="hash">${short(b.hash)}</td><td class="n">${num(b.daa_score)}</td><td class="hash">${short(b.finder)} ${esc(b.worker)}</td><td class="n">${ign6(b.reward_ign)}</td><td class="n">${Math.round(b.effort * 100)}%</td><td>${chip(b.status)}</td><td>${rel(b.ts_ms)}</td></tr>`).join('') : '<tr><td colspan="7">No block yet.</td></tr>';
} catch (e) {}
}
async function payments() {
try {
const r = await fetch('/api/payments?limit=25'); const j = await r.json();
$('pay-body').innerHTML = j.payments.length ? j.payments.map(p => `<tr><td>${rel(p.ts_ms)}</td><td class="hash">${short(p.address)}</td><td class="n">${ign6(p.amount_ign)}</td><td class="hash">${p.tx_hash ? short(p.tx_hash) : ''}</td><td>${chip(p.status)}</td></tr>`).join('') : '<tr><td colspan="5">No payment yet.</td></tr>';
} catch (e) {}
}
function spark(h) {
const svg = $('m-spark');
if (!h.length) { svg.innerHTML = ''; $('m-spark-s').textContent = 'no hourly history yet'; return; }
const max = Math.max(...h.map(x => x.hashrate), 1), w = 600, hh = 64, n = h.length, bw = Math.max(1, w / Math.max(n, 24) - 1);
svg.innerHTML = h.map((x, i) => `<rect x="${(i * w / Math.max(n, 24)).toFixed(1)}" y="${(hh - x.hashrate / max * hh).toFixed(1)}" width="${bw.toFixed(1)}" height="${(x.hashrate / max * hh).toFixed(1)}" fill="#F2541B"><title>${new Date(x.hour_ms).toISOString().slice(0, 13)}:00Z ${hr(x.hashrate)}, ${num(x.shares)} shares</title></rect>`).join('');
$('m-spark-s').textContent = num(n) + ' hours of history, peak ' + hr(max);
}
$('search').addEventListener('submit', async e => {
e.preventDefault(); const a = $('q').value.trim();
if (!/^0x[0-9a-fA-F]{40}$/.test(a)) { $('msg').textContent = 'An address is 0x followed by 40 hex characters.'; return; }
$('msg').textContent = 'Looking.';
const r = await fetch('/api/miners/' + a); const m = await r.json();
if (!m.ok) { $('msg').textContent = m.error; return; }
$('msg').textContent = m.online ? 'Online.' : (m.first_seen_ms ? 'Not connected now.' : 'No shares from this address yet.');
$('miner').hidden = false;
$('m-hr').textContent = hr(m.hashrate) + ' (10 min), ' + hr(m.hashrate_1h) + ' (1 h)';
$('m-shares').textContent = num(m.shares.accepted) + ' accepted, ' + num(m.shares.stale) + ' stale, ' + num(m.shares.rejected) + ' rejected';
$('m-balance').textContent = openMode ? 'none: the coinbase pays' : ign6(m.balance_ign) + ' IGN'; $('m-paid').textContent = ign6(m.paid_ign) + ' IGN'; $('m-blocks').textContent = num(m.blocks);
$('m-workers').innerHTML = m.workers.length ? m.workers.map(w => `<tr><td>${esc(w.name)}</td><td class="n">${hr(w.hashrate)}</td><td class="n">${num(w.accepted)}</td><td class="n">${num(w.stale)}</td><td class="n">${num(w.rejected)}</td><td>${w.shift ?? ''}</td><td>${w.proving ? 'yes' : 'no'}</td><td>${chip(w.online ? 'online' : 'offline')}</td></tr>`).join('') : '<tr><td colspan="8">No worker yet.</td></tr>';
// Q71: the payment history the API already returned
$('m-pay').innerHTML = (m.payments || []).length ? m.payments.map(p => `<tr><td>${rel(p.ts_ms)}</td><td class="n">${ign6(p.amount_ign)}</td><td class="hash">${p.tx_hash ? short(p.tx_hash) : ''}</td><td>${chip(p.status)}</td></tr>`).join('') : '<tr><td colspan="4">No payment yet.</td></tr>';
spark(m.history || []);
history.replaceState(null, '', '#lookup');
});
stats().then(open); blocks(); payments();
setInterval(() => stats().then(open), 5000); setInterval(blocks, 5000); setInterval(payments, 15000);
</script>
</body>
</html>