// Igneum supply. GET /api/supply: coins minted so far by the emission rule at the node's DAA score, the hard cap, the // halving schedule as a table and the 30-day launch ramp (docs/spec/02-consensus.md 2.5, consensus/core/src/igneum.rs), // plus the observer's hourly check of the chain's coinbase sums against that rule (live_state.supply_check). Cached // 10 s at the edge. Documented in docs/api/public-stats.md. import { neon, num, tablePrefix, chainIdOf } from './_neon.mjs'; import * as E from '../lib/emission.mjs'; export const FIELDS = ['ok', 'now', 'network', 'chain_id', 'unit', 'daa', 'max_supply_ign', 'circulating_ign', 'circulating_sompi', 'minted_at_end_ign', 'never_minted_ign', 'emission_per_second_ign', 'block_reward_ign', 'halving', 'ramp', 'schedule', 'check', 'rule', 'source', 'note']; export const SCHEDULE_FIELDS = ['period', 'start_daa', 'end_daa', 'years_from_genesis', 'per_second_ign', 'per_block_ign_at_1bps', 'minted_by_end_ign', 'share_of_cap_by_end']; const S = Number(E.SECONDS_PER_YEAR); export function shape({ now, state: s, daa }) { const minted = daa === null ? null : E.mintedByRule(daa); const end = E.mintedAtEnd(); const period = daa === null ? null : Math.floor(daa / Number(E.HALVING_INTERVAL_SECONDS)); const nextHalving = period === null ? null : (period + 1) * Number(E.HALVING_INTERVAL_SECONDS); const ramp = daa === null ? null : Number(E.LAUNCH_RAMP_SECONDS) - daa; const schedule = E.halvingTable().map(r => ({ period: r.period, start_daa: Number(r.start_daa), end_daa: Number(r.end_daa), years_from_genesis: `${Number(r.start_daa) / S} to ${Number(r.end_daa) / S}`, per_second_ign: E.sompiToIgn(r.per_second_sompi), per_block_ign_at_1bps: E.sompiToIgn(r.per_second_sompi), minted_by_end_ign: E.sompiToIgn(r.minted_by_end_sompi), share_of_cap_by_end: Math.round(Number(r.minted_by_end_sompi * 1_000_000n / E.SUPPLY_CAP_SOMPI)) / 10_000, })); return { ok: true, now: new Date(now).toISOString(), network: s ? s.network : null, chain_id: s ? chainIdOf(s.network) : null, unit: { symbol: 'IGN', decimals_consensus: 8, decimals_evm: 18, note: 'the coinbase pays in 8-decimal units (open item O-2.6 keeps Kaspa\'s SOMPI_PER_KASPA); the EVM shows the same amount at 18 decimals' }, daa, max_supply_ign: E.sompiToIgn(E.SUPPLY_CAP_SOMPI), circulating_ign: minted === null ? null : E.sompiToIgn(minted), circulating_sompi: minted === null ? null : minted.toString(), minted_at_end_ign: E.sompiToIgn(end), never_minted_ign: E.sompiToIgn(E.SUPPLY_CAP_SOMPI - end), emission_per_second_ign: daa === null ? null : E.sompiToIgn(E.blockSubsidy(daa, 1)), block_reward_ign: daa === null ? null : E.sompiToIgn(E.blockSubsidy(daa, 1)), halving: { interval_daa_s: Number(E.HALVING_INTERVAL_SECONDS), interval_years: 2, period, next_halving_daa: nextHalving, next_halving_in_s: nextHalving === null ? null : nextHalving - daa, next_halving_estimate: nextHalving === null ? null : new Date(now + (nextHalving - daa) * 1000).toISOString(), estimate_note: 'the estimate assumes one DAA second per wall-clock second from now', }, ramp: { start_percent: Number(E.LAUNCH_RAMP_START_PERCENT), length_daa_s: Number(E.LAUNCH_RAMP_SECONDS), length_days: 30, factor_now: daa === null ? null : Math.round(E.rampFactor(daa) * 1e6) / 1e6, complete: daa === null ? null : daa >= Number(E.LAUNCH_RAMP_SECONDS), remaining_s: ramp === null ? null : Math.max(0, ramp), withheld_ign: E.sompiToIgn(E.SUPPLY_CAP_SOMPI - end), withheld_note: 'the ramp withholds about 37 million IGN that are never minted; integer floors withhold the rest (spec 2.5)', }, schedule, check: s && s.supply_check ? s.supply_check : null, rule: 'E(t) = ramp(t) * floor(10^9 * UNIT / 31,557,600) >> floor(t / 63,115,200); ramp(t) = min(1, 1/10 + 9/10 * t / 2,592,000); t = DAA score / bps, bps = 1', source: 'docs/spec/02-consensus.md 2.5; vendor/igneum-node consensus/core/src/igneum.rs block_subsidy and launch_ramp; circulating = the rule summed over every DAA second from 0 to the node\'s DAA score (site/lib/emission.mjs mintedByRule, exact)', note: 'circulating is the schedule at this DAA score. The chain pays E per blue block it merges and per red inside the DAA window, which tracks the schedule one block per DAA step; the devnet of 3 October 2026 ran 4.7x the schedule for eight minutes during a retarget lag (spec 2.5). check reports what the observer measured on the newest blocks.', }; } export function createHandler({ env = process.env, sql } = {}) { return async function handler(req, res) { res.setHeader('Cache-Control', 'public, max-age=10, s-maxage=10'); res.setHeader('Access-Control-Allow-Origin', '*'); if (req.method !== 'GET') { res.setHeader('Allow', 'GET'); return res.status(405).json({ ok: false, error: 'method not allowed' }); } try { sql = sql || neon(env.DATABASE_URL); const T = tablePrefix(); const rows = await sql(`SELECT now() AS now, (SELECT row_to_json(s) FROM ${T}live_state s WHERE s.id = 1) AS state, (SELECT max(daa_score) FROM ${T}live_blocks WHERE received_at > now() - interval '10 minutes') AS daa`); const now = new Date(rows[0].now).getTime(); return res.status(200).json(shape({ now, state: rows[0].state || null, daa: num(rows[0].daa) })); } catch (e) { res.setHeader('Cache-Control', 'no-store'); return res.status(500).json({ ok: false, error: String(e.message || e) }); } }; } export default createHandler();