const PRICE_CACHE_TTL_MS = 60_000; const PRICE_REQUEST_TIMEOUT_MS = 10_000; let cachedPrices: Record | null = null; let cachedAt = 0; interface CoinGeckoPriceResponse { [coinId: string]: { usd?: number; }; } export async function fetchUsdPrices(coinIds: string[]): Promise> { const uniqueCoinIds = Array.from(new Set(coinIds.filter(Boolean))); if (!uniqueCoinIds.length) { return {}; } if ( cachedPrices && Date.now() - cachedAt < PRICE_CACHE_TTL_MS && uniqueCoinIds.every((coinId) => coinId in cachedPrices!) ) { return uniqueCoinIds.reduce>((acc, coinId) => { acc[coinId] = cachedPrices![coinId]; return acc; }, {}); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), PRICE_REQUEST_TIMEOUT_MS); try { const url = new URL('https://api.coingecko.com/api/v3/simple/price'); url.searchParams.set('ids', uniqueCoinIds.join(',')); url.searchParams.set('vs_currencies', 'usd'); const response = await fetch(url.toString(), { signal: controller.signal, cache: 'no-store', headers: { Accept: 'application/json', }, }); if (!response.ok) { throw new Error(`CoinGecko returned ${response.status}`); } const payload = (await response.json()) as CoinGeckoPriceResponse; const prices = uniqueCoinIds.reduce>((acc, coinId) => { const price = payload[coinId]?.usd; if (typeof price === 'number') { acc[coinId] = price; } return acc; }, {}); cachedPrices = { ...(cachedPrices ?? {}), ...prices, }; cachedAt = Date.now(); return prices; } finally { clearTimeout(timeoutId); } }