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>
73 lines
5.3 KiB
JavaScript
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);
|
|
}
|
|
});
|