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.8 KB · 131 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>token — coinexplorer</title>8<link rel="stylesheet" href="/assets/style.css">9</head>10<body>11<main>12  <div class="pagehead"><h1 id="title">Token</h1><span class="sub" id="issuerNote"></span></div>13  <div class="filters">14    <select id="tokenSel" aria-label="Token"></select>15    <div class="presets" role="group" aria-label="Window">16      <button data-w="24h" aria-pressed="true">24h</button><button data-w="7d">7d</button>17    </div>18  </div>1920  <section class="card tile"><h2>On-chain supply</h2>21    <div class="big" id="tSupply">—</div><div class="note" id="tSupplyN"></div></section>22  <section class="card tile"><h2>Volume <span class="sub" id="tVolW"></span></h2>23    <div class="big" id="tVol">—</div><div class="note" id="tVolN"></div></section>24  <section class="card tile"><h2>Active senders <span class="sub" id="tActW"></span></h2>25    <div class="big" id="tAct">—</div><div class="note">distinct sending addresses</div></section>26  <section class="card tile"><h2>Net issuance <span class="sub" id="tNetW"></span></h2>27    <div class="big" id="tNet">—</div><div class="note" id="tNetN"></div></section>2829  <section class="card half"><h2 id="volTitle">Volume over time</h2><div id="volChart"></div></section>30  <section class="card half"><h2 id="supTitle">Supply over time</h2><div id="supChart"></div></section>3132  <section class="card third"><h2>Supply by chain</h2><div class="bars" id="supplyBars"></div></section>33  <section class="card twothird"><h2>Largest transfers <span class="sub" id="whaleW"></span></h2>34    <div class="scroll"><table>35      <thead><tr><th>chain</th><th class="num">USD</th><th>from</th><th>to</th><th>tx</th><th>when</th></tr></thead>36      <tbody id="whales"></tbody></table></div></section>3738  <section class="card half"><h2>Mints &amp; burns <span class="sub" id="mbW"></span></h2>39    <div class="scroll"><table>40      <thead><tr><th></th><th>chain</th><th class="num">amount</th><th>tx</th><th>when</th></tr></thead>41      <tbody id="mintburn"></tbody></table></div></section>42  <section class="card half"><h2>Deployments by chain</h2>43    <div class="scroll"><table>44      <thead><tr><th>chain</th><th>identifier</th><th>issuance</th><th class="num">decimals</th></tr></thead>45      <tbody id="deploys"></tbody></table></div></section>46</main>4748<script type="module">49import { $, qs, j, fmtUsd, fmtN, fmtAmt, mountNav, hbars, lineChart, topSeries,50         addrLink, txLink, chainLink, coinIcon, ago } from "/assets/app.js";51mountNav("Tokens");52let TOKEN = (qs.get("symbol") || "USDT").toUpperCase(), WINDOW = "24h";53const ivl = () => ({ "24h": "1h", "7d": "6h" }[WINDOW]);5455async function refresh() {56  document.title = `${TOKEN} — coinexplorer`;57  $("title").innerHTML = `${coinIcon(TOKEN, 26)} ${TOKEN}`;58  const [supply, vol, volSeries, supSeries, flows, tokens] = await Promise.all([59    j(`/v1/stablecoins/supply?token=${TOKEN}`),60    j(`/v1/stablecoins/volume?token=${TOKEN}&window=${WINDOW}`),61    j(`/v1/stablecoins/volume/series?token=${TOKEN}&window=${WINDOW}&interval=${ivl()}`),62    j(`/v1/stablecoins/supply/series?token=${TOKEN}&window=7d`),63    j(`/v1/stablecoins/flows?token=${TOKEN}&window=${WINDOW}&interval=${ivl()}`),64    j("/v1/tokens"),65  ]);66  const s = supply[TOKEN];67  $("tSupply").textContent = s ? fmtUsd(s.total_usd ?? s.total) : "—";68  $("tSupplyN").textContent = s ? `across ${Object.keys(s.chains).length} indexed chains` : "no snapshot yet";69  $("tVol").textContent = fmtUsd(vol.total_volume);70  ["tVolW", "tActW", "tNetW", "whaleW", "mbW"].forEach((id) => $(id).textContent = "· " + WINDOW);71  $("tVolN").textContent = `${fmtN(Object.values(vol.chains).reduce((a, c) => a + c.transfers, 0))} transfers`;72  $("tAct").textContent = fmtN(Object.values(vol.chains).reduce((a, c) => a + c.senders, 0));73  const fl = (flows.tokens[TOKEN] || []);74  const net = fl.reduce((a, b) => a + b.net, 0);75  $("tNet").textContent = (net >= 0 ? "+" : "") + fmtUsd(net);76  $("tNet").className = "big " + (net >= 0 ? "up" : "down");77  $("tNetN").textContent = `minted ${fmtUsd(fl.reduce((a, b) => a + b.minted, 0))} · burned ${fmtUsd(fl.reduce((a, b) => a + b.burned, 0))}`;7879  $("volTitle").textContent = `${TOKEN} volume — ${WINDOW}`;80  lineChart($("volChart"), topSeries(volSeries.chains, 3));81  $("supTitle").textContent = `${TOKEN} supply — 7d (hourly snapshots)`;82  lineChart($("supChart"), topSeries(supSeries.chains, 3, "supply"), { area: false });8384  hbars($("supplyBars"), Object.entries(s ? s.chains : {})85    .map(([k, v]) => ({ k, v: v.usd ?? v.supply, link: chainLink(k) }))86    .sort((a, b) => b.v - a.v).slice(0, 14));8788  const info = tokens[TOKEN] || { chains: {} };89  $("deploys").innerHTML = Object.entries(info.chains).map(([c, d]) =>90    `<tr><td>${chainLink(c)}</td><td class="mono" title="${d.id}">${d.id}</td>91     <td><span class="badge ${d.discontinued ? "discontinued" : d.native ? "native" : "bridged"}">92       ${d.discontinued ? "discontinued" : d.native ? "native" : "bridged"}</span></td>93     <td class="num">${d.decimals ?? "—"}</td></tr>`).join("");94}9596async function refreshLists() {97  const [whales, mb] = await Promise.all([98    j(`/v1/stablecoins/whales?token=${TOKEN}&window=${WINDOW}&limit=25`),99    j(`/v1/stablecoins/mints-burns?token=${TOKEN}&window=${WINDOW}&limit=25`),100  ]);101  $("whales").innerHTML = whales.map((x) =>102    `<tr><td>${chainLink(x.chain)}</td><td class="num"><b>${fmtUsd(x.usd)}</b></td>103     <td>${addrLink(x.from)}</td><td>${addrLink(x.to)}</td>104     <td>${txLink(x.chain, x.tx_hash)}</td><td>${ago(x.timestamp)} ago</td></tr>`).join("") ||105    `<tr><td colspan="6" class="empty">none in window</td></tr>`;106  $("mintburn").innerHTML = mb.map((m) =>107    `<tr><td class="${m.direction}">${m.direction === "mint" ? "▲ mint" : "▼ burn"}</td>108     <td>${chainLink(m.chain)}</td><td class="num">${fmtAmt(m)}</td>109     <td>${txLink(m.chain, m.tx_hash)}</td><td>${ago(m.timestamp)} ago</td></tr>`).join("") ||110    `<tr><td colspan="5" class="empty">none in window</td></tr>`;111}112113j("/v1/tokens").then((t) => {114  $("tokenSel").innerHTML = Object.keys(t).sort()115    .map((s) => `<option ${s === TOKEN ? "selected" : ""}>${s}</option>`).join("");116});117$("tokenSel").addEventListener("change", (e) => {118  TOKEN = e.target.value;119  history.replaceState(null, "", `?symbol=${TOKEN}`);120  refresh(); refreshLists();121});122document.querySelectorAll(".presets button").forEach((b) => b.addEventListener("click", () => {123  document.querySelectorAll(".presets button").forEach((x) => x.setAttribute("aria-pressed", "false"));124  b.setAttribute("aria-pressed", "true"); WINDOW = b.dataset.w; refresh(); refreshLists();125}));126refresh(); refreshLists();127setInterval(refresh, 60_000);128</script>129</body>130</html>131