feat(sirius-x): prices in BCH, amounts in bits, live BCH/USD rate from several exchanges

Every dollar figure on the site came from a hard-coded 250,000 sats per
dollar, which implies $400 per BCH; the market is near $250, so name
prices, TLD fees and sale listings were shown about 60% too cheap in
dollars. The gateway now serves /api/price: the median of Coinbase,
Kraken, Bitstamp, Binance and CoinGecko public tickers, no keys, cached
60 s, last good answer kept if every source fails. The site reads it
first, queries the same tickers itself if the gateway is unreachable,
remembers the last rate per browser, and only falls back to a constant
for the very first paint. Pages repaint when the rate arrives, and the
rate line says where it came from and how old it is.

Units: satoshi no longer appear anywhere. Sale prices, the seller's
input and the buy dialog are in BCH with the dollar figure beside
them; balances, fees and dust are in bits (1 bit = 100 satoshi).
This commit is contained in:
Local Dev 2026-09-20 18:18:43 +02:00
parent 0396988124
commit 0c6753d6a3

View file

@ -161,6 +161,35 @@ async function findTokenUtxo(lockHex, category, commitmentHex) {
}
// Listings whose certificate is still where the seller left it. Cached 30 s.
let _marketCheck = { at: 0, list: null };
// ---- BCH/USD price: median of public exchange tickers ----
const PRICE_SOURCES = [
{ name: "coinbase", url: "https://api.coinbase.com/v2/prices/BCH-USD/spot", pick: (j) => Number(j?.data?.amount) },
{ name: "kraken", url: "https://api.kraken.com/0/public/Ticker?pair=BCHUSD", pick: (j) => Number(Object.values(j?.result || {})[0]?.c?.[0]) },
{ name: "bitstamp", url: "https://www.bitstamp.net/api/v2/ticker/bchusd/", pick: (j) => Number(j?.last) },
{ name: "binance", url: "https://api.binance.com/api/v3/ticker/price?symbol=BCHUSDT", pick: (j) => Number(j?.price) },
{ name: "coingecko", url: "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin-cash&vs_currencies=usd", pick: (j) => Number(j?.["bitcoin-cash"]?.usd) },
];
let priceCache = { at: 0, body: null };
async function getPrice() {
if (priceCache.body && Date.now() - priceCache.at < 60_000) return priceCache.body;
const results = await Promise.allSettled(PRICE_SOURCES.map(async (src) => {
const r = await fetch(src.url, { signal: AbortSignal.timeout(5000), headers: { "user-agent": "bns-gateway/1", accept: "application/json" } });
if (!r.ok) throw new Error(String(r.status));
const usd = src.pick(await r.json());
if (!Number.isFinite(usd) || usd <= 0) throw new Error("no price");
return { name: src.name, usd: Math.round(usd * 100) / 100 };
}));
const ok = results.filter((r) => r.status === "fulfilled").map((r) => r.value).sort((a, b) => a.usd - b.usd);
if (ok.length) {
const mid = Math.floor(ok.length / 2);
const usd = ok.length % 2 ? ok[mid].usd : Math.round((ok[mid - 1].usd + ok[mid].usd) * 50) / 100;
priceCache = { at: Date.now(), body: { usd, sats_per_usd: Math.round(1e8 / usd), sources: ok, median_of: ok.length, at: new Date().toISOString(), stale: false } };
return priceCache.body;
}
if (priceCache.body) return { ...priceCache.body, stale: true };
return { usd: null, sats_per_usd: null, sources: [], median_of: 0, at: new Date().toISOString(), stale: true, error: results.map((r) => r.reason?.message).join("; ") };
}
async function activeListings() {
if (_marketCheck.list && Date.now() - _marketCheck.at < 30000) return _marketCheck.list;
const stored = readMarket();
@ -1138,6 +1167,22 @@ const server = http.createServer(async (req, res) => {
}
// --- TLD certificate UTXO (for co-signed registrations) ---
// GET /api/price -> { usd, sats_per_usd, sources: [{name, usd}], median_of, at, stale }
// BCH/USD from several exchanges' public endpoints (no keys), median of
// whoever answers, cached 60 s; the last good answer is served (marked
// stale) if every source fails. Clients also query the same sources
// themselves when this route is unreachable.
if (req.url === "/api/price" || req.url.startsWith("/api/price?")) {
res.setHeader("access-control-allow-origin", "*");
res.setHeader("content-type", "application/json");
res.setHeader("cache-control", "public, max-age=30");
try {
const q = await getPrice();
res.writeHead(q.usd ? 200 : 503); res.end(JSON.stringify(q, null, 2));
} catch (e) { res.writeHead(500); res.end(JSON.stringify({ error: e.message })); }
return;
}
// GET /api/tld-cert/<tld> -> { tld, owner, utxo: {txid, vout, satoshis, lockingBytecode, token} }
if (req.url.startsWith("/api/tld-cert/")) {
res.setHeader("access-control-allow-origin", "*");