65 lines
1.8 KiB
TypeScript
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));
|
|
}
|