igneum/site/api/public-stats.test.mjs
igneum-labs 48d987cfb4 Public stats API: /api/stats and /api/supply, documented with live examples, contract test and a live check
The numbers profitability sites and pool software read (WhatToMine's form: explorer or pool with an API, the halving
schedule, a source for total coins). Reward and supply from the emission rule at the node's DAA score; the halving table
(33 rows), the 30-day ramp and the observer's coinbase check. Cached 10 s, CORS open. FIELDS in each handler is the
contract; public-stats.test.mjs checks it from a fixture, tools/ci/public-api-check.mjs checks a deployment.

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

73 lines
5.3 KiB
JavaScript

// The public stats contract: /api/stats and /api/supply answer with every field docs/api/public-stats.md documents
// (FIELDS in each handler), computed from a fixture of what the observer writes. Runs without a database.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createHandler as stats, FIELDS as STATS_FIELDS, REWARD_FIELDS, LAST_BLOCK_FIELDS, rewardAt } from './stats.mjs';
import { createHandler as supply, FIELDS as SUPPLY_FIELDS, SCHEDULE_FIELDS } from './supply.mjs';
const NOW = '2026-10-05T19:21:56.000Z';
const state = {
id: 1, block_count: 125066, header_count: 125066, blue_score: 122211, difficulty: 7748714.75, hashes_per_second_estimate: 27759288, peers: 7, mempool: 0,
node_version: '2.1.0', network: 'igneum-devnet', blocks_60s: 58, blocks_per_minute: Array.from({ length: 60 }, (_, i) => [1791227000000 + i * 60000, 60]),
observer_started_at: '2026-10-05T19:01:00.000Z', updated_at: '2026-10-05T19:21:55.000Z',
finality: { chain_id: 'igneum-devnet', finality_active: true, latest_locked_index: 4073, latest_locked_blue_score: 122190 },
supply_check: { checked_at: NOW, sampled: 500, rule_match: 500, rule_mismatch: 0, sum_match: 480, sum_mismatch: 0, sum_skipped: 20, examples: [], bps: 1 },
};
const last = { hash: '2622db7698c6825b370bbffba03ea11bba250792040296971d2b790a954203ee', blue_score: 122211, daa_score: 125064, timestamp_ms: 1791227608731,
miner_address: 'igneumdev:qr4yfyf9mzn643fj8faksflmxur0wxgtmpg8y7fa6qkm8pcjh3ngzqedfx47e', vote_key_hash: '1c3f1190b777365e419f02fd46ce365f464377d0fdbdf1332da6600d09a75a0e', tx_count: 0, is_chain_block: true, number: 81263 };
function fakeSql(rows) { return async (query) => { if (/row_to_json/.test(query) && /max\(daa_score\)/.test(query)) return [{ now: NOW, state, daa: 125064 }]; if (/row_to_json/.test(query)) return [{ now: NOW, state }]; if (/ORDER BY received_at DESC LIMIT 1/.test(query)) return [last]; if (/count\(DISTINCT vote_key_hash\)/.test(query)) return [{ n: 25 }]; return rows || []; }; }
function fakeRes() { const r = { code: 0, headers: {}, body: null, status(c) { r.code = c; return r; }, setHeader(k, v) { r.headers[k] = v; }, json(o) { r.body = o; return r; } }; return r; }
test('/api/stats carries every documented field and the reward of spec 2.5', async () => {
const res = fakeRes();
await stats({ sql: fakeSql(), env: {} })({ method: 'GET', url: '/api/stats' }, res);
assert.equal(res.code, 200);
for (const f of STATS_FIELDS) assert.ok(f in res.body, `missing ${f}`);
for (const f of REWARD_FIELDS) assert.ok(f in res.body.block_reward, `missing block_reward.${f}`);
for (const f of LAST_BLOCK_FIELDS) assert.ok(f in res.body.last_block, `missing last_block.${f}`);
assert.equal(res.body.chain_id, 4463);
assert.equal(res.body.height, 81263);
assert.equal(res.body.block_reward.sompi, '454486399'); // E(125,064), the payload of block 2622db76
assert.equal(res.body.block_reward.ign, '4.54486399');
assert.equal(res.body.block_reward.miner_ign, '3.6358912'); // 80%: 454486399 - 90897279
assert.equal(res.body.block_reward.proving_pool_ign, '0.90897279');
assert.equal(res.body.block_time_measured_s, 1.034);
assert.equal(res.body.blocks_per_day_measured, 86400);
assert.equal(res.body.stale, false);
assert.equal(res.headers['Cache-Control'], 'public, max-age=10, s-maxage=10');
});
test('/api/stats marks a stale observer', async () => {
const res = fakeRes();
const old = { ...state, updated_at: '2026-10-05T19:00:00.000Z' };
const sql = async (q) => (/row_to_json/.test(q) ? [{ now: NOW, state: old }] : /LIMIT 1/.test(q) ? [last] : [{ n: 0 }]);
await stats({ sql, env: {} })({ method: 'GET', url: '/api/stats' }, res);
assert.equal(res.body.stale, true);
assert.ok(res.body.age_s > 1000);
});
test('rewardAt: halving and ramp bookkeeping', () => {
const r = rewardAt(63_115_200);
assert.equal(r.sompi, '1584404390'); assert.equal(r.halving_period, 1); assert.equal(r.next_halving_daa, 126_230_400); assert.equal(r.ramp_factor, 1);
assert.equal(rewardAt(0).ramp_factor, 0.1);
});
test('/api/supply carries every documented field, the cap and a 33-row schedule', async () => {
const res = fakeRes();
await supply({ sql: fakeSql(), env: {} })({ method: 'GET', url: '/api/supply' }, res);
assert.equal(res.code, 200);
for (const f of SUPPLY_FIELDS) assert.ok(f in res.body, `missing ${f}`);
assert.equal(res.body.max_supply_ign, '4000000000');
assert.equal(res.body.schedule.length, 33);
for (const f of SCHEDULE_FIELDS) assert.ok(f in res.body.schedule[0], `missing schedule.${f}`);
assert.equal(res.body.schedule[0].per_second_ign, '31.68808781');
assert.equal(res.body.schedule[1].per_second_ign, '15.8440439');
assert.equal(res.body.schedule[1].start_daa, 63115200);
assert.equal(res.body.minted_at_end_ign, '3963038988.86765648');
assert.equal(res.body.circulating_ign, '482350.69732271'); // mintedByRule(125,064): the rule summed over DAA 0 to 125,063
assert.equal(res.body.halving.next_halving_daa, 63115200);
assert.equal(res.body.ramp.complete, false);
assert.equal(res.body.check.rule_match, 500);
});
test('both refuse anything but GET', async () => {
for (const h of [stats({ sql: fakeSql(), env: {} }), supply({ sql: fakeSql(), env: {} })]) {
const res = fakeRes(); await h({ method: 'POST', url: '/' }, res); assert.equal(res.code, 405);
}
});