Files
timmy-tower/artifacts/api-server/src/lib/btc-oracle.ts

65 lines
1.8 KiB
TypeScript

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<number> {
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));
}