SPB Git

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%
6.1 KB · 126 lines html
Raw Blame History
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>coinexplorer — stablecoins & major crypto</title>8<link rel="stylesheet" href="/assets/style.css">9</head>10<body>11<main>12  <div class="pagehead"><h1>Market overview</h1>13    <span class="sub">on-chain data, indexed from free public RPCs</span></div>1415  <div class="filters">16    <select id="tokenSel" aria-label="Token"></select>17    <div class="presets" role="group" aria-label="Window">18      <button data-w="1h">1h</button><button data-w="24h" aria-pressed="true">24h</button><button data-w="7d">7d</button>19    </div>20  </div>2122  <section class="card tile"><h2>Tracked supply</h2>23    <div class="big" id="tSupply">—</div><div class="note" id="tSupplyN">all stablecoins, all chains</div></section>24  <section class="card tile"><h2>Volume <span class="sub" id="tVolW"></span></h2>25    <div class="big" id="tVol">—</div><div class="note" id="tVolN"></div></section>26  <section class="card tile"><h2>Transfers indexed</h2>27    <div class="big" id="tCount">—</div><div class="note">all tokens · all chains</div></section>28  <section class="card tile"><h2>Chains live</h2>29    <div class="big" id="tChains">—</div><div class="note" id="tChainsN"></div></section>3031  <section class="card twothird"><h2 id="volTitle">Volume over time</h2><div id="volChart"></div></section>32  <section class="card third"><h2>Assets by tracked supply (USD)</h2><div class="bars" id="supplyBars"></div></section>3334  <section class="card half"><h2 id="chainVolTitle">Volume by chain</h2><div class="bars" id="chainBars"></div></section>35  <section class="card half"><h2>Live transfers <span class="sub" id="wsState">connecting…</span></h2>36    <div class="scroll"><table>37      <thead><tr><th>chain</th><th>token</th><th class="num">amount</th><th>from → to</th><th>tx</th></tr></thead>38      <tbody id="feed"></tbody></table></div></section>3940  <section class="card"><h2>Largest transfers <span class="sub" id="whaleW"></span></h2>41    <div class="scroll"><table>42      <thead><tr><th>chain</th><th>token</th><th class="num">USD</th><th>from</th><th>to</th><th>tx</th><th>when</th></tr></thead>43      <tbody id="whales"></tbody></table></div></section>44</main>4546<script type="module">47import { $, j, fmtUsd, fmtN, fmtAmt, mountNav, hbars, lineChart, topSeries,48         addrLink, txLink, chainLink, tokenLink, ago } from "/assets/app.js";49mountNav("Overview");50let TOKEN = "USDT", WINDOW = "24h";51const ivl = () => ({ "1h": "5m", "24h": "1h", "7d": "6h" }[WINDOW]);5253async function refresh() {54  const [supplyAll, vol, status, series] = await Promise.all([55    j("/v1/stablecoins/supply"),56    j(`/v1/stablecoins/volume?token=${TOKEN}&window=${WINDOW}`),57    j("/v1/status"),58    j(`/v1/stablecoins/volume/series?token=${TOKEN}&window=${WINDOW}&interval=${ivl()}`),59  ]);60  const totalSupply = Object.values(supplyAll).reduce((a, s) => a + (s.total_usd ?? s.total), 0);61  $("tSupply").textContent = fmtUsd(totalSupply);62  $("tVol").textContent = fmtUsd(vol.total_volume);63  $("tVolW").textContent = "· " + WINDOW;64  $("tVolN").textContent = `${TOKEN} · ${fmtN(Object.values(vol.chains).reduce((a, c) => a + c.transfers, 0))} transfers`;65  $("tCount").textContent = fmtN(Object.values(status.indexed_transfers).reduce((a, b) => a + b, 0));66  $("tChains").textContent = Object.keys(status.cursors).length;67  const lagging = Object.values(status.cursors).filter((c) => c.lag > 100).length;68  $("tChainsN").textContent = lagging ? `${lagging} catching up` : "all at head";6970  $("volTitle").textContent = `${TOKEN} volume — ${WINDOW}`;71  lineChart($("volChart"), topSeries(series.chains, 3));7273  hbars($("supplyBars"), Object.entries(supplyAll)74    .map(([k, s]) => ({ k, v: s.total_usd ?? s.total, link: tokenLink(k) }))75    .sort((a, b) => b.v - a.v).slice(0, 11));7677  $("chainVolTitle").textContent = `${TOKEN} volume by chain — ${WINDOW}`;78  hbars($("chainBars"), Object.entries(vol.chains)79    .map(([k, c]) => ({ k, v: c.volume, link: chainLink(k),80      extra: `<br>${fmtN(c.transfers)} transfers` }))81    .sort((a, b) => b.v - a.v).slice(0, 12));82}8384async function refreshWhales() {85  const w = await j(`/v1/stablecoins/whales?window=${WINDOW}&limit=30`);86  $("whaleW").textContent = "· " + WINDOW;87  $("whales").innerHTML = w.map((x) =>88    `<tr><td>${chainLink(x.chain)}</td><td>${tokenLink(x.symbol)}</td>89     <td class="num"><b>${fmtUsd(x.usd)}</b></td><td>${addrLink(x.from)}</td>90     <td>${addrLink(x.to)}</td><td>${txLink(x.chain, x.tx_hash)}</td>91     <td>${ago(x.timestamp)} ago</td></tr>`).join("") ||92    `<tr><td colspan="7" class="empty">none in window</td></tr>`;93}9495function connectWS() {96  const ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/v1/stream/transfers?min_usd=500`);97  ws.onopen = () => $("wsState").textContent = "· live";98  ws.onclose = () => { $("wsState").textContent = "· reconnecting…"; setTimeout(connectWS, 3000); };99  ws.onmessage = (ev) => {100    const feed = $("feed");101    for (const r of JSON.parse(ev.data).slice(-25).reverse()) {102      const tr = document.createElement("tr");103      tr.innerHTML = `<td>${chainLink(r.chain)}</td>104        <td>${tokenLink(r.symbol)}</td><td class="num">${fmtAmt(r)}</td>105        <td>${addrLink(r.from)} → ${addrLink(r.to)}</td><td>${txLink(r.chain, r.tx_hash)}</td>`;106      feed.prepend(tr);107    }108    while (feed.rows.length > 60) feed.deleteRow(-1);109  };110}111112j("/v1/tokens").then((t) => {113  $("tokenSel").innerHTML = Object.keys(t).sort()114    .map((s) => `<option ${s === TOKEN ? "selected" : ""}>${s}</option>`).join("");115});116$("tokenSel").addEventListener("change", (e) => { TOKEN = e.target.value; refresh(); });117document.querySelectorAll(".presets button").forEach((b) => b.addEventListener("click", () => {118  document.querySelectorAll(".presets button").forEach((x) => x.setAttribute("aria-pressed", "false"));119  b.setAttribute("aria-pressed", "true"); WINDOW = b.dataset.w; refresh(); refreshWhales();120}));121refresh(); refreshWhales(); connectWS();122setInterval(refresh, 30_000); setInterval(refreshWhales, 60_000);123</script>124</body>125</html>126