igneum/tools/ci/public-api-check.mjs
igneum-labs 303038a75d /api/stats gains proving (coverage_10m, provers, lag, verifier) from live_state.proving; the live page says whose verifier is off (reviewer C33)
A third party reading the public API could not see that proving coverage on the devnet is 2.7% of chain blocks with
one prover (13 of 476 in ten minutes at 22:33 UTC) while the homepage says every block is proven. The object is the one
/api/live reads, with coverage_10m = blocks_fully_proven_10m / blocks_10m and a note that the observer's node reads
the chain's payouts with its own verifier off. Documented with the live figures, in the contract test and the live check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 22:35:21 +00:00

28 lines
2.5 KiB
JavaScript

#!/usr/bin/env node
// Checks that a deployed site answers /api/stats and /api/supply with every documented field (the FIELDS lists in
// site/api/stats.mjs and site/api/supply.mjs, the same lists docs/api/public-stats.md is written from).
// node tools/ci/public-api-check.mjs [https://igneum.network]
// Exit 1 on a missing field, a non-200 answer or a stale observer older than 10 minutes (the numbers are what pool
// software and profitability sites read; a stale answer is a wrong answer).
import { FIELDS as STATS, REWARD_FIELDS, LAST_BLOCK_FIELDS, PROVING_FIELDS } from '../../site/api/stats.mjs';
import { FIELDS as SUPPLY, SCHEDULE_FIELDS } from '../../site/api/supply.mjs';
const base = (process.argv[2] || process.env.PUBLIC_API_URL || 'https://igneum.network').replace(/\/$/, '');
const missing = [];
async function get(path) {
const r = await fetch(base + path, { headers: { 'cache-control': 'no-cache' }, signal: AbortSignal.timeout(15000) });
const j = await r.json().catch(() => null);
if (r.status !== 200 || !j || j.ok !== true) { console.error(`${path}: HTTP ${r.status} ${j ? JSON.stringify(j).slice(0, 200) : ''}`); process.exit(1); }
return j;
}
const s = await get('/api/stats');
for (const f of STATS) if (!(f in s)) missing.push(`stats.${f}`);
if (s.block_reward) for (const f of REWARD_FIELDS) if (!(f in s.block_reward)) missing.push(`stats.block_reward.${f}`);
if (s.last_block) for (const f of LAST_BLOCK_FIELDS) if (!(f in s.last_block)) missing.push(`stats.last_block.${f}`);
if (s.proving) for (const f of PROVING_FIELDS) if (!(f in s.proving)) missing.push(`stats.proving.${f}`); else missing.push('stats.proving');
const p = await get('/api/supply');
for (const f of SUPPLY) if (!(f in p)) missing.push(`supply.${f}`);
if (p.schedule && p.schedule[0]) for (const f of SCHEDULE_FIELDS) if (!(f in p.schedule[0])) missing.push(`supply.schedule.${f}`);
if (missing.length) { console.error(`public api check: ${missing.length} missing field(s): ${missing.join(', ')}`); process.exit(1); }
if (s.stale && s.age_s > 600) { console.error(`public api check: the observer is stale (${s.age_s} s)`); process.exit(1); }
console.log(`public api check: ${base} /api/stats (${STATS.length} fields, height ${s.height}, reward ${s.block_reward && s.block_reward.ign} IGN, proving coverage ${s.proving && s.proving.coverage_10m}, ${s.stale ? 'STALE ' + s.age_s + ' s' : 'live'}) and /api/supply (${SUPPLY.length} fields, circulating ${p.circulating_ign} IGN of ${p.max_supply_ign}) answer with every documented field`);