71 lines
1.8 KiB
TypeScript
71 lines
1.8 KiB
TypeScript
const PRICE_CACHE_TTL_MS = 60_000;
|
|
const PRICE_REQUEST_TIMEOUT_MS = 10_000;
|
|
|
|
let cachedPrices: Record<string, number> | null = null;
|
|
let cachedAt = 0;
|
|
|
|
interface CoinGeckoPriceResponse {
|
|
[coinId: string]: {
|
|
usd?: number;
|
|
};
|
|
}
|
|
|
|
export async function fetchUsdPrices(coinIds: string[]): Promise<Record<string, number>> {
|
|
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<Record<string, number>>((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<Record<string, number>>((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);
|
|
}
|
|
}
|