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) {