49 lines
1.9 KiB
JavaScript
49 lines
1.9 KiB
JavaScript
|
|
/**
|
||
|
|
* BCH ↔ USD price lookup, with a 60-second in-memory cache.
|
||
|
|
*
|
||
|
|
* We pull from CoinGecko by default — public, no API key, generous for the
|
||
|
|
* once-a-minute cadence we need. The price feeds the invoice quote; a
|
||
|
|
* publisher's quote is locked at issue time and does not re-quote while they
|
||
|
|
* decide whether to pay.
|
||
|
|
*
|
||
|
|
* On fetch failure we return the last good value if it is under 15 minutes
|
||
|
|
* old. Beyond that, we throw — handing out invoices with stale prices is
|
||
|
|
* worse than refusing to quote until the oracle recovers.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const CACHE_TTL_MS = 60_000;
|
||
|
|
const STALE_FALLBACK_MS = 15 * 60_000;
|
||
|
|
const SOURCE = process.env.PROMETHEUS_PRICE_SOURCE
|
||
|
|
|| 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin-cash&vs_currencies=usd';
|
||
|
|
|
||
|
|
let cached = null; // { price_usd, at }
|
||
|
|
|
||
|
|
export async function bchUsd() {
|
||
|
|
const now = Date.now();
|
||
|
|
if (cached && now - cached.at < CACHE_TTL_MS) return cached.price_usd;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const resp = await fetch(SOURCE, {
|
||
|
|
headers: { accept: 'application/json' },
|
||
|
|
signal: AbortSignal.timeout(5000),
|
||
|
|
});
|
||
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||
|
|
const body = await resp.json();
|
||
|
|
const p = Number(body?.['bitcoin-cash']?.usd);
|
||
|
|
if (!Number.isFinite(p) || p <= 0) throw new Error('bad price in response');
|
||
|
|
cached = { price_usd: p, at: now };
|
||
|
|
return p;
|
||
|
|
} catch (err) {
|
||
|
|
if (cached && now - cached.at < STALE_FALLBACK_MS) return cached.price_usd;
|
||
|
|
throw new Error(`price oracle: ${err?.message || err}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** USD → sats, using the current (or last-good) BCH/USD price. */
|
||
|
|
export async function usdToSats(usd) {
|
||
|
|
const price = await bchUsd(); // USD per BCH
|
||
|
|
const bch = Number(usd) / price; // BCH per `usd` dollars
|
||
|
|
const sats = Math.round(bch * 1e8); // satoshis per BCH = 1e8
|
||
|
|
return { sats, rate_sat_per_usd: Math.round(1e8 / price), price_usd: price };
|
||
|
|
}
|