/* global React */ /** * ExchangeSummary — Live HC Exchange ticker widget (M2 data wiring) * GET /api/v1/public/exchange/tickers (unauthenticated) * GET /api/v1/public/exchange/custody/reserves (unauthenticated) * DATA_CONTRACTS: hc-exchange.json */ const { useState, useEffect } = React; const API_TICKERS = "https://api.conceptualhealth.com/api/v1/public/exchange/tickers"; const API_RESERVES = "https://api.conceptualhealth.com/api/v1/public/exchange/custody/reserves"; const REFRESH_MS = 15 * 1000; const DEMO_TICKERS = []; const DEMO_RESERVES = []; function ExchangeSummary() { const [tickers, setTickers] = useState([]); const [reserves, setReserves] = useState([]); const [live, setLive] = useState(false); const fetch_ = () => { Promise.all([ fetch(API_TICKERS).then(r => r.ok ? r.json() : null).catch(() => null), fetch(API_RESERVES).then(r => r.ok ? r.json() : null).catch(() => null), ]).then(([t, r]) => { if (t && t.length) { setTickers(t); setLive(true); } else { setTickers(DEMO_TICKERS); setLive(false); } if (r && r.length) setReserves(r); else setReserves(DEMO_RESERVES); }); }; useEffect(() => { fetch_(); const id = setInterval(fetch_, REFRESH_MS); return () => clearInterval(id); }, []); if (!tickers.length) return null; const badge = { display: "inline-flex", alignItems: "center", gap: 5, fontSize: 9, fontFamily: "var(--font-mono,monospace)", padding: "3px 9px", borderRadius: 20, background: live ? "rgba(16,185,129,0.12)" : "rgba(156,163,175,0.12)", color: live ? "#10b981" : "#9ca3af", border: live ? "1px solid rgba(16,185,129,0.25)" : "1px solid rgba(156,163,175,0.2)" }; const mono = { fontFamily: "var(--font-mono,monospace)", fontSize: 12 }; const fmt = (n, d=4) => n != null ? Number(n).toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d }) : "—"; const fmtVol = n => n != null ? (n >= 1e6 ? `$${(n/1e6).toFixed(2)}M` : `$${Math.round(n).toLocaleString()}`) : "—"; return (
HC Exchange · market data
{live ? "LIVE" : "DEMO"}
{/* Ticker table */}
{["Pair","Last","Bid","Ask","24h Vol"].map(h => (
{h}
))}
{tickers.slice(0, 6).map((t, i) => (
{t.pair}
{fmt(t.last)}
{fmt(t.bid)}
{fmt(t.ask)}
{fmtVol(t.volume_24h)}
))}
{/* Proof of reserves */} {reserves.length > 0 && (
Proof of reserves
{["Asset","On-chain","Liabilities","Ratio"].map(h => (
{h}
))}
{reserves.map((r, i) => { const ratio = r.on_chain && r.liabilities ? (r.on_chain / r.liabilities) : null; const ratioColor = ratio == null ? "var(--fg-2)" : ratio >= 1.02 ? "#10b981" : ratio >= 1.0 ? "#fbbf24" : "#ef4444"; return (
{r.asset}
{r.on_chain != null ? `${(r.on_chain/1e6).toFixed(2)}M` : "—"}
{r.liabilities != null ? `${(r.liabilities/1e6).toFixed(2)}M` : "—"}
{ratio != null ? `${(ratio*100).toFixed(1)}%` : "—"}
); })}
)}
); } window.ExchangeSummary = ExchangeSummary;