rspace-online/modules/rexchange/exchange-rates.ts

123 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
}