/** * Exchange rate feed — 5-min cached CoinGecko USD/fiat pairs. * * cUSDC is pegged to USDC (≈ $1 USD), so cUSDC/fiat ≈ USD/fiat. * $MYCO uses bonding curve price × USD/fiat rate. */ import type { TokenId, FiatCurrency } from './schemas'; // ── Cache ── interface RateEntry { rates: Record; // fiat currency → USD/fiat rate ts: number; } const TTL = 5 * 60 * 1000; let cached: RateEntry | null = null; let inFlight: Promise | null = null; const CG_API_KEY = process.env.COINGECKO_API_KEY || ''; function cgUrl(url: string): string { if (!CG_API_KEY) return url; const sep = url.includes('?') ? '&' : '?'; return `${url}${sep}x_cg_demo_api_key=${CG_API_KEY}`; } const FIAT_IDS: Record = { EUR: 'eur', USD: 'usd', GBP: 'gbp', BRL: 'brl', MXN: 'mxn', INR: 'inr', NGN: 'ngn', ARS: 'ars', }; async function fetchRates(): Promise { if (cached && Date.now() - cached.ts < TTL) return cached; if (inFlight) return inFlight; inFlight = (async (): Promise => { try { const currencies = Object.values(FIAT_IDS).join(','); const res = await fetch( cgUrl(`https://api.coingecko.com/api/v3/simple/price?ids=usd-coin&vs_currencies=${currencies}`), { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(10000) }, ); if (res.status === 429) { console.warn('[exchange-rates] CoinGecko rate limited, waiting 60s...'); await new Promise(r => setTimeout(r, 60000)); const retry = await fetch( cgUrl(`https://api.coingecko.com/api/v3/simple/price?ids=usd-coin&vs_currencies=${currencies}`), { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(10000) }, ); if (!retry.ok) return cached || { rates: {}, ts: Date.now() }; const data = await retry.json() as any; const entry: RateEntry = { rates: data['usd-coin'] || {}, ts: Date.now() }; cached = entry; return entry; } if (!res.ok) return cached || { rates: {}, ts: Date.now() }; const data = await res.json() as any; const entry: RateEntry = { rates: data['usd-coin'] || {}, ts: Date.now() }; cached = entry; return entry; } catch (e) { console.warn('[exchange-rates] Failed to fetch rates:', e); return cached || { rates: {}, ts: Date.now() }; } finally { inFlight = null; } })(); return inFlight; } /** * Get the exchange rate for a token in a fiat currency. * Returns fiat amount per 1 whole token (not base units). */ export async function getExchangeRate(tokenId: TokenId, fiat: FiatCurrency): Promise { const fiatKey = FIAT_IDS[fiat]; if (!fiatKey) return 0; const entry = await fetchRates(); const usdFiatRate = entry.rates[fiatKey] || 0; if (tokenId === 'cusdc' || tokenId === 'fusdc') { // cUSDC/fUSDC ≈ 1 USD, so rate = USD/fiat return usdFiatRate; } if (tokenId === 'myco') { // $MYCO price from bonding curve × USD/fiat try { const { calculatePrice } = await import('../../server/bonding-curve'); const { getTokenDoc } = await import('../../server/token-service'); const doc = getTokenDoc('myco'); const supply = doc?.token?.totalSupply || 0; const priceInCusdcBase = calculatePrice(supply); // priceInCusdcBase is cUSDC base units per 1 MYCO base unit // Convert to USD: base / 1_000_000 const priceInUsd = priceInCusdcBase / 1_000_000; return priceInUsd * usdFiatRate; } catch { return 0; } } return 0; } /** * Get all supported fiat rates for a token. Returns { currency → rate }. */ export async function getAllRates(tokenId: TokenId): Promise> { const result: Record = {}; const fiats: FiatCurrency[] = ['EUR', 'USD', 'GBP', 'BRL', 'MXN', 'INR', 'NGN', 'ARS']; // Fetch once (cached), then compute per-fiat await fetchRates(); for (const fiat of fiats) { result[fiat] = await getExchangeRate(tokenId, fiat); } return result; }