spb/coinexplorer Public MIT
Self-hosted, zero-API-key explorer for stablecoins and major crypto.
Python 60.3%
HTML 23.6%
JavaScript 8.1%
CSS 6.8%
SQL 1%
1<!doctype html>2<!-- Author: Simon-Pierre Boucher -->3<!-- Mail: contact@spboucher.ai -->4<html lang="en">5<head>6<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">7<title>chain — coinexplorer</title>8<link rel="stylesheet" href="/assets/style.css">9</head>10<body>11<main>12 <div class="pagehead"><h1 id="title">Chain</h1><span class="sub" id="metaNote"></span></div>13 <div class="filters"><select id="chainSel" aria-label="Chain"></select></div>1415 <section class="card tile"><h2>Transfers indexed</h2>16 <div class="big" id="tCount">—</div><div class="note">stablecoin transfers stored</div></section>17 <section class="card tile"><h2>Volume · 24h</h2>18 <div class="big" id="tVol">—</div><div class="note" id="tVolN"></div></section>19 <section class="card tile"><h2>Indexer lag</h2>20 <div class="big" id="tLag">—</div><div class="note" id="tLagN"></div></section>21 <section class="card tile"><h2>Coverage floor</h2>22 <div class="big" id="tFloor">—</div><div class="note">backfilled down to this block</div></section>2324 <section class="card half"><h2>24h volume by token</h2><div class="bars" id="tokenBars"></div></section>25 <section class="card half"><h2>Assets on this chain</h2>26 <div class="scroll"><table>27 <thead><tr><th>token</th><th>identifier</th><th>issuance</th><th class="num">decimals</th></tr></thead>28 <tbody id="tokens"></tbody></table></div></section>2930 <section class="card"><h2>Recent transfers</h2>31 <div class="filters" style="padding:0 0 8px">32 <select id="symFilter"><option value="">all tokens</option></select>33 <input id="minUsd" type="number" placeholder="min USD" style="width:110px">34 <button class="btn" id="apply">Apply</button>35 </div>36 <div class="scroll"><table>37 <thead><tr><th>block</th><th>token</th><th class="num">amount</th><th>from</th><th>to</th><th>tx</th><th>when</th></tr></thead>38 <tbody id="transfers"></tbody></table></div>39 <div style="margin-top:8px"><button class="btn" id="more">Load more</button></div></section>4041 <section class="card"><h2>RPC endpoints</h2>42 <div class="scroll"><table>43 <thead><tr><th>endpoint</th><th class="num">score</th><th class="num">ok</th><th class="num">fail</th><th class="num">latency</th><th class="num">cooldown</th></tr></thead>44 <tbody id="rpc"></tbody></table></div></section>45</main>4647<script type="module">48import { $, qs, j, fmtUsd, fmtN, fmtAmt, mountNav, hbars, addrLink, txLink, tokenLink, chainIcon, ago } from "/assets/app.js";49mountNav("Chains");50let CHAIN = qs.get("chain") || "ethereum";51let beforeBlock = null;5253async function refresh() {54 document.title = `${CHAIN} — coinexplorer`;55 const [sum, status, health] = await Promise.all([56 j(`/v1/${CHAIN}/summary`), j("/v1/status"), j("/v1/rpc/health"),57 ]);58 $("title").innerHTML = `${chainIcon(CHAIN, 26)} ${CHAIN}`;59 $("metaNote").textContent = `${sum.family}${sum.chain_id ? " · chain id " + sum.chain_id : ""} · ${sum.block_time_s}s blocks`;60 $("tCount").textContent = fmtN(sum.indexed_transfers);61 const vol24 = sum.volume_24h.reduce((a, r) => a + r.volume, 0);62 $("tVol").textContent = fmtUsd(vol24);63 $("tVolN").textContent = `${fmtN(sum.volume_24h.reduce((a, r) => a + r.transfers, 0))} transfers`;64 const cur = status.cursors[CHAIN] || {};65 $("tLag").textContent = cur.lag != null ? fmtN(cur.lag) + " blocks" : "—";66 $("tLagN").textContent = cur.last_block ? `at block ${fmtN(cur.last_block)}` : "not indexing";67 $("tFloor").textContent = cur.backfill_block != null ? fmtN(cur.backfill_block) : "—";6869 hbars($("tokenBars"), sum.volume_24h.map((r) =>70 ({ k: r.symbol, v: r.volume, link: tokenLink(r.symbol),71 extra: `<br>${fmtN(r.transfers)} transfers` })));7273 $("tokens").innerHTML = sum.tokens.map((t) => {74 const id = t.address || t.id;75 const cls = t.discontinued ? "discontinued" : t.native ? "native" : "bridged";76 return `<tr><td>${tokenLink(t.symbol)}</td><td class="mono" title="${id}">${id}</td>77 <td><span class="badge ${cls}">${cls}</span></td><td class="num">${t.decimals ?? "—"}</td></tr>`;78 }).join("");7980 $("symFilter").innerHTML = `<option value="">all tokens</option>` +81 [...new Set(sum.tokens.map((t) => t.symbol))].sort().map((s) => `<option>${s}</option>`).join("");8283 $("rpc").innerHTML = (health[CHAIN] || []).map((e) =>84 `<tr><td class="mono" style="max-width:none" title="${e.url}">${e.url}</td>85 <td class="num">${e.score}</td><td class="num">${fmtN(e.ok)}</td><td class="num">${fmtN(e.fail)}</td>86 <td class="num">${e.latency_ms} ms</td><td class="num">${e.cooldown_s > 0 ? e.cooldown_s + "s" : "—"}</td></tr>`).join("") ||87 `<tr><td colspan="6" class="empty">no health data yet</td></tr>`;88}8990async function loadTransfers(append = false) {91 const sym = $("symFilter").value, min = $("minUsd").value;92 let url = `/v1/${CHAIN}/transfers?limit=50`;93 if (sym) url += `&symbol=${sym}`;94 if (min) url += `&min_amount=${min}`;95 if (append && beforeBlock) url += `&before_block=${beforeBlock}`;96 const rows = await j(url);97 if (rows.length) beforeBlock = rows[rows.length - 1].block;98 const html = rows.map((r) =>99 `<tr><td>${fmtN(r.block)}</td><td>${tokenLink(r.symbol)}</td>100 <td class="num">${fmtAmt(r)}</td><td>${addrLink(r.from)}</td>101 <td>${addrLink(r.to)}</td><td>${txLink(r.chain, r.tx_hash)}</td>102 <td>${ago(r.timestamp)} ago</td></tr>`).join("");103 if (append) $("transfers").insertAdjacentHTML("beforeend", html);104 else $("transfers").innerHTML = html || `<tr><td colspan="7" class="empty">nothing indexed yet</td></tr>`;105}106107j("/v1/chains").then((chains) => {108 $("chainSel").innerHTML = Object.keys(chains).sort()109 .map((c) => `<option ${c === CHAIN ? "selected" : ""}>${c}</option>`).join("");110});111$("chainSel").addEventListener("change", (e) => {112 CHAIN = e.target.value; beforeBlock = null;113 history.replaceState(null, "", `?chain=${CHAIN}`);114 refresh(); loadTransfers();115});116$("apply").addEventListener("click", () => { beforeBlock = null; loadTransfers(); });117$("more").addEventListener("click", () => loadTransfers(true));118refresh(); loadTransfers();119setInterval(refresh, 30_000);120</script>121</body>122</html>123