/** * Currency Service - XMR/Fiat Conversion * * Uses CoinGecko API for real-time exchange rates (CORS-friendly) * Supports two modes: fiat-fix and xmr-fix */ const COINGECKO_API = 'https://api.coingecko.com/api/v3/simple/price' const CURRENCY_SYMBOLS = { XMR: 'ɱ', EUR: '€', USD: '$', GBP: '£', CHF: 'CHF', JPY: '¥', RUB: '₽', BRL: 'R$' } const CACHE_DURATION = 60 * 60 * 1000 // 60 minutes (CoinGecko free tier is strict) const MIN_REQUEST_INTERVAL = 120 * 1000 // 2 minutes between requests const STORAGE_KEY = 'xmr_rates_cache' let cachedRates = null let cacheTimestamp = 0 let lastRequestTime = 0 let pendingRequest = null // Load from localStorage on init function loadFromStorage() { try { const stored = localStorage.getItem(STORAGE_KEY) if (stored) { const { rates, timestamp } = JSON.parse(stored) if (rates && timestamp && Date.now() - timestamp < CACHE_DURATION) { cachedRates = rates cacheTimestamp = timestamp } } } catch (e) { // Ignore storage errors } } function saveToStorage() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ rates: cachedRates, timestamp: cacheTimestamp })) } catch (e) { // Ignore storage errors } } loadFromStorage() /** * Fetches current XMR rates from CoinGecko with rate limiting * @returns {Promise} Rates per currency (e.g. { EUR: 329.05, USD: 388.87 }) */ export async function getXmrRates() { // Return cached rates if still valid if (cachedRates && Date.now() - cacheTimestamp < CACHE_DURATION) { return cachedRates } // If a request is already pending, wait for it if (pendingRequest) { return pendingRequest } // Rate limiting: ensure minimum interval between requests const timeSinceLastRequest = Date.now() - lastRequestTime if (timeSinceLastRequest < MIN_REQUEST_INTERVAL && cachedRates) { return cachedRates } // Create and store the pending request pendingRequest = fetchRates() try { const rates = await pendingRequest return rates } finally { pendingRequest = null } } async function fetchRates() { lastRequestTime = Date.now() try { const currencies = 'eur,usd,gbp,chf,jpy,rub,brl' const response = await fetch(`${COINGECKO_API}?ids=monero&vs_currencies=${currencies}`) // Handle rate limit response if (response.status === 429) { console.warn('CoinGecko rate limit hit, using cached rates') return cachedRates || getDefaultRates() } const data = await response.json() if (!data.monero) { console.error('CoinGecko API Error: No data returned') return cachedRates || getDefaultRates() } const rates = { EUR: data.monero.eur, USD: data.monero.usd, GBP: data.monero.gbp, CHF: data.monero.chf, JPY: data.monero.jpy, RUB: data.monero.rub, BRL: data.monero.brl } // Update cache cachedRates = rates cacheTimestamp = Date.now() saveToStorage() return rates } catch (error) { console.error('Failed to fetch XMR rates:', error) return cachedRates || getDefaultRates() } } /** * Fallback rates if API is unreachable */ function getDefaultRates() { return { EUR: 150, USD: 165, GBP: 130, CHF: 145, JPY: 24000, RUB: 15000, BRL: 850 } } /** * Converts amount to XMR * @param {number} amount - Amount in source currency * @param {string} currency - Source currency (EUR, USD, etc.) * @param {Object} rates - Rates from getXmrRates() * @returns {number} Amount in XMR */ export function convertToXmr(amount, currency, rates) { if (currency === 'XMR') return amount if (!rates[currency]) return amount return amount / rates[currency] } /** * Converts XMR to fiat * @param {number} xmrAmount - Amount in XMR * @param {string} currency - Target currency (EUR, USD, etc.) * @param {Object} rates - Rates from getXmrRates() * @returns {number} Amount in target currency */ export function convertFromXmr(xmrAmount, currency, rates) { if (currency === 'XMR') return xmrAmount if (!rates[currency]) return xmrAmount return xmrAmount * rates[currency] } /** * Converts between fiat currencies using XMR as bridge * @param {number} amount - Amount in source currency * @param {string} fromCurrency - Source currency * @param {string} toCurrency - Target currency * @param {Object} rates - Rates from getXmrRates() (XMR -> Fiat) * @returns {number} Amount in target currency */ export function convertFiat(amount, fromCurrency, toCurrency, rates) { if (fromCurrency === toCurrency) return amount if (!rates[fromCurrency] || !rates[toCurrency]) return amount // Use XMR as bridge: EUR -> XMR -> USD // 1 XMR = rates[EUR] EUR, so 1 EUR = 1/rates[EUR] XMR // 1 XMR = rates[USD] USD // Therefore: 1 EUR = rates[USD] / rates[EUR] USD return amount * (rates[toCurrency] / rates[fromCurrency]) } /** * Get user's preferred display currency from settings * @returns {string} Currency code (default: 'USD') */ export function getDisplayCurrency() { return localStorage.getItem('kashilo_currency') || 'USD' } /** * Formats a price for display * Shows price in user's preferred currency with XMR equivalent * @param {Object} listing - Listing with price, currency, price_mode * @param {Object} rates - Rates from getXmrRates() * @param {string} [displayCurrency] - Override display currency * @returns {Object} { primary, secondary, xmrAmount } */ export function formatPrice(listing, rates, displayCurrency = null) { const { price, currency, price_mode } = listing const userCurrency = displayCurrency || getDisplayCurrency() if (!price || price === 0) { return { primary: 'Free', secondary: null, xmrAmount: 0 } } // XMR mode: XMR is the reference price if (price_mode === 'xmr' || currency === 'XMR') { const xmrPrice = currency === 'XMR' ? price : convertToXmr(price, currency, rates) const fiatEquivalent = convertFromXmr(xmrPrice, userCurrency, rates) return { primary: formatFiat(fiatEquivalent, userCurrency), secondary: `≈ ${formatXmr(xmrPrice)}`, xmrAmount: xmrPrice } } // Fiat mode: Convert to user's preferred currency const xmrEquivalent = convertToXmr(price, currency, rates) // Always show in user's currency as primary const displayPrice = userCurrency === currency ? price : convertFiat(price, currency, userCurrency, rates) return { primary: formatFiat(displayPrice, userCurrency), secondary: `≈ ${formatXmr(xmrEquivalent)}`, xmrAmount: xmrEquivalent } } /** * Formats XMR amount * @param {number} amount - Amount in XMR * @returns {string} Formatted string (e.g. "0.5234 XMR") */ export function formatXmr(amount) { if (amount >= 1) { return `${amount.toFixed(4)} XMR` } return `${amount.toFixed(6)} XMR` } /** * Formats fiat amount * @param {number} amount - Amount * @param {string} currency - Currency * @returns {string} Formatted string (e.g. "€ 150,00") */ export function formatFiat(amount, currency) { const symbol = CURRENCY_SYMBOLS[currency] || currency let locale = 'en-US' try { const stored = localStorage.getItem('locale') const localeMap = { de: 'de-DE', en: 'en-US', fr: 'fr-FR', it: 'it-IT', es: 'es-ES', pt: 'pt-BR', ru: 'ru-RU' } locale = localeMap[stored] || 'en-US' } catch (e) { // Fallback } const formatted = new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount) if (['EUR', 'GBP', 'USD', 'BRL'].includes(currency)) { return `${symbol} ${formatted}` } return `${formatted} ${symbol}` } /** * Returns the currency symbol * @param {string} currency - Currency code * @returns {string} Symbol */ export function getCurrencySymbol(currency) { return CURRENCY_SYMBOLS[currency] || currency } /** * List of supported currencies */ export const SUPPORTED_CURRENCIES = ['XMR', 'EUR', 'CHF', 'USD', 'GBP', 'JPY', 'RUB', 'BRL'] export default { getXmrRates, getDisplayCurrency, convertToXmr, convertFromXmr, convertFiat, formatPrice, formatXmr, formatFiat, getCurrencySymbol, SUPPORTED_CURRENCIES }