// Igneum emission, spec 2.5 (docs/spec/02-consensus.md) as the node computes it // (vendor/igneum-node/consensus/core/src/igneum.rs: block_subsidy, launch_ramp). Shared by the observer's coinbase // check (tools/observer/observer.mjs) and the public /api/supply. BigInt throughout; sompi is the 8-decimal unit the // devnet pays in (open item O-2.6 keeps Kaspa's SOMPI_PER_KASPA), the EVM side shows the same amount at 18 decimals. export const SOMPI_PER_IGN = 100_000_000n; export const SECONDS_PER_YEAR = 31_557_600n; // 365.25 days export const HALVING_INTERVAL_SECONDS = 2n * SECONDS_PER_YEAR; // 63,115,200 export const YEAR_ONE_EMISSION_SOMPI = 1_000_000_000n * SOMPI_PER_IGN; export const BASE_SUBSIDY_PER_SECOND_SOMPI = YEAR_ONE_EMISSION_SOMPI / SECONDS_PER_YEAR; // 3,168,808,781 (floor) export const SUPPLY_CAP_SOMPI = 4_000_000_000n * SOMPI_PER_IGN; export const LAUNCH_RAMP_SECONDS = 2_592_000n; // 30 days export const LAUNCH_RAMP_START_PERCENT = 10n; export const PROVING_POOL_SHARE_PERCENT = 20n; export const SUBSIDY_PERIODS = 33; // period 32 pays 0 /** Per-second subsidy of two-year period i: BASE >> i (igneum.rs SUBSIDY_PER_SECOND_BY_PERIOD). */ export function subsidyPerSecond(period) { const i = Math.min(Math.max(0, period), SUBSIDY_PERIODS - 1); return BASE_SUBSIDY_PER_SECOND_SOMPI >> BigInt(i); } /** igneum.rs launch_ramp: linear from 10% at second 0 to 100% at LAUNCH_RAMP_SECONDS, integer floor. */ export function launchRamp(fullSubsidy, secondsSinceGenesis) { const s = BigInt(secondsSinceGenesis); if (s >= LAUNCH_RAMP_SECONDS) return fullSubsidy; const numerator = LAUNCH_RAMP_START_PERCENT * LAUNCH_RAMP_SECONDS + (100n - LAUNCH_RAMP_START_PERCENT) * s; const denominator = 100n * LAUNCH_RAMP_SECONDS; return fullSubsidy * numerator / denominator; } /** igneum.rs block_subsidy(daa_score, bps): per-block subsidy in sompi paid for each blue block merged at this DAA score. */ export function blockSubsidy(daaScore, bps = 1) { const b = BigInt(bps); const seconds = BigInt(daaScore) / b; const period = Number(seconds / HALVING_INTERVAL_SECONDS); const perBlock = subsidyPerSecond(period) / b; return launchRamp(perBlock, seconds); } /** The ramp factor at a DAA score as a decimal in [0.1, 1], for display. */ export function rampFactor(daaScore) { const s = BigInt(daaScore); if (s >= LAUNCH_RAMP_SECONDS) return 1; return Number(LAUNCH_RAMP_START_PERCENT * LAUNCH_RAMP_SECONDS + (100n - LAUNCH_RAMP_START_PERCENT) * s) / Number(100n * LAUNCH_RAMP_SECONDS); } // sum_{i=0}^{n-1} floor((a*i + b) / m), exact in O(log) (the classic floor-sum; a, b >= 0, m > 0, BigInt). export function floorSum(n, m, a, b) { let ans = 0n; for (;;) { if (a >= m) { ans += (n - 1n) * n / 2n * (a / m); a %= m; } if (b >= m) { ans += n * (b / m); b %= m; } const yMax = a * n + b; if (yMax < m) return ans; const n2 = yMax / m, b2 = yMax % m; [n, m, a, b] = [n2, a, m, b2]; } } /** * Coins minted by the rule from DAA 0 up to (not including) `daaScore`, in sompi, at `bps` = 1: the sum of * blockSubsidy(t) over every DAA second t. Exact: the ramp part is a floor-sum, the rest is per-period arithmetic. * This is the schedule; the chain pays blockSubsidy per blue block it merges (and per red inside the window), so * the paid total tracks it one block per DAA step. The observer's coinbase check (live_state.supply_check) reports * the difference it sees. */ export function mintedByRule(daaScore) { let t = BigInt(daaScore); if (t <= 0n) return 0n; let total = 0n; // ramp: per-second = floor(B * (10R + 90 s) / (100 R)) = floor((90B s + 10RB) / (100R)) const B = BASE_SUBSIDY_PER_SECOND_SOMPI, R = LAUNCH_RAMP_SECONDS; const rampSeconds = t < R ? t : R; total += floorSum(rampSeconds, 100n * R, 90n * B, 10n * R * B); if (t <= R) return total; // after the ramp, period by period let from = R; while (from < t) { const period = Number(from / HALVING_INTERVAL_SECONDS); const periodEnd = BigInt(period + 1) * HALVING_INTERVAL_SECONDS; const to = t < periodEnd ? t : periodEnd; total += (to - from) * subsidyPerSecond(period); from = to; } return total; } /** Minted by the rule when every period has paid out: the cap minus the ramp's withheld part and the floors. */ export function mintedAtEnd() { return mintedByRule(BigInt(SUBSIDY_PERIODS) * HALVING_INTERVAL_SECONDS); } /** The halving table: one row per two-year period until the per-second subsidy is 0. */ export function halvingTable(maxRows = SUBSIDY_PERIODS) { const rows = []; for (let i = 0; i < Math.min(maxRows, SUBSIDY_PERIODS); i++) { const start = BigInt(i) * HALVING_INTERVAL_SECONDS; rows.push({ period: i, start_daa: start, end_daa: start + HALVING_INTERVAL_SECONDS, per_second_sompi: subsidyPerSecond(i), minted_by_end_sompi: mintedByRule(start + HALVING_INTERVAL_SECONDS), }); } return rows; } /** sompi -> decimal IGN string with up to 8 places, no exponent, no rounding. */ export function sompiToIgn(sompi, places = 8) { const v = BigInt(sompi); const neg = v < 0n; const a = neg ? -v : v; const whole = a / SOMPI_PER_IGN, frac = (a % SOMPI_PER_IGN).toString().padStart(8, '0').slice(0, places).replace(/0+$/, ''); return (neg ? '-' : '') + whole.toString() + (frac ? '.' + frac : ''); } /** wei (18 decimals) -> decimal IGN string, up to `places` places. */ export function weiToIgn(wei, places = 6) { const v = BigInt(wei); const neg = v < 0n; const a = neg ? -v : v; const U = 10n ** 18n; const whole = a / U, frac = (a % U).toString().padStart(18, '0').slice(0, places).replace(/0+$/, ''); return (neg ? '-' : '') + whole.toString() + (frac ? '.' + frac : ''); }