// Igneum explorer: the search router and the formatters shared by /api/explorer and the pages (the pages inline the // same functions; site/lib/explorer.test.mjs checks both). Zero dependencies. const HEX64 = /^(0x)?[0-9a-f]{64}$/i; const HEX40 = /^0x[0-9a-f]{40}$/i; const BECH = /^(igneum|igneumdev|igneumtest|igneumsim):[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{20,}$/i; const DIGITS = /^\d{1,12}$/; /** * Classifies what someone typed in the search box. * 64 hex (0x optional) -> {type: 'hash'} a DAG block hash, or an EVM transaction hash; the API tries the block first * 0x + 40 hex -> {type: 'address'} an EVM payout address * igneum...:bech32 -> {type: 'address'} the coinbase's UTXO address * digits -> {type: 'height'} a chain block number (the EVM block number) * anything else -> {type: 'none'} * `value` is the normalised form: hashes without 0x and lower case, 0x addresses lower case, numbers as Number. */ export function classify(input) { const q = String(input || '').trim(); if (!q) return { type: 'none', value: null }; if (HEX64.test(q)) return { type: 'hash', value: q.replace(/^0x/i, '').toLowerCase() }; if (HEX40.test(q)) return { type: 'address', value: q.toLowerCase(), kind: 'evm' }; if (BECH.test(q)) return { type: 'address', value: q.toLowerCase(), kind: 'utxo' }; if (DIGITS.test(q)) return { type: 'height', value: Number(q) }; return { type: 'none', value: null }; } /** The page a classified search leads to. A hash goes to /block/; the API answers 404 with a hint when it is not a block. */ export function hrefFor(c) { if (c.type === 'hash') return `/block/${c.value}`; if (c.type === 'address') return `/address/${c.value}`; if (c.type === 'height') return `/block/${c.value}`; return null; } /** sompi (8 decimals) as IGN text, trailing zeros trimmed, at most `places` places. */ export function ign(sompi, places = 4) { if (sompi === null || sompi === undefined) return 'n/a'; let v; try { v = BigInt(sompi); } catch { return 'n/a'; } const neg = v < 0n; if (neg) v = -v; const whole = v / 100000000n, frac = (v % 100000000n).toString().padStart(8, '0').slice(0, places).replace(/0+$/, ''); return (neg ? '-' : '') + whole.toLocaleString('en-GB') + (frac ? '.' + frac : ''); } /** wei (18 decimals) as IGN text. */ export function ignFromWei(wei, places = 4) { if (wei === null || wei === undefined) return 'n/a'; let v; try { v = BigInt(wei); } catch { return 'n/a'; } const neg = v < 0n; if (neg) v = -v; const U = 10n ** 18n; const whole = v / U, frac = (v % U).toString().padStart(18, '0').slice(0, places).replace(/0+$/, ''); return (neg ? '-' : '') + whole.toLocaleString('en-GB') + (frac ? '.' + frac : ''); } /** First and last characters of a hash or address: 8 and 6 by default. */ export function shortHash(h, head = 8, tail = 6) { const s = String(h || ''); if (!s) return ''; if (s.length <= head + tail + 1) return s; return s.slice(0, head) + '…' + s.slice(-tail); } /** A hash rate in H/s with a unit: 27,759,288 -> "27.8 MH/s". */ export function hashrate(n) { if (n === null || n === undefined || isNaN(n)) return 'n/a'; const u = ['H/s', 'kH/s', 'MH/s', 'GH/s', 'TH/s', 'PH/s']; let i = 0; let v = Number(n); while (v >= 1000 && i < u.length - 1) { v /= 1000; i++; } return (i ? v.toFixed(1) : String(Math.round(v))) + ' ' + u[i]; } /** A relative time from a millisecond difference: "12 s ago", "3 min ago", "2 h 5 min ago", "4 d ago". */ export function rel(ms) { if (ms === null || ms === undefined || isNaN(ms)) return 'n/a'; const s = Math.max(0, Math.round(ms / 1000)); if (s < 60) return `${s} s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m} min ago`; const h = Math.floor(m / 60); if (h < 24) return `${h} h ${m % 60} min ago`; return `${Math.floor(h / 24)} d ago`; } /** Thousands-separated integer, or n/a. */ export function int(n) { if (n === null || n === undefined || isNaN(n)) return 'n/a'; return Math.round(Number(n)).toLocaleString('en-GB'); } /** A millisecond timestamp as "2026-10-05 19:21:56 UTC". */ export function utc(ms) { if (!ms) return 'n/a'; const d = new Date(Number(ms)); if (isNaN(d)) return 'n/a'; return d.toISOString().slice(0, 19).replace('T', ' ') + ' UTC'; }