123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
/**
|
||
* 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<string, number>; // fiat currency → USD/fiat rate
|
||
ts: number;
|
||
}
|
||
|
||
const TTL = 5 * 60 * 1000;
|
||
let cached: RateEntry | null = null;
|
||
let inFlight: Promise<RateEntry> | 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<FiatCurrency, string> = {
|
||
EUR: 'eur', USD: 'usd', GBP: 'gbp', BRL: 'brl',
|
||
MXN: 'mxn', INR: 'inr', NGN: 'ngn', ARS: 'ars',
|
||
};
|
||
|
||
async function fetchRates(): Promise<RateEntry> {
|
||
if (cached && Date.now() - cached.ts < TTL) return cached;
|
||
if (inFlight) return inFlight;
|
||
|
||
inFlight = (async (): Promise<RateEntry> => {
|
||
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<number> {
|
||
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<Record<string, number>> {
|
||
const result: Record<string, number> = {};
|
||
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;
|
||
}
|