Files
kashilo/js/services/currency.js

307 lines
8.4 KiB
JavaScript

/**
* 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<Object>} 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('dgray_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
const formatted = new Intl.NumberFormat('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount)
// Symbol before or after 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
}