mirror of
https://github.com/basicswap/basicswap.git
synced 2025-01-22 10:34:34 +00:00
JS: Enhanced 429 fix + better error handle. Updated refresh button. (#229)
* JS: Enhanced 429 fix + better error handle. Updated refresh button. * Update offerstable.js
This commit is contained in:
parent
5d1bed6423
commit
67d808cbe4
2 changed files with 490 additions and 309 deletions
|
@ -77,7 +77,7 @@ let filterTimeout = null;
|
||||||
const CACHE_DURATION = 10 * 60 * 1000;
|
const CACHE_DURATION = 10 * 60 * 1000;
|
||||||
|
|
||||||
// Application Constants
|
// Application Constants
|
||||||
const itemsPerPage = 50;
|
const itemsPerPage = 100;
|
||||||
const isSentOffers = window.offersTableConfig.isSentOffers;
|
const isSentOffers = window.offersTableConfig.isSentOffers;
|
||||||
|
|
||||||
const offersConfig = {
|
const offersConfig = {
|
||||||
|
@ -1106,7 +1106,7 @@ function getEmptyPriceData() {
|
||||||
|
|
||||||
async function fetchLatestPrices() {
|
async function fetchLatestPrices() {
|
||||||
const PRICES_CACHE_KEY = 'prices_coingecko';
|
const PRICES_CACHE_KEY = 'prices_coingecko';
|
||||||
const minRequestInterval = 30000;
|
const minRequestInterval = 60000;
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
|
|
||||||
if (!window.isManualRefresh) {
|
if (!window.isManualRefresh) {
|
||||||
|
@ -2553,17 +2553,51 @@ function initializeTableEvents() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const refreshButton = document.getElementById('refreshOffers');
|
const refreshButton = document.getElementById('refreshOffers');
|
||||||
if (refreshButton) {
|
if (refreshButton) {
|
||||||
|
let lastRefreshTime = 0;
|
||||||
|
const REFRESH_COOLDOWN = 6000;
|
||||||
|
let countdownInterval;
|
||||||
|
|
||||||
EventManager.add(refreshButton, 'click', async () => {
|
EventManager.add(refreshButton, 'click', async () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastRefreshTime < REFRESH_COOLDOWN) {
|
||||||
|
console.log('Refresh rate limited. Please wait before refreshing again.');
|
||||||
|
const startTime = now;
|
||||||
|
const refreshText = document.getElementById('refreshText');
|
||||||
|
|
||||||
|
refreshButton.classList.remove('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
||||||
|
refreshButton.classList.add('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||||||
|
|
||||||
|
if (countdownInterval) clearInterval(countdownInterval);
|
||||||
|
|
||||||
|
countdownInterval = setInterval(() => {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const elapsedTime = currentTime - startTime;
|
||||||
|
const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
|
||||||
|
|
||||||
|
if (remainingTime <= 0) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
refreshText.textContent = 'Refresh';
|
||||||
|
|
||||||
|
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||||||
|
refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
||||||
|
} else {
|
||||||
|
refreshText.textContent = `Refresh (${remainingTime}s)`;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('Manual refresh initiated');
|
console.log('Manual refresh initiated');
|
||||||
|
lastRefreshTime = now;
|
||||||
const refreshIcon = document.getElementById('refreshIcon');
|
const refreshIcon = document.getElementById('refreshIcon');
|
||||||
const refreshText = document.getElementById('refreshText');
|
const refreshText = document.getElementById('refreshText');
|
||||||
refreshButton.disabled = true;
|
refreshButton.disabled = true;
|
||||||
refreshIcon.classList.add('animate-spin');
|
refreshIcon.classList.add('animate-spin');
|
||||||
refreshText.textContent = 'Refreshing...';
|
refreshText.textContent = 'Refreshing...';
|
||||||
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
refreshButton.classList.add('opacity-75', 'cursor-wait');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cachedPrices = CacheManager.get('prices_coingecko');
|
const cachedPrices = CacheManager.get('prices_coingecko');
|
||||||
let previousPrices = cachedPrices ? cachedPrices.value : null;
|
let previousPrices = cachedPrices ? cachedPrices.value : null;
|
||||||
|
@ -2591,12 +2625,15 @@ if (refreshButton) {
|
||||||
}
|
}
|
||||||
updateJsonView();
|
updateJsonView();
|
||||||
updatePaginationInfo();
|
updatePaginationInfo();
|
||||||
lastRefreshTime = Date.now();
|
lastRefreshTime = now;
|
||||||
updateLastRefreshTime();
|
updateLastRefreshTime();
|
||||||
|
|
||||||
console.log('Manual refresh completed successfully');
|
console.log('Manual refresh completed successfully');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during manual refresh:', error);
|
console.error('Error during manual refresh:', error);
|
||||||
ui.displayErrorMessage('Unable to refresh data. Previous data will be preserved.');
|
ui.displayErrorMessage('Unable to refresh data. Previous data will be preserved.');
|
||||||
|
|
||||||
const cachedData = CacheManager.get('prices_coingecko');
|
const cachedData = CacheManager.get('prices_coingecko');
|
||||||
if (cachedData?.value) {
|
if (cachedData?.value) {
|
||||||
latestPrices = cachedData.value;
|
latestPrices = cachedData.value;
|
||||||
|
@ -2608,6 +2645,13 @@ if (refreshButton) {
|
||||||
refreshIcon.classList.remove('animate-spin');
|
refreshIcon.classList.remove('animate-spin');
|
||||||
refreshText.textContent = 'Refresh';
|
refreshText.textContent = 'Refresh';
|
||||||
refreshButton.classList.remove('opacity-75', 'cursor-wait');
|
refreshButton.classList.remove('opacity-75', 'cursor-wait');
|
||||||
|
|
||||||
|
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
|
||||||
|
refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
|
||||||
|
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,191 +76,256 @@ const logger = {
|
||||||
error: (message) => console.error(`[AppError] ${new Date().toISOString()}: ${message}`)
|
error: (message) => console.error(`[AppError] ${new Date().toISOString()}: ${message}`)
|
||||||
};
|
};
|
||||||
|
|
||||||
// API
|
|
||||||
const api = {
|
const api = {
|
||||||
makePostRequest: (url, headers = {}) => {
|
makePostRequest: (url, headers = {}) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', '/json/readurl');
|
xhr.open('POST', '/json/readurl');
|
||||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
xhr.timeout = 30000;
|
xhr.timeout = 30000;
|
||||||
xhr.ontimeout = () => reject(new AppError('Request timed out'));
|
xhr.ontimeout = () => reject(new AppError('Request timed out'));
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
logger.log(`Response for ${url}:`, xhr.responseText);
|
logger.log(`Response for ${url}:`, xhr.responseText);
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
try {
|
try {
|
||||||
const response = JSON.parse(xhr.responseText);
|
const response = JSON.parse(xhr.responseText);
|
||||||
if (response.Error) {
|
if (response.Error) {
|
||||||
logger.error(`API Error for ${url}:`, response.Error);
|
logger.error(`API Error for ${url}:`, response.Error);
|
||||||
reject(new AppError(response.Error, 'APIError'));
|
reject(new AppError(response.Error, 'APIError'));
|
||||||
} else {
|
} else {
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Invalid JSON response for ${url}:`, xhr.responseText);
|
logger.error(`Invalid JSON response for ${url}:`, xhr.responseText);
|
||||||
reject(new AppError(`Invalid JSON response: ${error.message}`, 'ParseError'));
|
reject(new AppError(`Invalid JSON response: ${error.message}`, 'ParseError'));
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`);
|
|
||||||
reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError'));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
xhr.onerror = () => reject(new AppError('Network error occurred', 'NetworkError'));
|
|
||||||
xhr.send(JSON.stringify({
|
|
||||||
url: url,
|
|
||||||
headers: headers
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchCryptoCompareDataXHR: (coin) => {
|
|
||||||
const url = `${config.apiEndpoints.cryptoCompare}?fsyms=${coin}&tsyms=USD,BTC&api_key=${config.apiKeys.cryptoCompare}`;
|
|
||||||
const headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
|
||||||
};
|
|
||||||
return api.makePostRequest(url, headers).catch(error => ({
|
|
||||||
error: error.message
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
fetchCoinGeckoDataXHR: async () => {
|
|
||||||
const cacheKey = 'coinGeckoOneLiner';
|
|
||||||
const minRequestInterval = 30000;
|
|
||||||
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) {
|
|
||||||
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}`;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
|
|
||||||
} 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 };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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) return;
|
|
||||||
|
|
||||||
const cacheKey = `historical_${coin}_${config.currentResolution}`;
|
|
||||||
const cachedData = cache.get(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
results[coin] = cachedData.value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
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) {
|
} else {
|
||||||
results[coin] = response.Data;
|
logger.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`);
|
||||||
cache.set(cacheKey, response.Data);
|
reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError'));
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = () => reject(new AppError('Network error occurred', 'NetworkError'));
|
||||||
|
xhr.send(JSON.stringify({
|
||||||
|
url: url,
|
||||||
|
headers: headers
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchCryptoCompareDataXHR: (coin) => {
|
||||||
|
return rateLimiter.queueRequest('cryptocompare', async () => {
|
||||||
|
const url = `${config.apiEndpoints.cryptoCompare}?fsyms=${coin}&tsyms=USD,BTC&api_key=${config.apiKeys.cryptoCompare}`;
|
||||||
|
const headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
return await api.makePostRequest(url, headers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
|
return { error: error.message };
|
||||||
if (cachedData) {
|
|
||||||
results[coin] = cachedData.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchCoinGeckoDataXHR: async () => {
|
||||||
|
const cacheKey = 'coinGeckoOneLiner';
|
||||||
|
const cachedData = cache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cachedData) {
|
||||||
|
console.log('Using cached CoinGecko data');
|
||||||
|
return cachedData.value;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
await Promise.all(fetchPromises);
|
return rateLimiter.queueRequest('coingecko', async () => {
|
||||||
return results;
|
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}`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching CoinGecko data:', error);
|
||||||
|
const cachedData = cache.get(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
console.log('Using expired cache data due to error');
|
||||||
|
return cachedData.value;
|
||||||
|
}
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
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) return;
|
||||||
|
|
||||||
|
const cacheKey = `historical_${coin}_${config.currentResolution}`;
|
||||||
|
const cachedData = cache.get(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
results[coin] = cachedData.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coin === 'WOW') {
|
||||||
|
return rateLimiter.queueRequest('coingecko', async () => {
|
||||||
|
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 {
|
||||||
|
return rateLimiter.queueRequest('cryptocompare', async () => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(fetchPromises);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rateLimiter = {
|
||||||
|
lastRequestTime: {},
|
||||||
|
minRequestInterval: {
|
||||||
|
coingecko: 15000,
|
||||||
|
cryptocompare: 1000
|
||||||
|
},
|
||||||
|
requestQueue: {},
|
||||||
|
|
||||||
|
canMakeRequest: function(apiName) {
|
||||||
|
const now = Date.now();
|
||||||
|
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||||
|
return (now - lastRequest) >= this.minRequestInterval[apiName];
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLastRequestTime: function(apiName) {
|
||||||
|
this.lastRequestTime[apiName] = Date.now();
|
||||||
|
},
|
||||||
|
|
||||||
|
getWaitTime: function(apiName) {
|
||||||
|
const now = Date.now();
|
||||||
|
const lastRequest = this.lastRequestTime[apiName] || 0;
|
||||||
|
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
|
||||||
|
},
|
||||||
|
|
||||||
|
queueRequest: async function(apiName, requestFn) {
|
||||||
|
if (!this.requestQueue[apiName]) {
|
||||||
|
this.requestQueue[apiName] = Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.requestQueue[apiName];
|
||||||
|
|
||||||
|
const executeRequest = async () => {
|
||||||
|
const waitTime = this.getWaitTime(apiName);
|
||||||
|
if (waitTime > 0) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateLastRequestTime(apiName);
|
||||||
|
return await requestFn();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.requestQueue[apiName] = executeRequest();
|
||||||
|
return await this.requestQueue[apiName];
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Queue error for ${apiName}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// CACHE
|
// CACHE
|
||||||
|
@ -918,46 +983,47 @@ const app = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad: async () => {
|
onLoad: async () => {
|
||||||
console.log('App onLoad event triggered');
|
console.log('App onLoad event triggered');
|
||||||
ui.showLoader();
|
ui.showLoader();
|
||||||
try {
|
try {
|
||||||
volumeToggle.init();
|
volumeToggle.init();
|
||||||
await app.updateBTCPrice();
|
await app.updateBTCPrice();
|
||||||
const chartContainer = document.getElementById('coin-chart');
|
const chartContainer = document.getElementById('coin-chart');
|
||||||
if (chartContainer) {
|
if (chartContainer) {
|
||||||
chartModule.initChart();
|
chartModule.initChart();
|
||||||
chartModule.showChartLoader();
|
chartModule.showChartLoader();
|
||||||
} else {
|
}
|
||||||
//console.warn('Chart container not found, skipping chart initialization');
|
|
||||||
|
console.log('Loading all coin data...');
|
||||||
|
await app.loadAllCoinData();
|
||||||
|
|
||||||
|
if (chartModule.chart) {
|
||||||
|
config.currentResolution = 'day';
|
||||||
|
await chartModule.updateChart('BTC');
|
||||||
|
app.updateResolutionButtons('BTC');
|
||||||
|
|
||||||
|
|
||||||
|
const chartTitle = document.getElementById('chart-title');
|
||||||
|
if (chartTitle) {
|
||||||
|
chartTitle.textContent = 'Price Chart (BTC)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.setActiveContainer('btc-container');
|
||||||
|
|
||||||
|
app.setupEventListeners();
|
||||||
|
app.initializeSelectImages();
|
||||||
|
app.initAutoRefresh();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.');
|
||||||
|
} finally {
|
||||||
|
ui.hideLoader();
|
||||||
|
if (chartModule.chart) {
|
||||||
|
chartModule.hideChartLoader();
|
||||||
|
}
|
||||||
|
console.log('App onLoad completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Loading all coin data...');
|
|
||||||
await app.loadAllCoinData();
|
|
||||||
|
|
||||||
if (chartModule.chart) {
|
|
||||||
config.currentResolution = 'day';
|
|
||||||
await chartModule.updateChart('BTC');
|
|
||||||
app.updateResolutionButtons('BTC');
|
|
||||||
}
|
|
||||||
ui.setActiveContainer('btc-container');
|
|
||||||
|
|
||||||
//console.log('Setting up event listeners and initializations...');
|
|
||||||
app.setupEventListeners();
|
|
||||||
app.initializeSelectImages();
|
|
||||||
app.initAutoRefresh();
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
//console.error('Error during initialization:', error);
|
|
||||||
ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.');
|
|
||||||
} finally {
|
|
||||||
ui.hideLoader();
|
|
||||||
if (chartModule.chart) {
|
|
||||||
chartModule.hideChartLoader();
|
|
||||||
}
|
|
||||||
console.log('App onLoad completed');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
loadAllCoinData: async () => {
|
loadAllCoinData: async () => {
|
||||||
//console.log('Loading data for all coins...');
|
//console.log('Loading data for all coins...');
|
||||||
try {
|
try {
|
||||||
|
@ -1021,23 +1087,25 @@ const app = {
|
||||||
//console.log(`Data loaded for ${coin.symbol}`);
|
//console.log(`Data loaded for ${coin.symbol}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
setupEventListeners: () => {
|
setupEventListeners: () => {
|
||||||
//console.log('Setting up event listeners...');
|
|
||||||
config.coins.forEach(coin => {
|
config.coins.forEach(coin => {
|
||||||
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
|
const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener('click', () => {
|
container.addEventListener('click', () => {
|
||||||
//console.log(`${coin.symbol} container clicked`);
|
const chartTitle = document.getElementById('chart-title');
|
||||||
ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
|
if (chartTitle) {
|
||||||
if (chartModule.chart) {
|
chartTitle.textContent = `Price Chart (${coin.symbol})`;
|
||||||
if (coin.symbol === 'WOW') {
|
}
|
||||||
config.currentResolution = 'day';
|
ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
|
||||||
}
|
if (chartModule.chart) {
|
||||||
chartModule.updateChart(coin.symbol);
|
if (coin.symbol === 'WOW') {
|
||||||
app.updateResolutionButtons(coin.symbol);
|
config.currentResolution = 'day';
|
||||||
}
|
}
|
||||||
});
|
chartModule.updateChart(coin.symbol);
|
||||||
}
|
app.updateResolutionButtons(coin.symbol);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshAllButton = document.getElementById('refresh-all');
|
const refreshAllButton = document.getElementById('refresh-all');
|
||||||
|
@ -1102,79 +1170,142 @@ const app = {
|
||||||
} else {
|
} else {
|
||||||
nextRefreshTime = now + config.cacheTTL;
|
nextRefreshTime = now + config.cacheTTL;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeUntilRefresh = nextRefreshTime - now;
|
const timeUntilRefresh = nextRefreshTime - now;
|
||||||
console.log(`Next refresh scheduled in ${timeUntilRefresh / 1000} seconds`);
|
console.log(`Next refresh scheduled in ${timeUntilRefresh / 1000} seconds`);
|
||||||
|
|
||||||
app.nextRefreshTime = nextRefreshTime;
|
app.nextRefreshTime = nextRefreshTime;
|
||||||
app.autoRefreshInterval = setTimeout(() => {
|
app.autoRefreshInterval = setTimeout(() => {
|
||||||
console.log('Auto-refresh triggered');
|
console.log('Auto-refresh triggered');
|
||||||
app.refreshAllData();
|
app.refreshAllData();
|
||||||
}, timeUntilRefresh);
|
}, timeUntilRefresh);
|
||||||
|
|
||||||
localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString());
|
localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString());
|
||||||
app.updateNextRefreshTime();
|
app.updateNextRefreshTime();
|
||||||
},
|
},
|
||||||
|
refreshAllData: async () => {
|
||||||
refreshAllData: async () => {
|
if (app.isRefreshing) {
|
||||||
if (app.isRefreshing) {
|
console.log('Refresh already in progress, skipping...');
|
||||||
console.log('Refresh already in progress, skipping...');
|
return;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0;
|
||||||
|
const timeSinceLastRequest = Date.now() - lastGeckoRequest;
|
||||||
|
if (timeSinceLastRequest < rateLimiter.minRequestInterval.coingecko) {
|
||||||
|
let waitTime = Math.ceil((rateLimiter.minRequestInterval.coingecko - timeSinceLastRequest) / 1000);
|
||||||
|
|
||||||
|
ui.displayErrorMessage(`Rate limit: Please wait ${waitTime} seconds before refreshing`);
|
||||||
|
|
||||||
|
const countdownInterval = setInterval(() => {
|
||||||
|
waitTime--;
|
||||||
|
if (waitTime > 0) {
|
||||||
|
ui.displayErrorMessage(`Rate limit: Please wait ${waitTime} seconds before refreshing`);
|
||||||
|
} else {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
ui.hideErrorMessage();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Refreshing all data...');
|
||||||
|
app.isRefreshing = true;
|
||||||
|
ui.showLoader();
|
||||||
|
chartModule.showChartLoader();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ui.hideErrorMessage();
|
||||||
|
cache.clear();
|
||||||
|
|
||||||
|
const btcUpdateSuccess = await app.updateBTCPrice();
|
||||||
|
if (!btcUpdateSuccess) {
|
||||||
|
console.warn('BTC price update failed, continuing with cached or default value');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Refreshing all data...');
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
app.isRefreshing = true;
|
|
||||||
ui.showLoader();
|
|
||||||
chartModule.showChartLoader();
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
cache.clear();
|
const allCoinData = await api.fetchCoinGeckoDataXHR();
|
||||||
|
if (allCoinData.error) {
|
||||||
await app.updateBTCPrice();
|
throw new Error(`CoinGecko API Error: ${allCoinData.error}`);
|
||||||
|
}
|
||||||
const allCoinData = await api.fetchCoinGeckoDataXHR();
|
|
||||||
if (allCoinData.error) {
|
|
||||||
throw new Error(allCoinData.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const coin of config.coins) {
|
|
||||||
const symbol = coin.symbol.toLowerCase();
|
|
||||||
const coinData = allCoinData[symbol];
|
|
||||||
if (coinData) {
|
|
||||||
coinData.displayName = coin.displayName || coin.symbol;
|
|
||||||
|
|
||||||
ui.displayCoinData(coin.symbol, coinData);
|
const failedCoins = [];
|
||||||
|
|
||||||
const cacheKey = `coinData_${coin.symbol}`;
|
for (const coin of config.coins) {
|
||||||
cache.set(cacheKey, coinData);
|
const symbol = coin.symbol.toLowerCase();
|
||||||
} else {
|
const coinData = allCoinData[symbol];
|
||||||
//console.error(`No data found for ${coin.symbol}`);
|
|
||||||
|
try {
|
||||||
|
if (!coinData) {
|
||||||
|
throw new Error(`No data received`);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
coinData.displayName = coin.displayName || coin.symbol;
|
||||||
if (chartModule.currentCoin) {
|
ui.displayCoinData(coin.symbol, coinData);
|
||||||
await chartModule.updateChart(chartModule.currentCoin, true);
|
|
||||||
}
|
const cacheKey = `coinData_${coin.symbol}`;
|
||||||
|
cache.set(cacheKey, coinData);
|
||||||
app.lastRefreshedTime = new Date();
|
|
||||||
localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
|
} catch (coinError) {
|
||||||
ui.updateLastRefreshedTime();
|
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
|
||||||
|
failedCoins.push(coin.symbol);
|
||||||
console.log('All data refreshed successfully');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
//console.error('Error refreshing all data:', error);
|
|
||||||
ui.displayErrorMessage('Failed to refresh all data. Please try again.');
|
|
||||||
} finally {
|
|
||||||
ui.hideLoader();
|
|
||||||
chartModule.hideChartLoader();
|
|
||||||
app.isRefreshing = false;
|
|
||||||
if (app.isAutoRefreshEnabled) {
|
|
||||||
app.scheduleNextRefresh();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
if (chartModule.currentCoin) {
|
||||||
|
try {
|
||||||
|
await chartModule.updateChart(chartModule.currentCoin, true);
|
||||||
|
} catch (chartError) {
|
||||||
|
console.error('Chart update failed:', chartError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.lastRefreshedTime = new Date();
|
||||||
|
localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
|
||||||
|
ui.updateLastRefreshedTime();
|
||||||
|
|
||||||
|
if (failedCoins.length > 0) {
|
||||||
|
const failureMessage = failedCoins.length === config.coins.length
|
||||||
|
? 'Failed to update any coin data'
|
||||||
|
: `Failed to update some coins: ${failedCoins.join(', ')}`;
|
||||||
|
|
||||||
|
let countdown = 5;
|
||||||
|
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||||
|
|
||||||
|
const countdownInterval = setInterval(() => {
|
||||||
|
countdown--;
|
||||||
|
if (countdown > 0) {
|
||||||
|
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
|
||||||
|
} else {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
ui.hideErrorMessage();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
console.log(`Refresh completed. Failed coins: ${failedCoins.length}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Critical error during refresh:', error);
|
||||||
|
let countdown = 10;
|
||||||
|
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||||
|
const countdownInterval = setInterval(() => {
|
||||||
|
countdown--;
|
||||||
|
if (countdown > 0) {
|
||||||
|
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
|
||||||
|
} else {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
ui.hideErrorMessage();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
ui.hideLoader();
|
||||||
|
chartModule.hideChartLoader();
|
||||||
|
app.isRefreshing = false;
|
||||||
|
|
||||||
|
if (app.isAutoRefreshEnabled) {
|
||||||
|
app.scheduleNextRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
updateNextRefreshTime: () => {
|
updateNextRefreshTime: () => {
|
||||||
console.log('Updating next refresh time display');
|
console.log('Updating next refresh time display');
|
||||||
|
@ -1266,26 +1397,32 @@ const app = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateBTCPrice: async () => {
|
updateBTCPrice: async () => {
|
||||||
//console.log('Updating BTC price...');
|
console.log('Updating BTC price...');
|
||||||
try {
|
try {
|
||||||
const priceData = await api.fetchCoinGeckoDataXHR();
|
const priceData = await api.fetchCoinGeckoDataXHR();
|
||||||
if (priceData.error) {
|
|
||||||
//console.error('Error fetching BTC price:', priceData.error);
|
|
||||||
app.btcPriceUSD = 0;
|
|
||||||
} else if (priceData.btc && priceData.btc.current_price) {
|
|
||||||
|
|
||||||
app.btcPriceUSD = priceData.btc.current_price;
|
if (priceData.error) {
|
||||||
} else {
|
console.warn('API error when fetching BTC price:', priceData.error);
|
||||||
//console.error('Unexpected BTC data structure:', priceData);
|
return false;
|
||||||
app.btcPriceUSD = 0;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
//console.error('Error fetching BTC price:', error);
|
|
||||||
app.btcPriceUSD = 0;
|
|
||||||
}
|
}
|
||||||
//console.log('Current BTC price:', app.btcPriceUSD);
|
|
||||||
},
|
if (priceData.btc && typeof priceData.btc.current_price === 'number') {
|
||||||
|
app.btcPriceUSD = priceData.btc.current_price;
|
||||||
|
return true;
|
||||||
|
} else if (priceData.bitcoin && typeof priceData.bitcoin.usd === 'number') {
|
||||||
|
app.btcPriceUSD = priceData.bitcoin.usd;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Unexpected BTC price data structure:', priceData);
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching BTC price:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
sortTable: (columnIndex) => {
|
sortTable: (columnIndex) => {
|
||||||
//console.log(`Sorting column: ${columnIndex}`);
|
//console.log(`Sorting column: ${columnIndex}`);
|
||||||
|
|
Loading…
Reference in a new issue