(function() { function esc(v) { return String(v === undefined || v === null ? '' : v) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function short(v, n) { if (!v) return '-'; return v.length > n ? v.slice(0, n) + '...' : v; } function fmtTime(iso) { if (!iso) return '-'; return iso.replace('T', ' ').replace('Z', ' UTC'); } function timeAgo(iso) { if (!iso) return '-'; var now = Date.now(); var ts = new Date(iso).getTime(); if (!isFinite(ts)) return '-'; var diffSec = Math.max(0, Math.floor((now - ts) / 1000)); if (diffSec < 10) return 'a few seconds ago'; if (diffSec < 60) return diffSec + ' seconds ago'; var diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return diffMin === 1 ? 'a minute ago' : diffMin + ' minutes ago'; var diffHour = Math.floor(diffMin / 60); if (diffHour < 24) return diffHour === 1 ? 'an hour ago' : diffHour + ' hours ago'; var diffDay = Math.floor(diffHour / 24); if (diffDay === 1) return 'yesterday'; if (diffDay < 30) return diffDay + ' days ago'; var diffMonth = Math.floor(diffDay / 30); if (diffMonth < 12) return diffMonth === 1 ? 'a month ago' : diffMonth + ' months ago'; var diffYear = Math.floor(diffMonth / 12); return diffYear === 1 ? 'a year ago' : diffYear + ' years ago'; } function shortAddress(addr) { if (!addr) return '-'; if (addr.length <= 9) return addr; return addr.slice(0, 3) + '...' + addr.slice(-3); } function txLabel(eventType) { var map = { TRANSFER: 'Transfer', REGISTER_KEY: 'Register', CREATE_CHANNEL: 'Create Channel', ADD_MEMBER: 'Add Member', OPEN_PAY_CHAN: 'Open Channel', CLOSE_PAY_CHAN: 'Close Channel', RELAY_PROOF: 'Relay Proof', BIND_WALLET: 'Bind Wallet', SLASH: 'Slash', HEARTBEAT: 'Heartbeat', BLOCK_REWARD: 'Reward' }; return map[eventType] || eventType || 'Transaction'; } function toToken(micro) { if (micro === undefined || micro === null) return '-'; var n = Number(micro); if (isNaN(n)) return String(micro); return (n / 1000000).toFixed(6) + ' T'; } // Compact token display: 21000000 T → "21M T", 1234 T → "1.23k T" function _fmtNum(n) { if (n >= 100) return Math.round(n).toString(); if (n >= 10) return n.toFixed(1).replace(/\.0$/, ''); return n.toFixed(2).replace(/\.?0+$/, ''); } function toTokenShort(micro) { if (micro === undefined || micro === null) return '-'; var t = Number(micro) / 1000000; if (isNaN(t)) return String(micro); if (t >= 1e9) return _fmtNum(t / 1e9) + 'B T'; if (t >= 1e6) return _fmtNum(t / 1e6) + 'M T'; if (t >= 1e3) return _fmtNum(t / 1e3) + 'k T'; if (t >= 0.01) return _fmtNum(t) + ' T'; var ut = Number(micro); if (ut >= 1000) return _fmtNum(ut / 1000) + 'k µT'; return ut + ' µT'; } function isPubKey(s) { return /^[0-9a-fA-F]{64}$/.test(s || ''); } function setStatus(text, cls) { var el = document.getElementById('status'); if (!el) return; el.className = 'status' + (cls ? ' ' + cls : ''); el.textContent = text; } async function fetchJSON(url) { var resp = await fetch(url); var json = await resp.json().catch(function() { return {}; }); if (!resp.ok) { throw new Error(json && json.error ? json.error : 'request failed'); } if (json && json.error) { throw new Error(json.error); } return json; } function q(name) { return new URLSearchParams(window.location.search).get(name) || ''; } function navAddress(address) { window.location.href = '/address?address=' + encodeURIComponent(address); } function navTx(id) { window.location.href = '/tx?id=' + encodeURIComponent(id); } function navNode(node) { window.location.href = '/node?node=' + encodeURIComponent(node); } function refreshIcons() { if (window.lucide && typeof window.lucide.createIcons === 'function') { window.lucide.createIcons(); } } // ── SSE live event stream ───────────────────────────────────────────────── // // connectSSE(handlers) opens a connection to GET /api/events and dispatches // events to the supplied handler map, e.g.: // // C.connectSSE({ // block: function(data) { ... }, // tx: function(data) { ... }, // contract_log: function(data) { ... }, // connected: function() { /* SSE connection established */ }, // error: function() { /* connection lost */ }, // }); // // Returns the EventSource instance so the caller can close it if needed. function connectSSE(handlers) { if (!window.EventSource) return null; // browser doesn't support SSE var es = new EventSource('/api/events'); es.addEventListener('open', function() { if (handlers.connected) handlers.connected(); }); es.addEventListener('error', function() { if (handlers.error) handlers.error(); }); ['block', 'tx', 'contract_log'].forEach(function(type) { if (!handlers[type]) return; es.addEventListener(type, function(e) { try { var data = JSON.parse(e.data); handlers[type](data); } catch (_) {} }); }); return es; } window.ExplorerCommon = { esc: esc, short: short, fmtTime: fmtTime, timeAgo: timeAgo, shortAddress: shortAddress, txLabel: txLabel, toToken: toToken, toTokenShort: toTokenShort, isPubKey: isPubKey, setStatus: setStatus, fetchJSON: fetchJSON, q: q, navAddress: navAddress, navTx: navTx, navNode: navNode, refreshIcons: refreshIcons, connectSSE: connectSSE }; refreshIcons(); })();