From 765ef9571a9e1f6f628ac93e19e7c3c873200735 Mon Sep 17 00:00:00 2001 From: gerlofvanek <gerlof@particl.io> Date: Fri, 17 Jan 2025 20:15:58 +0100 Subject: [PATCH 1/3] JS: Fix HTTP Error 429 --- basicswap/static/js/offerstable.js | 108 +++++++++------- basicswap/static/js/pricechart.js | 191 +++++++++++++++++------------ 2 files changed, 177 insertions(+), 122 deletions(-) diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js index 2a595d6..e6210b7 100644 --- a/basicswap/static/js/offerstable.js +++ b/basicswap/static/js/offerstable.js @@ -74,7 +74,7 @@ let filterTimeout = null; // CONFIGURATION CONSTANTS // Time Constants -const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes +const CACHE_DURATION = 10 * 60 * 1000; // Application Constants const itemsPerPage = 50; @@ -1106,7 +1106,20 @@ function getEmptyPriceData() { async function fetchLatestPrices() { const PRICES_CACHE_KEY = 'prices_coingecko'; + const minRequestInterval = 15000; + const currentTime = Date.now(); + if (!window.isManualRefresh) { + const lastRequestTime = window.lastPriceRequest || 0; + if (currentTime - lastRequestTime < minRequestInterval) { + console.log('Request too soon, using cache'); + const cachedData = CacheManager.get(PRICES_CACHE_KEY); + if (cachedData) { + return cachedData.value; + } + } + } + window.lastPriceRequest = currentTime; if (!window.isManualRefresh) { const cachedData = CacheManager.get(PRICES_CACHE_KEY); if (cachedData && cachedData.remainingTime > 60000) { @@ -1120,10 +1133,11 @@ async function fetchLatestPrices() { return cachedData.value; } } - const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`; - try { console.log('Initiating fresh price data fetch...'); + const existingCache = CacheManager.get(PRICES_CACHE_KEY, true); + let fallbackData = existingCache ? existingCache.value : null; + const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`; const response = await fetch('/json/readurl', { method: 'POST', headers: { @@ -1131,23 +1145,26 @@ async function fetchLatestPrices() { }, body: JSON.stringify({ url: url, - headers: {} + headers: { + 'User-Agent': 'Mozilla/5.0', + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.5' + } }) }); - if (!response.ok) { throw new Error(`HTTP Error: ${response.status} ${response.statusText}`); } - const data = await response.json(); - if (data.Error) { - console.error('API Error:', data.Error); + if (fallbackData) { + console.log('Using fallback data due to API error'); + return fallbackData; + } throw new Error(data.Error); } - if (data && Object.keys(data).length > 0) { - console.log('Processing fresh price data...'); + console.log('Processing fresh price data'); latestPrices = data; CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION); @@ -1156,14 +1173,27 @@ async function fetchLatestPrices() { tableRateModule.setFallbackValue(coin, prices.usd); } }); - return data; - } else { - console.warn('No price data received'); - return null; } + if (fallbackData) { + console.log('Using fallback data due to empty response'); + return fallbackData; + } + const fallbackPrices = {}; + Object.keys(getEmptyPriceData()).forEach(coin => { + const fallbackValue = tableRateModule.getFallbackValue(coin); + if (fallbackValue !== null) { + fallbackPrices[coin] = { usd: fallbackValue, btc: null }; + } + }); + if (Object.keys(fallbackPrices).length > 0) { + return fallbackPrices; + } + console.warn('No price data received'); + return null; } catch (error) { console.error('Price Fetch Error:', error); + const fallbackPrices = {}; Object.keys(getEmptyPriceData()).forEach(coin => { const fallbackValue = tableRateModule.getFallbackValue(coin); @@ -2523,21 +2553,20 @@ function initializeTableEvents() { }); } + const refreshButton = document.getElementById('refreshOffers'); if (refreshButton) { EventManager.add(refreshButton, 'click', async () => { console.log('Manual refresh initiated'); const refreshIcon = document.getElementById('refreshIcon'); const refreshText = document.getElementById('refreshText'); - refreshButton.disabled = true; refreshIcon.classList.add('animate-spin'); refreshText.textContent = 'Refreshing...'; refreshButton.classList.add('opacity-75', 'cursor-wait'); - try { - const PRICES_CACHE_KEY = 'prices_coingecko'; - localStorage.removeItem(PRICES_CACHE_KEY); + const cachedPrices = CacheManager.get('prices_coingecko'); + let previousPrices = cachedPrices ? cachedPrices.value : null; CacheManager.clear(); window.isManualRefresh = true; const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers'; @@ -2547,43 +2576,32 @@ if (refreshButton) { } const newData = await response.json(); const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); - console.log('Fetched offers:', processedNewData.length); - jsonData = formatInitialData(processedNewData); originalJsonData = [...jsonData]; - - const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`; - console.log('Fetching fresh prices...'); - const priceResponse = await fetch('/json/readurl', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: url, headers: {} }) - }); - - if (priceResponse.ok) { - const priceData = await priceResponse.json(); - if (priceData && Object.keys(priceData).length > 0) { - console.log('Updating with fresh price data'); - latestPrices = priceData; - CacheManager.set(PRICES_CACHE_KEY, priceData, CACHE_DURATION); - Object.entries(priceData).forEach(([coin, prices]) => { - if (prices.usd) { - tableRateModule.setFallbackValue(coin, prices.usd); - } - }); - } + const priceData = await fetchLatestPrices(); + if (!priceData && previousPrices) { + console.log('Using previous price data after failed refresh'); + latestPrices = previousPrices; + await updateOffersTable(); + } else if (priceData) { + latestPrices = priceData; + await updateOffersTable(); + } else { + throw new Error('Unable to fetch price data'); } - - await updateOffersTable(); updateJsonView(); updatePaginationInfo(); lastRefreshTime = Date.now(); updateLastRefreshTime(); - console.log('Manual refresh completed successfully'); } catch (error) { console.error('Error during manual refresh:', error); - ui.displayErrorMessage('Failed to refresh offers. Please try again later.'); + ui.displayErrorMessage('Unable to refresh data. Previous data will be preserved.'); + const cachedData = CacheManager.get('prices_coingecko'); + if (cachedData?.value) { + latestPrices = cachedData.value; + await updateOffersTable(); + } } finally { window.isManualRefresh = false; refreshButton.disabled = false; diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index 0cc9163..c311bab 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -28,7 +28,7 @@ const config = { } }, showVolume: false, - cacheTTL: 5 * 60 * 1000, // 5 minutes + cacheTTL: 10 * 60 * 1000, specialCoins: [''], resolutions: { year: { days: 365, interval: 'month' }, @@ -124,106 +124,143 @@ const api = { error: error.message })); }, - fetchCoinGeckoDataXHR: async () => { - const cacheKey = 'coinGeckoOneLiner'; + const cacheKey = 'coinGeckoOneLiner'; + const minRequestInterval = 15000; + const currentTime = Date.now(); + const lastRequestTime = window.lastGeckoRequest || 0; + if (currentTime - lastRequestTime < minRequestInterval) { + console.log('Request too soon, using cache'); const cachedData = cache.get(cacheKey); - if (cachedData) { - console.log('Using cached CoinGecko data'); return cachedData.value; } - + } + window.lastGeckoRequest = currentTime; + const cachedData = cache.get(cacheKey); + if (cachedData) { + console.log('Using cached CoinGecko data'); + return cachedData.value; + } + try { + const existingCache = localStorage.getItem(cacheKey); + let fallbackData = null; + if (existingCache) { + try { + const parsed = JSON.parse(existingCache); + fallbackData = parsed.value; + } catch (e) { + console.warn('Failed to parse existing cache:', e); + } + } const coinIds = config.coins .filter(coin => coin.usesCoinGecko) .map(coin => coin.name) .join(','); const url = `${config.apiEndpoints.coinGecko}/simple/price?ids=${coinIds}&vs_currencies=usd,btc&include_24hr_vol=true&include_24hr_change=true&api_key=${config.apiKeys.coinGecko}`; - //console.log(`Fetching data for multiple coins from CoinGecko: ${url}`); - try { - const data = await api.makePostRequest(url); - //console.log(`Raw CoinGecko data:`, data); - if (typeof data !== 'object' || data === null) { - throw new AppError(`Invalid data structure received from CoinGecko`); + const response = await api.makePostRequest(url, { + 'User-Agent': 'Mozilla/5.0', + 'Accept': 'application/json', + 'Accept-Language': 'en-US,en;q=0.5' + }); + if (typeof response !== 'object' || response === null) { + if (fallbackData) { + console.log('Using fallback data due to invalid response'); + return fallbackData; } - const transformedData = {}; - Object.entries(data).forEach(([id, values]) => { - const coinConfig = config.coins.find(coin => coin.name === id); - const symbol = coinConfig?.symbol.toLowerCase() || id; - transformedData[symbol] = { - current_price: values.usd, - price_btc: values.btc, - total_volume: values.usd_24h_vol, - price_change_percentage_24h: values.usd_24h_change, - displayName: coinConfig?.displayName || coinConfig?.symbol || id - }; - }); - //console.log(`Transformed CoinGecko data:`, transformedData); - cache.set(cacheKey, transformedData); - return transformedData; - } catch (error) { - //console.error(`Error fetching CoinGecko data:`, error); - return { error: error.message }; + throw new AppError(`Invalid data structure received from CoinGecko`); } - }, + if (response.error || response.Error) { + if (fallbackData) { + console.log('Using fallback data due to API error'); + return fallbackData; + } + throw new AppError(response.error || response.Error); + } + const transformedData = {}; + Object.entries(response).forEach(([id, values]) => { + const coinConfig = config.coins.find(coin => coin.name === id); + const symbol = coinConfig?.symbol.toLowerCase() || id; + transformedData[symbol] = { + current_price: values.usd, + price_btc: values.btc, + total_volume: values.usd_24h_vol, + price_change_percentage_24h: values.usd_24h_change, + displayName: coinConfig?.displayName || coinConfig?.symbol || id + }; + }); + cache.set(cacheKey, transformedData); + return transformedData; - fetchHistoricalDataXHR: async (coinSymbols) => { - if (!Array.isArray(coinSymbols)) { - coinSymbols = [coinSymbols]; + } catch (error) { + console.error(`Error fetching CoinGecko data:`, error); + if (cachedData) { + console.log('Using expired cache data due to error'); + return cachedData.value; + } + return { error: error.message }; } +}, - //console.log(`Fetching historical data for coins: ${coinSymbols.join(', ')}`); - +fetchHistoricalDataXHR: async (coinSymbols) => { + if (!Array.isArray(coinSymbols)) { + coinSymbols = [coinSymbols]; + } const results = {}; - const fetchPromises = coinSymbols.map(async coin => { - const coinConfig = config.coins.find(c => c.symbol === coin); - if (!coinConfig) { - //console.error(`Coin configuration not found for ${coin}`); - return; - } + const coinConfig = config.coins.find(c => c.symbol === coin); + if (!coinConfig) return; - if (coin === 'WOW') { - const url = `${config.apiEndpoints.coinGecko}/coins/wownero/market_chart?vs_currency=usd&days=1&api_key=${config.apiKeys.coinGecko}`; - //console.log(`CoinGecko URL for WOW: ${url}`); - - try { - const response = await api.makePostRequest(url); - if (response && response.prices) { - results[coin] = response.prices; - } else { - //console.error(`Unexpected data structure for WOW:`, response); - } - } catch (error) { - console.error(`Error fetching CoinGecko data for WOW:`, error); + const cacheKey = `historical_${coin}_${config.currentResolution}`; + const cachedData = cache.get(cacheKey); + if (cachedData) { + results[coin] = cachedData.value; + return; } - } else { - const resolution = config.resolutions[config.currentResolution]; - let url; - if (resolution.interval === 'hourly') { - url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=${resolution.days * 24}&api_key=${config.apiKeys.cryptoCompare}`; + if (coin === 'WOW') { + const url = `${config.apiEndpoints.coinGecko}/coins/wownero/market_chart?vs_currency=usd&days=1&api_key=${config.apiKeys.coinGecko}`; + try { + const response = await api.makePostRequest(url); + if (response && response.prices) { + results[coin] = response.prices; + cache.set(cacheKey, response.prices); + } + } catch (error) { + console.error(`Error fetching CoinGecko data for WOW:`, error); + if (cachedData) { + results[coin] = cachedData.value; + } + } } else { - url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coin}&tsym=USD&limit=${resolution.days}&api_key=${config.apiKeys.cryptoCompare}`; + const resolution = config.resolutions[config.currentResolution]; + let url; + if (resolution.interval === 'hourly') { + url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=${resolution.days * 24}&api_key=${config.apiKeys.cryptoCompare}`; + } else { + url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coin}&tsym=USD&limit=${resolution.days}&api_key=${config.apiKeys.cryptoCompare}`; + } + try { + const response = await api.makePostRequest(url); + if (response.Response === "Error") { + console.error(`API Error for ${coin}:`, response.Message); + if (cachedData) { + results[coin] = cachedData.value; + } + } else if (response.Data && response.Data.Data) { + results[coin] = response.Data; + cache.set(cacheKey, response.Data); + } + } catch (error) { + console.error(`Error fetching CryptoCompare data for ${coin}:`, error); + if (cachedData) { + results[coin] = cachedData.value; + } + } } - //console.log(`CryptoCompare URL for ${coin}: ${url}`); - try { - const response = await api.makePostRequest(url); - if (response.Response === "Error") { - //console.error(`API Error for ${coin}:`, response.Message); - } else if (response.Data && response.Data.Data) { - results[coin] = response.Data; - } else { - //console.error(`Unexpected data structure for ${coin}:`, response); - } - } catch (error) { - console.error(`Error fetching CryptoCompare data for ${coin}:`, error); - } - } }); await Promise.all(fetchPromises); - //console.log('Final results object:', JSON.stringify(results, null, 2)); return results; - } +} }; // CACHE From 8de365f9d34646633cb42397fc5b484d9983cc34 Mon Sep 17 00:00:00 2001 From: Gerlof van Ek <gerlof@particl.io> Date: Fri, 17 Jan 2025 22:13:37 +0100 Subject: [PATCH 2/3] Update offerstable.js --- basicswap/static/js/offerstable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js index e6210b7..74c10af 100644 --- a/basicswap/static/js/offerstable.js +++ b/basicswap/static/js/offerstable.js @@ -1106,7 +1106,7 @@ function getEmptyPriceData() { async function fetchLatestPrices() { const PRICES_CACHE_KEY = 'prices_coingecko'; - const minRequestInterval = 15000; + const minRequestInterval = 30000; const currentTime = Date.now(); if (!window.isManualRefresh) { From aee66712b8541b6d253312bd91f51b470ba0bbd9 Mon Sep 17 00:00:00 2001 From: Gerlof van Ek <gerlof@particl.io> Date: Fri, 17 Jan 2025 22:14:19 +0100 Subject: [PATCH 3/3] Update pricechart.js --- basicswap/static/js/pricechart.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index c311bab..d37e533 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -126,7 +126,7 @@ const api = { }, fetchCoinGeckoDataXHR: async () => { const cacheKey = 'coinGeckoOneLiner'; - const minRequestInterval = 15000; + const minRequestInterval = 30000; const currentTime = Date.now(); const lastRequestTime = window.lastGeckoRequest || 0; if (currentTime - lastRequestTime < minRequestInterval) {