import { makeLogger } from "./logger.js"; const logger = makeLogger("btc-oracle"); const COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"; interface CachedPrice { price: number; at: number; } let cache: CachedPrice | null = null; const CACHE_MS = 60_000; function fallbackPrice(): number { const raw = parseFloat(process.env.BTC_PRICE_USD_FALLBACK ?? ""); return Number.isFinite(raw) && raw > 0 ? raw : 100_000; } /** * Returns the current BTC/USD price. * Caches for 60 s; falls back to BTC_PRICE_USD_FALLBACK env var (default $100,000) * on network failure so the service keeps running without internet access. */ export async function getBtcPriceUsd(): Promise { if (cache && Date.now() - cache.at < CACHE_MS) { return cache.price; } try { const res = await fetch(COINGECKO_URL, { headers: { Accept: "application/json" }, signal: AbortSignal.timeout(5_000), }); if (!res.ok) throw new Error(`CoinGecko HTTP ${res.status}`); const data = (await res.json()) as { bitcoin?: { usd?: number } }; const price = data?.bitcoin?.usd; if (!price || !Number.isFinite(price) || price <= 0) { throw new Error(`Unexpected CoinGecko response: ${JSON.stringify(data)}`); } cache = { price, at: Date.now() }; return price; } catch (err) { const fb = fallbackPrice(); logger.warn("price fetch failed — using fallback", { fallback_usd: fb, error: err instanceof Error ? err.message : String(err), }); return fb; } } /** * Convert a USD amount to satoshis at the given BTC/USD price. * Always rounds up (ceil) so costs are fully covered; minimum 1 sat. */ export function usdToSats(usd: number, btcPriceUsd: number): number { if (usd <= 0) return 1; return Math.max(1, Math.ceil((usd / btcPriceUsd) * 1e8)); }