Refactor + Optimizations

This commit is contained in:
gerlofvanek
2025-10-10 11:08:23 +02:00
parent 2f7e425da9
commit 73d486d6f0
79 changed files with 5835 additions and 4419 deletions

View File

@@ -4,34 +4,35 @@ const ApiManager = (function() {
isInitialized: false
};
const config = {
requestTimeout: 60000,
retryDelays: [5000, 15000, 30000],
rateLimits: {
coingecko: {
requestsPerMinute: 50,
minInterval: 1200
},
cryptocompare: {
requestsPerMinute: 30,
minInterval: 2000
function getConfig() {
return window.config || window.ConfigManager || {
requestTimeout: 60000,
retryDelays: [5000, 15000, 30000],
rateLimits: {
coingecko: { requestsPerMinute: 50, minInterval: 1200 },
cryptocompare: { requestsPerMinute: 30, minInterval: 2000 }
}
}
};
};
}
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
coingecko: 1200,
cryptocompare: 2000
},
requestQueue: {},
retryDelays: [5000, 15000, 30000],
getMinInterval: function(apiName) {
const config = getConfig();
return config.rateLimits?.[apiName]?.minInterval || 1200;
},
getRetryDelays: function() {
const config = getConfig();
return config.retryDelays || [5000, 15000, 30000];
},
canMakeRequest: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return (now - lastRequest) >= this.minRequestInterval[apiName];
return (now - lastRequest) >= this.getMinInterval(apiName);
},
updateLastRequestTime: function(apiName) {
@@ -41,7 +42,7 @@ const ApiManager = (function() {
getWaitTime: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
return Math.max(0, this.getMinInterval(apiName) - (now - lastRequest));
},
queueRequest: async function(apiName, requestFn, retryCount = 0) {
@@ -55,29 +56,30 @@ const ApiManager = (function() {
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
}
try {
this.updateLastRequestTime(apiName);
return await requestFn();
} catch (error) {
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
const retryDelays = this.getRetryDelays();
if (error.message.includes('429') && retryCount < retryDelays.length) {
const delay = retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
retryCount < retryDelays.length) {
const delay = retryDelays[retryCount];
console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
apiName,
retryCount,
error: error.message
});
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -118,19 +120,7 @@ const ApiManager = (function() {
}
if (options.config) {
Object.assign(config, options.config);
}
if (config.rateLimits) {
Object.keys(config.rateLimits).forEach(api => {
if (config.rateLimits[api].minInterval) {
rateLimiter.minRequestInterval[api] = config.rateLimits[api].minInterval;
}
});
}
if (config.retryDelays) {
rateLimiter.retryDelays = [...config.retryDelays];
console.log('[ApiManager] Config options provided, but using ConfigManager instead');
}
if (window.CleanupManager) {
@@ -143,6 +133,31 @@ const ApiManager = (function() {
},
makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
if (window.ErrorHandler) {
return window.ErrorHandler.safeExecuteAsync(async () => {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
...headers
},
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
};
if (body) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}, `ApiManager.makeRequest(${url})`, null);
}
try {
const options = {
method: method,
@@ -150,7 +165,7 @@ const ApiManager = (function() {
'Content-Type': 'application/json',
...headers
},
signal: AbortSignal.timeout(config.requestTimeout)
signal: AbortSignal.timeout(getConfig().requestTimeout || 60000)
};
if (body) {
@@ -233,11 +248,8 @@ const ApiManager = (function() {
.join(',') :
'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin';
//console.log('Fetching coin prices for:', coins);
const response = await this.fetchCoinPrices(coins);
//console.log('Full API response:', response);
if (!response || typeof response !== 'object') {
throw new Error('Invalid response type');
}
@@ -260,80 +272,38 @@ const ApiManager = (function() {
fetchVolumeData: async function() {
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
let coinList = (window.config && window.config.coins) ?
window.config.coins
.filter(coin => coin.usesCoinGecko)
.map(coin => {
return window.config.getCoinBackendId ?
window.config.getCoinBackendId(coin.name) :
(typeof getCoinBackendId === 'function' ?
getCoinBackendId(coin.name) : coin.name.toLowerCase());
})
.join(',') :
'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred,namecoin,wownero';
const coinSymbols = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: (window.config.coins
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
if (!coinList.includes('zcoin') && coinList.includes('firo')) {
coinList = coinList + ',zcoin';
}
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coinList}&vs_currencies=usd&include_24hr_vol=true&include_24hr_change=true`;
const response = await this.makePostRequest(url, {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
const response = await this.makeRequest('/json/coinvolume', 'POST', {}, {
coins: coinSymbols.join(','),
source: 'coingecko.com',
ttl: 300
});
if (!response || typeof response !== 'object') {
throw new Error('Invalid response from CoinGecko API');
if (!response) {
console.error('No response from backend');
throw new Error('Invalid response from backend');
}
if (!response.data) {
console.error('Response missing data field:', response);
throw new Error('Invalid response from backend');
}
const volumeData = {};
Object.entries(response).forEach(([coinId, data]) => {
if (data && data.usd_24h_vol !== undefined) {
volumeData[coinId] = {
total_volume: data.usd_24h_vol || 0,
price_change_percentage_24h: data.usd_24h_change || 0
};
}
Object.entries(response.data).forEach(([coinSymbol, data]) => {
const coinKey = coinSymbol.toLowerCase();
volumeData[coinKey] = {
total_volume: (data.volume_24h !== undefined && data.volume_24h !== null) ? data.volume_24h : null,
price_change_percentage_24h: data.price_change_24h || 0
};
});
const coinMappings = {
'firo': ['firo', 'zcoin'],
'zcoin': ['zcoin', 'firo'],
'bitcoin-cash': ['bitcoin-cash', 'bitcoincash', 'bch'],
'bitcoincash': ['bitcoincash', 'bitcoin-cash', 'bch'],
'particl': ['particl', 'part']
};
if (response['zcoin'] && (!volumeData['firo'] || volumeData['firo'].total_volume === 0)) {
volumeData['firo'] = {
total_volume: response['zcoin'].usd_24h_vol || 0,
price_change_percentage_24h: response['zcoin'].usd_24h_change || 0
};
}
if (response['bitcoin-cash'] && (!volumeData['bitcoincash'] || volumeData['bitcoincash'].total_volume === 0)) {
volumeData['bitcoincash'] = {
total_volume: response['bitcoin-cash'].usd_24h_vol || 0,
price_change_percentage_24h: response['bitcoin-cash'].usd_24h_change || 0
};
}
for (const [mainCoin, alternativeIds] of Object.entries(coinMappings)) {
if (!volumeData[mainCoin] || volumeData[mainCoin].total_volume === 0) {
for (const altId of alternativeIds) {
if (response[altId] && response[altId].usd_24h_vol) {
volumeData[mainCoin] = {
total_volume: response[altId].usd_24h_vol,
price_change_percentage_24h: response[altId].usd_24h_change || 0
};
break;
}
}
}
}
return volumeData;
} catch (error) {
console.error("Error fetching volume data:", error);
@@ -342,104 +312,45 @@ const ApiManager = (function() {
});
},
fetchCryptoCompareData: function(coin) {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
const url = `https://min-api.cryptocompare.com/data/pricemultifull?fsyms=${coin}&tsyms=USD,BTC&api_key=${apiKey}`;
const headers = {
'User-Agent': 'Mozilla/5.0',
'Accept': 'application/json'
};
return await this.makePostRequest(url, headers);
} catch (error) {
console.error(`CryptoCompare request failed for ${coin}:`, error);
throw error;
}
});
},
fetchHistoricalData: async function(coinSymbols, resolution = 'day') {
if (!Array.isArray(coinSymbols)) {
coinSymbols = [coinSymbols];
}
const results = {};
const fetchPromises = coinSymbols.map(async coin => {
let useCoinGecko = false;
let coingeckoId = null;
if (window.CoinManager) {
const coinConfig = window.CoinManager.getCoinByAnyIdentifier(coin);
if (coinConfig) {
useCoinGecko = !coinConfig.usesCryptoCompare || coin === 'PART';
coingeckoId = coinConfig.coingeckoId;
return this.rateLimiter.queueRequest('coingecko', async () => {
try {
let days;
if (resolution === 'day') {
days = 1;
} else if (resolution === 'year') {
days = 365;
} else {
days = 180;
}
} else {
const coinGeckoCoins = {
'WOW': 'wownero',
'PART': 'particl',
'BTC': 'bitcoin'
};
if (coinGeckoCoins[coin]) {
useCoinGecko = true;
coingeckoId = coinGeckoCoins[coin];
const response = await this.makeRequest('/json/coinhistory', 'POST', {}, {
coins: coinSymbols.join(','),
days: days,
source: 'coingecko.com',
ttl: 3600
});
if (!response) {
console.error('No response from backend');
throw new Error('Invalid response from backend');
}
}
if (useCoinGecko && coingeckoId) {
return this.rateLimiter.queueRequest('coingecko', async () => {
let days;
if (resolution === 'day') {
days = 1;
} else if (resolution === 'year') {
days = 365;
} else {
days = 180;
}
const url = `https://api.coingecko.com/api/v3/coins/${coingeckoId}/market_chart?vs_currency=usd&days=${days}`;
try {
const response = await this.makePostRequest(url);
if (response && response.prices) {
results[coin] = response.prices;
}
} catch (error) {
console.error(`Error fetching CoinGecko data for ${coin}:`, error);
throw error;
}
});
} else {
return this.rateLimiter.queueRequest('cryptocompare', async () => {
try {
const apiKey = window.config?.apiKeys?.cryptoCompare || '';
let url;
if (!response.data) {
console.error('Response missing data field:', response);
throw new Error('Invalid response from backend');
}
if (resolution === 'day') {
url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=24&api_key=${apiKey}`;
} else if (resolution === 'year') {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=365&api_key=${apiKey}`;
} else {
url = `https://min-api.cryptocompare.com/data/v2/histoday?fsym=${coin}&tsym=USD&limit=180&api_key=${apiKey}`;
}
const response = await this.makePostRequest(url);
if (response.Response === "Error") {
console.error(`API Error for ${coin}:`, response.Message);
throw new Error(response.Message);
} else if (response.Data && response.Data.Data) {
results[coin] = response.Data;
}
} catch (error) {
console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
throw error;
}
});
return response.data;
} catch (error) {
console.error('Error fetching historical data:', error);
throw error;
}
});
await Promise.all(fetchPromises);
return results;
},
dispose: function() {
@@ -453,17 +364,6 @@ const ApiManager = (function() {
return publicAPI;
})();
function getCoinBackendId(coinName) {
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'zcoin',
'zcoin': 'zcoin',
'bitcoincash': 'bitcoin-cash'
};
return nameMap[coinName.toLowerCase()] || coinName.toLowerCase();
}
window.Api = ApiManager;
window.ApiManager = ApiManager;
@@ -474,5 +374,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
console.log('ApiManager initialized');

View File

@@ -15,7 +15,21 @@ const BalanceUpdatesManager = (function() {
initialized: false
};
function fetchBalanceData() {
async function fetchBalanceData() {
if (window.ApiManager) {
const data = await window.ApiManager.makeRequest('/json/walletbalances', 'GET');
if (data && data.error) {
throw new Error(data.error);
}
if (!Array.isArray(data)) {
throw new Error('Invalid response format');
}
return data;
}
return fetch('/json/walletbalances', {
headers: {
'Accept': 'application/json',
@@ -43,27 +57,41 @@ const BalanceUpdatesManager = (function() {
function clearTimeoutByKey(key) {
if (state.timeouts.has(key)) {
clearTimeout(state.timeouts.get(key));
const timeoutId = state.timeouts.get(key);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
state.timeouts.delete(key);
}
}
function setTimeoutByKey(key, callback, delay) {
clearTimeoutByKey(key);
const timeoutId = setTimeout(callback, delay);
const timeoutId = window.CleanupManager
? window.CleanupManager.setTimeout(callback, delay)
: setTimeout(callback, delay);
state.timeouts.set(key, timeoutId);
}
function clearIntervalByKey(key) {
if (state.intervals.has(key)) {
clearInterval(state.intervals.get(key));
const intervalId = state.intervals.get(key);
if (window.CleanupManager) {
window.CleanupManager.clearInterval(intervalId);
} else {
clearInterval(intervalId);
}
state.intervals.delete(key);
}
}
function setIntervalByKey(key, callback, interval) {
clearIntervalByKey(key);
const intervalId = setInterval(callback, interval);
const intervalId = window.CleanupManager
? window.CleanupManager.setInterval(callback, interval)
: setInterval(callback, interval);
state.intervals.set(key, intervalId);
}

View File

@@ -1,9 +1,19 @@
const CacheManager = (function() {
const defaults = window.config?.cacheConfig?.storage || {
maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200,
defaultTTL: 5 * 60 * 1000
};
function getDefaults() {
if (window.config?.cacheConfig?.storage) {
return window.config.cacheConfig.storage;
}
if (window.ConfigManager?.cacheConfig?.storage) {
return window.ConfigManager.cacheConfig.storage;
}
return {
maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200,
defaultTTL: 5 * 60 * 1000
};
}
const defaults = getDefaults();
const PRICES_CACHE_KEY = 'crypto_prices_unified';
@@ -45,8 +55,12 @@ const CacheManager = (function() {
const cacheAPI = {
getTTL: function(resourceType) {
const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
const ttlConfig = window.config?.cacheConfig?.ttlSettings ||
window.ConfigManager?.cacheConfig?.ttlSettings || {};
const defaultTTL = window.config?.cacheConfig?.defaultTTL ||
window.ConfigManager?.cacheConfig?.defaultTTL ||
defaults.defaultTTL;
return ttlConfig[resourceType] || defaultTTL;
},
set: function(key, value, resourceTypeOrCustomTtl = null) {
@@ -73,13 +87,18 @@ const CacheManager = (function() {
expiresAt: Date.now() + ttl
};
let serializedItem;
try {
serializedItem = JSON.stringify(item);
} catch (e) {
console.error('Failed to serialize cache item:', e);
return false;
}
const serializedItem = window.ErrorHandler
? window.ErrorHandler.safeExecute(() => JSON.stringify(item), 'CacheManager.set.serialize', null)
: (() => {
try {
return JSON.stringify(item);
} catch (e) {
console.error('Failed to serialize cache item:', e);
return null;
}
})();
if (!serializedItem) return false;
const itemSize = new Blob([serializedItem]).size;
if (itemSize > defaults.maxSizeBytes) {
@@ -118,7 +137,7 @@ const CacheManager = (function() {
const keysToDelete = Array.from(memoryCache.keys())
.filter(k => isCacheKey(k))
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
.slice(0, Math.floor(memoryCache.size * 0.2)); // Remove oldest 20%
.slice(0, Math.floor(memoryCache.size * 0.2));
keysToDelete.forEach(k => memoryCache.delete(k));
}
@@ -285,7 +304,7 @@ const CacheManager = (function() {
const keysToDelete = Array.from(memoryCache.keys())
.filter(key => isCacheKey(key))
.sort((a, b) => memoryCache.get(a).timestamp - memoryCache.get(b).timestamp)
.slice(0, Math.floor(memoryCache.size * 0.3)); // Remove oldest 30% during aggressive cleanup
.slice(0, Math.floor(memoryCache.size * 0.3));
keysToDelete.forEach(key => memoryCache.delete(key));
}
@@ -328,7 +347,6 @@ const CacheManager = (function() {
.filter(key => isCacheKey(key))
.forEach(key => memoryCache.delete(key));
//console.log("Cache cleared successfully");
return true;
},
@@ -531,6 +549,4 @@ const CacheManager = (function() {
window.CacheManager = CacheManager;
//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
console.log('CacheManager initialized');

View File

@@ -233,7 +233,7 @@ const CleanupManager = (function() {
},
setupMemoryOptimization: function(options = {}) {
const memoryCheckInterval = options.interval || 2 * 60 * 1000; // Default: 2 minutes
const memoryCheckInterval = options.interval || 2 * 60 * 1000;
const maxCacheSize = options.maxCacheSize || 100;
const maxDataSize = options.maxDataSize || 1000;

View File

@@ -178,19 +178,7 @@ const CoinManager = (function() {
function getCoinByAnyIdentifier(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
const coin = coinAliasesMap[normalizedId];
if (coin) return coin;
if (normalizedId.includes('bitcoin') && normalizedId.includes('cash') ||
normalizedId === 'bch') {
return symbolToInfo['bch'];
}
if (normalizedId === 'zcoin' || normalizedId.includes('firo')) {
return symbolToInfo['firo'];
}
if (normalizedId.includes('particl')) {
return symbolToInfo['part'];
}
return null;
return coinAliasesMap[normalizedId] || null;
}
return {

View File

@@ -0,0 +1,191 @@
const CoinUtils = (function() {
function buildAliasesFromCoinManager() {
const aliases = {};
const symbolMap = {};
if (window.CoinManager) {
const coins = window.CoinManager.getAllCoins();
coins.forEach(coin => {
const canonical = coin.name.toLowerCase();
aliases[canonical] = coin.aliases || [coin.name.toLowerCase()];
symbolMap[canonical] = coin.symbol;
});
}
return { aliases, symbolMap };
}
let COIN_ALIASES = {};
let CANONICAL_TO_SYMBOL = {};
function initializeAliases() {
const { aliases, symbolMap } = buildAliasesFromCoinManager();
COIN_ALIASES = aliases;
CANONICAL_TO_SYMBOL = symbolMap;
}
if (window.CoinManager) {
initializeAliases();
} else {
document.addEventListener('DOMContentLoaded', () => {
if (window.CoinManager) {
initializeAliases();
}
});
}
function getCanonicalName(coin) {
if (!coin) return null;
const lower = coin.toString().toLowerCase().trim();
for (const [canonical, aliases] of Object.entries(COIN_ALIASES)) {
if (aliases.includes(lower)) {
return canonical;
}
}
return lower;
}
return {
normalizeCoinName: function(coin, priceData = null) {
const canonical = getCanonicalName(coin);
if (!canonical) return null;
if (priceData) {
if (canonical === 'bitcoin-cash') {
if (priceData['bitcoin-cash']) return 'bitcoin-cash';
if (priceData['bch']) return 'bch';
if (priceData['bitcoincash']) return 'bitcoincash';
return 'bitcoin-cash';
}
if (canonical === 'particl') {
if (priceData['part']) return 'part';
if (priceData['particl']) return 'particl';
return 'part';
}
}
return canonical;
},
isSameCoin: function(coin1, coin2) {
if (!coin1 || !coin2) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(coin1, coin2);
}
const canonical1 = getCanonicalName(coin1);
const canonical2 = getCanonicalName(coin2);
if (canonical1 === canonical2) return true;
const lower1 = coin1.toString().toLowerCase().trim();
const lower2 = coin2.toString().toLowerCase().trim();
const particlVariants = ['particl', 'particl anon', 'particl blind', 'part', 'part_anon', 'part_blind'];
if (particlVariants.includes(lower1) && particlVariants.includes(lower2)) {
return true;
}
if (lower1.includes(' ') || lower2.includes(' ')) {
const word1 = lower1.split(' ')[0];
const word2 = lower2.split(' ')[0];
if (word1 === word2 && word1.length > 4) {
return true;
}
}
return false;
},
getCoinSymbol: function(identifier) {
if (!identifier) return null;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
if (coin) return coin.symbol;
}
const canonical = getCanonicalName(identifier);
if (canonical && CANONICAL_TO_SYMBOL[canonical]) {
return CANONICAL_TO_SYMBOL[canonical];
}
return identifier.toString().toUpperCase();
},
getDisplayName: function(identifier) {
if (!identifier) return null;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(identifier);
if (coin) return coin.displayName || coin.name;
}
const symbol = this.getCoinSymbol(identifier);
return symbol || identifier;
},
getCoinImage: function(coinName) {
if (!coinName) return null;
const canonical = getCanonicalName(coinName);
const symbol = this.getCoinSymbol(canonical);
if (!symbol) return null;
const imagePath = `/static/images/coins/${symbol.toLowerCase()}.png`;
return imagePath;
},
getPriceKey: function(coin, priceData = null) {
return this.normalizeCoinName(coin, priceData);
},
getCoingeckoId: function(coinName) {
if (!coinName) return null;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
if (coin && coin.coingeckoId) {
return coin.coingeckoId;
}
}
const canonical = getCanonicalName(coinName);
return canonical;
},
formatCoinAmount: function(amount, decimals = 8) {
if (amount === null || amount === undefined) return '0';
const numAmount = parseFloat(amount);
if (isNaN(numAmount)) return '0';
return numAmount.toFixed(decimals).replace(/\.?0+$/, '');
},
getAllAliases: function(coin) {
const canonical = getCanonicalName(coin);
return COIN_ALIASES[canonical] || [canonical];
},
isValidCoin: function(coin) {
if (!coin) return false;
const canonical = getCanonicalName(coin);
return canonical !== null && COIN_ALIASES.hasOwnProperty(canonical);
},
refreshAliases: function() {
initializeAliases();
return Object.keys(COIN_ALIASES).length;
}
};
})();
if (typeof window !== 'undefined') {
window.CoinUtils = CoinUtils;
}
console.log('CoinUtils module loaded');

View File

@@ -53,20 +53,11 @@ const ConfigManager = (function() {
},
retryDelays: [5000, 15000, 30000],
get coins() {
return window.CoinManager ? window.CoinManager.getAllCoins() : [
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
];
if (window.CoinManager) {
return window.CoinManager.getAllCoins();
}
console.warn('[ConfigManager] CoinManager not available, returning empty array');
return [];
},
chartConfig: {
colors: {
@@ -122,55 +113,20 @@ const ConfigManager = (function() {
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coinName);
}
const nameMap = {
'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash',
'firo': 'firo',
'zcoin': 'firo',
'bitcoincash': 'bitcoin-cash'
};
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
return nameMap[lowerCoinName] || lowerCoinName;
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coinName);
}
return typeof coinName === 'string' ? coinName.toLowerCase() : '';
},
coinMatches: function(offerCoin, filterCoin) {
if (!offerCoin || !filterCoin) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(offerCoin, filterCoin);
}
offerCoin = offerCoin.toLowerCase();
filterCoin = filterCoin.toLowerCase();
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
if (window.CoinUtils) {
return window.CoinUtils.isSameCoin(offerCoin, filterCoin);
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
return true;
}
const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true;
}
if (filterCoin.includes(' ') || offerCoin.includes(' ')) {
const filterFirstWord = filterCoin.split(' ')[0];
const offerFirstWord = offerCoin.split(' ')[0];
if (filterFirstWord === 'bitcoin' && offerFirstWord === 'bitcoin') {
const filterHasCash = filterCoin.includes('cash');
const offerHasCash = offerCoin.includes('cash');
return filterHasCash === offerHasCash;
}
if (filterFirstWord === offerFirstWord && filterFirstWord.length > 4) {
return true;
}
}
if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin;
}
return false;
return offerCoin.toLowerCase() === filterCoin.toLowerCase();
},
update: function(path, value) {
const parts = path.split('.');
@@ -229,7 +185,7 @@ const ConfigManager = (function() {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
timeoutId = CleanupManager.setTimeout(() => func(...args), delay);
};
},
formatTimeLeft: function(timestamp) {

View File

@@ -0,0 +1,207 @@
(function() {
'use strict';
const originalGetElementById = document.getElementById.bind(document);
const DOMCache = {
cache: {},
get: function(id, forceRefresh = false) {
if (!id) {
console.warn('DOMCache: No ID provided');
return null;
}
if (!forceRefresh && this.cache[id]) {
if (document.body.contains(this.cache[id])) {
return this.cache[id];
} else {
delete this.cache[id];
}
}
const element = originalGetElementById(id);
if (element) {
this.cache[id] = element;
}
return element;
},
getMultiple: function(ids) {
const elements = {};
ids.forEach(id => {
elements[id] = this.get(id);
});
return elements;
},
setValue: function(id, value) {
const element = this.get(id);
if (element) {
element.value = value;
return true;
}
console.warn(`DOMCache: Element not found: ${id}`);
return false;
},
getValue: function(id, defaultValue = '') {
const element = this.get(id);
return element ? element.value : defaultValue;
},
setText: function(id, text) {
const element = this.get(id);
if (element) {
element.textContent = text;
return true;
}
console.warn(`DOMCache: Element not found: ${id}`);
return false;
},
getText: function(id, defaultValue = '') {
const element = this.get(id);
return element ? element.textContent : defaultValue;
},
addClass: function(id, className) {
const element = this.get(id);
if (element) {
element.classList.add(className);
return true;
}
return false;
},
removeClass: function(id, className) {
const element = this.get(id);
if (element) {
element.classList.remove(className);
return true;
}
return false;
},
toggleClass: function(id, className) {
const element = this.get(id);
if (element) {
element.classList.toggle(className);
return true;
}
return false;
},
show: function(id) {
const element = this.get(id);
if (element) {
element.style.display = '';
return true;
}
return false;
},
hide: function(id) {
const element = this.get(id);
if (element) {
element.style.display = 'none';
return true;
}
return false;
},
exists: function(id) {
return this.get(id) !== null;
},
clear: function(id) {
if (id) {
delete this.cache[id];
} else {
this.cache = {};
}
},
size: function() {
return Object.keys(this.cache).length;
},
validate: function() {
const ids = Object.keys(this.cache);
let removed = 0;
ids.forEach(id => {
const element = this.cache[id];
if (!document.body.contains(element)) {
delete this.cache[id];
removed++;
}
});
return removed;
},
createScope: function(elementIds) {
const scope = {};
elementIds.forEach(id => {
Object.defineProperty(scope, id, {
get: () => this.get(id),
enumerable: true
});
});
return scope;
},
batch: function(operations) {
Object.keys(operations).forEach(id => {
const ops = operations[id];
const element = this.get(id);
if (!element) {
console.warn(`DOMCache: Element not found in batch operation: ${id}`);
return;
}
if (ops.value !== undefined) element.value = ops.value;
if (ops.text !== undefined) element.textContent = ops.text;
if (ops.html !== undefined) element.innerHTML = ops.html;
if (ops.class) element.classList.add(ops.class);
if (ops.removeClass) element.classList.remove(ops.removeClass);
if (ops.hide) element.style.display = 'none';
if (ops.show) element.style.display = '';
if (ops.disabled !== undefined) element.disabled = ops.disabled;
});
}
};
window.DOMCache = DOMCache;
if (!window.$) {
window.$ = function(id) {
return DOMCache.get(id);
};
}
document.getElementById = function(id) {
return DOMCache.get(id);
};
document.getElementByIdOriginal = originalGetElementById;
if (window.CleanupManager) {
const validationInterval = CleanupManager.setInterval(() => {
DOMCache.validate();
}, 30000);
CleanupManager.registerResource('domCacheValidation', validationInterval, () => {
clearInterval(validationInterval);
});
}
})();

View File

@@ -0,0 +1,215 @@
const ErrorHandler = (function() {
const config = {
logErrors: true,
throwErrors: false,
errorCallbacks: []
};
function formatError(error, context) {
const timestamp = new Date().toISOString();
const contextStr = context ? ` [${context}]` : '';
if (error instanceof Error) {
return `${timestamp}${contextStr} ${error.name}: ${error.message}`;
}
return `${timestamp}${contextStr} ${String(error)}`;
}
function notifyCallbacks(error, context) {
config.errorCallbacks.forEach(callback => {
try {
callback(error, context);
} catch (e) {
console.error('[ErrorHandler] Error in callback:', e);
}
});
}
return {
configure: function(options = {}) {
Object.assign(config, options);
return this;
},
addCallback: function(callback) {
if (typeof callback === 'function') {
config.errorCallbacks.push(callback);
}
return this;
},
removeCallback: function(callback) {
const index = config.errorCallbacks.indexOf(callback);
if (index > -1) {
config.errorCallbacks.splice(index, 1);
}
return this;
},
safeExecute: function(fn, context = null, fallbackValue = null) {
try {
return fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
},
safeExecuteAsync: async function(fn, context = null, fallbackValue = null) {
try {
return await fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
},
wrap: function(fn, context = null, fallbackValue = null) {
return (...args) => {
try {
return fn(...args);
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
};
},
wrapAsync: function(fn, context = null, fallbackValue = null) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
}
};
},
handleError: function(error, context = null, fallbackValue = null) {
if (config.logErrors) {
console.error(formatError(error, context));
}
notifyCallbacks(error, context);
if (config.throwErrors) {
throw error;
}
return fallbackValue;
},
try: function(fn, catchFn = null, finallyFn = null) {
try {
return fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, 'ErrorHandler.try'));
}
notifyCallbacks(error, 'ErrorHandler.try');
if (catchFn) {
return catchFn(error);
}
if (config.throwErrors) {
throw error;
}
return null;
} finally {
if (finallyFn) {
finallyFn();
}
}
},
tryAsync: async function(fn, catchFn = null, finallyFn = null) {
try {
return await fn();
} catch (error) {
if (config.logErrors) {
console.error(formatError(error, 'ErrorHandler.tryAsync'));
}
notifyCallbacks(error, 'ErrorHandler.tryAsync');
if (catchFn) {
return await catchFn(error);
}
if (config.throwErrors) {
throw error;
}
return null;
} finally {
if (finallyFn) {
await finallyFn();
}
}
},
createBoundary: function(context) {
return {
execute: (fn, fallbackValue = null) => {
return ErrorHandler.safeExecute(fn, context, fallbackValue);
},
executeAsync: (fn, fallbackValue = null) => {
return ErrorHandler.safeExecuteAsync(fn, context, fallbackValue);
},
wrap: (fn, fallbackValue = null) => {
return ErrorHandler.wrap(fn, context, fallbackValue);
},
wrapAsync: (fn, fallbackValue = null) => {
return ErrorHandler.wrapAsync(fn, context, fallbackValue);
}
};
}
};
})();
if (typeof window !== 'undefined') {
window.ErrorHandler = ErrorHandler;
}
console.log('ErrorHandler module loaded');

View File

@@ -0,0 +1,342 @@
(function() {
'use strict';
const EventHandlers = {
confirmPopup: function(action = 'proceed', coinName = '') {
const message = action === 'Accept'
? 'Are you sure you want to accept this bid?'
: coinName
? `Are you sure you want to ${action} ${coinName}?`
: 'Are you sure you want to proceed?';
return confirm(message);
},
confirmReseed: function() {
return confirm('Are you sure you want to reseed the wallet? This will generate new addresses.');
},
confirmWithdrawal: function() {
if (window.WalletPage && typeof window.WalletPage.confirmWithdrawal === 'function') {
return window.WalletPage.confirmWithdrawal();
}
return confirm('Are you sure you want to withdraw? Please verify the address and amount.');
},
confirmUTXOResize: function() {
return confirm('Are you sure you want to create a UTXO? This will split your balance.');
},
confirmRemoveExpired: function() {
return confirm('Are you sure you want to remove all expired offers and bids?');
},
fillDonationAddress: function(address, coinType) {
let addressInput = null;
addressInput = window.DOMCache
? window.DOMCache.get('address_to')
: document.getElementById('address_to');
if (!addressInput) {
addressInput = document.querySelector('input[name^="to_"]');
}
if (!addressInput) {
addressInput = document.querySelector('input[placeholder*="Address"]');
}
if (addressInput) {
addressInput.value = address;
console.log(`Filled donation address for ${coinType}: ${address}`);
} else {
console.error('EventHandlers: Address input not found');
}
},
setAmmAmount: function(percent, inputId) {
const amountInput = window.DOMCache
? window.DOMCache.get(inputId)
: document.getElementById(inputId);
if (!amountInput) {
console.error('EventHandlers: AMM amount input not found:', inputId);
return;
}
const balanceElement = amountInput.closest('form')?.querySelector('[data-balance]');
const balance = balanceElement ? parseFloat(balanceElement.getAttribute('data-balance')) : 0;
if (balance > 0) {
const calculatedAmount = balance * percent;
amountInput.value = calculatedAmount.toFixed(8);
} else {
console.warn('EventHandlers: No balance found for AMM amount calculation');
}
},
setOfferAmount: function(percent, inputId) {
const amountInput = window.DOMCache
? window.DOMCache.get(inputId)
: document.getElementById(inputId);
if (!amountInput) {
console.error('EventHandlers: Offer amount input not found:', inputId);
return;
}
const coinFromSelect = document.getElementById('coin_from');
if (!coinFromSelect) {
console.error('EventHandlers: coin_from select not found');
return;
}
const selectedOption = coinFromSelect.options[coinFromSelect.selectedIndex];
if (!selectedOption || selectedOption.value === '-1') {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Please select a coin first');
} else {
alert('Please select a coin first');
}
return;
}
const balance = selectedOption.getAttribute('data-balance');
if (!balance) {
console.error('EventHandlers: Balance not found for selected coin');
return;
}
const floatBalance = parseFloat(balance);
if (isNaN(floatBalance) || floatBalance <= 0) {
if (window.showErrorModal) {
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
} else {
alert('Invalid balance for selected coin');
}
return;
}
const calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
},
resetForm: function() {
const form = document.querySelector('form[name="offer_form"]') || document.querySelector('form');
if (form) {
form.reset();
}
},
hideConfirmModal: function() {
if (window.DOMCache) {
window.DOMCache.hide('confirmModal');
} else {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.style.display = 'none';
}
}
},
lookup_rates: function() {
if (window.lookup_rates && typeof window.lookup_rates === 'function') {
window.lookup_rates();
} else {
console.error('EventHandlers: lookup_rates function not found');
}
},
checkForUpdatesNow: function() {
if (window.checkForUpdatesNow && typeof window.checkForUpdatesNow === 'function') {
window.checkForUpdatesNow();
} else {
console.error('EventHandlers: checkForUpdatesNow function not found');
}
},
testUpdateNotification: function() {
if (window.testUpdateNotification && typeof window.testUpdateNotification === 'function') {
window.testUpdateNotification();
} else {
console.error('EventHandlers: testUpdateNotification function not found');
}
},
toggleNotificationDropdown: function(event) {
if (window.toggleNotificationDropdown && typeof window.toggleNotificationDropdown === 'function') {
window.toggleNotificationDropdown(event);
} else {
console.error('EventHandlers: toggleNotificationDropdown function not found');
}
},
closeMessage: function(messageId) {
if (window.DOMCache) {
window.DOMCache.hide(messageId);
} else {
const messageElement = document.getElementById(messageId);
if (messageElement) {
messageElement.style.display = 'none';
}
}
},
initialize: function() {
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm]');
if (target) {
const action = target.getAttribute('data-confirm-action') || 'proceed';
const coinName = target.getAttribute('data-confirm-coin') || '';
if (!this.confirmPopup(action, coinName)) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-reseed]');
if (target) {
if (!this.confirmReseed()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-utxo]');
if (target) {
if (!this.confirmUTXOResize()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-confirm-remove-expired]');
if (target) {
if (!this.confirmRemoveExpired()) {
e.preventDefault();
return false;
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-fill-donation]');
if (target) {
e.preventDefault();
const address = target.getAttribute('data-address');
const coinType = target.getAttribute('data-coin-type');
this.fillDonationAddress(address, coinType);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-set-amm-amount]');
if (target) {
e.preventDefault();
const percent = parseFloat(target.getAttribute('data-set-amm-amount'));
const inputId = target.getAttribute('data-input-id');
this.setAmmAmount(percent, inputId);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-set-offer-amount]');
if (target) {
e.preventDefault();
const percent = parseFloat(target.getAttribute('data-set-offer-amount'));
const inputId = target.getAttribute('data-input-id');
this.setOfferAmount(percent, inputId);
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-reset-form]');
if (target) {
e.preventDefault();
this.resetForm();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-hide-modal]');
if (target) {
e.preventDefault();
this.hideConfirmModal();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-lookup-rates]');
if (target) {
e.preventDefault();
this.lookup_rates();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-check-updates]');
if (target) {
e.preventDefault();
this.checkForUpdatesNow();
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-test-notification]');
if (target) {
e.preventDefault();
const type = target.getAttribute('data-test-notification');
if (type === 'update') {
this.testUpdateNotification();
} else {
window.NotificationManager && window.NotificationManager.testToasts();
}
}
});
document.addEventListener('click', (e) => {
const target = e.target.closest('[data-close-message]');
if (target) {
e.preventDefault();
const messageId = target.getAttribute('data-close-message');
this.closeMessage(messageId);
}
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
EventHandlers.initialize();
});
} else {
EventHandlers.initialize();
}
window.EventHandlers = EventHandlers;
window.confirmPopup = EventHandlers.confirmPopup.bind(EventHandlers);
window.confirmReseed = EventHandlers.confirmReseed.bind(EventHandlers);
window.confirmWithdrawal = EventHandlers.confirmWithdrawal.bind(EventHandlers);
window.confirmUTXOResize = EventHandlers.confirmUTXOResize.bind(EventHandlers);
window.confirmRemoveExpired = EventHandlers.confirmRemoveExpired.bind(EventHandlers);
window.fillDonationAddress = EventHandlers.fillDonationAddress.bind(EventHandlers);
window.setAmmAmount = EventHandlers.setAmmAmount.bind(EventHandlers);
window.setOfferAmount = EventHandlers.setOfferAmount.bind(EventHandlers);
window.resetForm = EventHandlers.resetForm.bind(EventHandlers);
window.hideConfirmModal = EventHandlers.hideConfirmModal.bind(EventHandlers);
window.toggleNotificationDropdown = EventHandlers.toggleNotificationDropdown.bind(EventHandlers);
})();

View File

@@ -0,0 +1,225 @@
(function() {
'use strict';
const FormValidator = {
checkPasswordStrength: function(password) {
const requirements = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password)
};
let score = 0;
if (requirements.length) score += 25;
if (requirements.uppercase) score += 25;
if (requirements.lowercase) score += 25;
if (requirements.number) score += 25;
return {
score: score,
requirements: requirements,
isStrong: score >= 60
};
},
updatePasswordStrengthUI: function(password, elements) {
const result = this.checkPasswordStrength(password);
const { score, requirements } = result;
if (!elements.bar || !elements.text) {
console.warn('FormValidator: Missing strength UI elements');
return result.isStrong;
}
elements.bar.style.width = `${score}%`;
if (score === 0) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-gray-300 dark:bg-gray-500';
elements.text.textContent = 'Enter password';
elements.text.className = 'text-sm font-medium text-gray-500 dark:text-gray-400';
} else if (score < 40) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
elements.text.textContent = 'Weak';
elements.text.className = 'text-sm font-medium text-red-600 dark:text-red-400';
} else if (score < 70) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
elements.text.textContent = 'Fair';
elements.text.className = 'text-sm font-medium text-yellow-600 dark:text-yellow-400';
} else if (score < 90) {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-blue-500';
elements.text.textContent = 'Good';
elements.text.className = 'text-sm font-medium text-blue-600 dark:text-blue-400';
} else {
elements.bar.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
elements.text.textContent = 'Strong';
elements.text.className = 'text-sm font-medium text-green-600 dark:text-green-400';
}
if (elements.requirements) {
this.updateRequirement(elements.requirements.length, requirements.length);
this.updateRequirement(elements.requirements.uppercase, requirements.uppercase);
this.updateRequirement(elements.requirements.lowercase, requirements.lowercase);
this.updateRequirement(elements.requirements.number, requirements.number);
}
return result.isStrong;
},
updateRequirement: function(element, met) {
if (!element) return;
if (met) {
element.className = 'flex items-center text-green-600 dark:text-green-400';
} else {
element.className = 'flex items-center text-gray-500 dark:text-gray-400';
}
},
checkPasswordMatch: function(password1, password2, elements) {
if (!elements) {
return password1 === password2;
}
const { container, success, error } = elements;
if (password2.length === 0) {
if (container) container.classList.add('hidden');
return false;
}
if (container) container.classList.remove('hidden');
if (password1 === password2) {
if (success) success.classList.remove('hidden');
if (error) error.classList.add('hidden');
return true;
} else {
if (success) success.classList.add('hidden');
if (error) error.classList.remove('hidden');
return false;
}
},
validateEmail: function(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
},
validateRequired: function(value) {
return value && value.trim().length > 0;
},
validateMinLength: function(value, minLength) {
return value && value.length >= minLength;
},
validateMaxLength: function(value, maxLength) {
return value && value.length <= maxLength;
},
validateNumeric: function(value) {
return !isNaN(value) && !isNaN(parseFloat(value));
},
validateRange: function(value, min, max) {
const num = parseFloat(value);
return !isNaN(num) && num >= min && num <= max;
},
showError: function(element, message) {
if (!element) return;
element.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
element.classList.remove('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
let errorElement = element.parentElement.querySelector('.validation-error');
if (!errorElement) {
errorElement = document.createElement('p');
errorElement.className = 'validation-error text-red-600 dark:text-red-400 text-sm mt-1';
element.parentElement.appendChild(errorElement);
}
errorElement.textContent = message;
errorElement.classList.remove('hidden');
},
clearError: function(element) {
if (!element) return;
element.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
element.classList.add('border-gray-300', 'focus:border-blue-500', 'focus:ring-blue-500');
const errorElement = element.parentElement.querySelector('.validation-error');
if (errorElement) {
errorElement.classList.add('hidden');
}
},
validateForm: function(form, rules) {
if (!form || !rules) return false;
let isValid = true;
Object.keys(rules).forEach(fieldName => {
const field = form.querySelector(`[name="${fieldName}"]`);
if (!field) return;
const fieldRules = rules[fieldName];
let fieldValid = true;
let errorMessage = '';
if (fieldRules.required && !this.validateRequired(field.value)) {
fieldValid = false;
errorMessage = fieldRules.requiredMessage || 'This field is required';
}
if (fieldValid && fieldRules.minLength && !this.validateMinLength(field.value, fieldRules.minLength)) {
fieldValid = false;
errorMessage = fieldRules.minLengthMessage || `Minimum ${fieldRules.minLength} characters required`;
}
if (fieldValid && fieldRules.maxLength && !this.validateMaxLength(field.value, fieldRules.maxLength)) {
fieldValid = false;
errorMessage = fieldRules.maxLengthMessage || `Maximum ${fieldRules.maxLength} characters allowed`;
}
if (fieldValid && fieldRules.email && !this.validateEmail(field.value)) {
fieldValid = false;
errorMessage = fieldRules.emailMessage || 'Invalid email format';
}
if (fieldValid && fieldRules.numeric && !this.validateNumeric(field.value)) {
fieldValid = false;
errorMessage = fieldRules.numericMessage || 'Must be a number';
}
if (fieldValid && fieldRules.range && !this.validateRange(field.value, fieldRules.range.min, fieldRules.range.max)) {
fieldValid = false;
errorMessage = fieldRules.rangeMessage || `Must be between ${fieldRules.range.min} and ${fieldRules.range.max}`;
}
if (fieldValid && fieldRules.custom) {
const customResult = fieldRules.custom(field.value, form);
if (!customResult.valid) {
fieldValid = false;
errorMessage = customResult.message || 'Invalid value';
}
}
if (fieldValid) {
this.clearError(field);
} else {
this.showError(field, errorMessage);
isValid = false;
}
});
return isValid;
}
};
window.FormValidator = FormValidator;
})();

View File

@@ -23,10 +23,24 @@ const IdentityManager = (function() {
return null;
}
const cachedData = this.getCachedIdentity(address);
if (cachedData) {
log(`Cache hit for ${address}`);
return cachedData;
const cached = state.cache.get(address);
const now = Date.now();
if (cached && (now - cached.timestamp) < state.config.cacheTimeout) {
log(`Cache hit (fresh) for ${address}`);
return cached.data;
}
if (cached && (now - cached.timestamp) < state.config.cacheTimeout * 2) {
log(`Cache hit (stale) for ${address}, refreshing in background`);
const staleData = cached.data;
if (!state.pendingRequests.has(address)) {
this.refreshIdentityInBackground(address);
}
return staleData;
}
if (state.pendingRequests.has(address)) {
@@ -47,6 +61,20 @@ const IdentityManager = (function() {
}
},
refreshIdentityInBackground: function(address) {
const request = fetchWithRetry(address);
state.pendingRequests.set(address, request);
request.then(data => {
this.setCachedIdentity(address, data);
log(`Background refresh completed for ${address}`);
}).catch(error => {
log(`Background refresh failed for ${address}:`, error);
}).finally(() => {
state.pendingRequests.delete(address);
});
},
getCachedIdentity: function(address) {
const cached = state.cache.get(address);
if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
@@ -155,15 +183,23 @@ const IdentityManager = (function() {
async function fetchWithRetry(address, attempt = 1) {
try {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
let data;
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
if (window.ApiManager) {
data = await window.ApiManager.makeRequest(`/json/identities/${address}`, 'GET');
} else {
const response = await fetch(`/json/identities/${address}`, {
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
data = await response.json();
}
return await response.json();
return data;
} catch (error) {
if (attempt >= state.config.maxRetries) {
console.error(`[IdentityManager] Error:`, error.message);
@@ -171,7 +207,10 @@ const IdentityManager = (function() {
return null;
}
await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
const delay = state.config.retryDelay * attempt;
await new Promise(resolve => {
CleanupManager.setTimeout(resolve, delay);
});
return fetchWithRetry(address, attempt + 1);
}
}
@@ -188,5 +227,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
console.log('IdentityManager initialized');

View File

@@ -108,7 +108,7 @@ const NetworkManager = (function() {
log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
state.reconnectTimer = setTimeout(() => {
state.reconnectTimer = CleanupManager.setTimeout(() => {
state.reconnectTimer = null;
this.attemptReconnect();
}, delay);
@@ -167,7 +167,20 @@ const NetworkManager = (function() {
});
},
testBackendConnection: function() {
testBackendConnection: async function() {
if (window.ApiManager) {
try {
await window.ApiManager.makeRequest(config.connectionTestEndpoint, 'HEAD', {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
});
return true;
} catch (error) {
log('Backend connection test failed:', error.message);
return false;
}
}
return fetch(config.connectionTestEndpoint, {
method: 'HEAD',
headers: {
@@ -275,6 +288,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
console.log('NetworkManager initialized');

View File

@@ -1,6 +1,5 @@
const NotificationManager = (function() {
const defaultConfig = {
showNewOffers: false,
showNewBids: true,
@@ -12,7 +11,6 @@ const NotificationManager = (function() {
notificationDuration: 20000
};
function loadConfig() {
const saved = localStorage.getItem('notification_settings');
if (saved) {
@@ -25,7 +23,6 @@ const NotificationManager = (function() {
return { ...defaultConfig };
}
function saveConfig(newConfig) {
try {
localStorage.setItem('notification_settings', JSON.stringify(newConfig));
@@ -269,7 +266,6 @@ function ensureToastContainer() {
return colors[type] || colors['success'];
}
// Todo: Remove later and use global.
function getCoinDisplayName(coinId) {
const coinMap = {
1: 'PART',
@@ -292,25 +288,24 @@ function ensureToastContainer() {
return coinMap[coinId] || `Coin ${coinId}`;
}
// Todo: Remove later.
function formatCoinAmount(amount, coinId) {
const divisors = {
1: 100000000, // PART - 8 decimals
2: 100000000, // BTC - 8 decimals
3: 100000000, // LTC - 8 decimals
4: 100000000, // DCR - 8 decimals
5: 100000000, // NMC - 8 decimals
1: 100000000, // PART - 8 decimals
2: 100000000, // BTC - 8 decimals
3: 100000000, // LTC - 8 decimals
4: 100000000, // DCR - 8 decimals
5: 100000000, // NMC - 8 decimals
6: 1000000000000, // XMR - 12 decimals
7: 100000000, // PART (Blind) - 8 decimals
8: 100000000, // PART (Anon) - 8 decimals
9: 100000000000, // WOW - 11 decimals
11: 100000000, // PIVX - 8 decimals
12: 100000000, // DASH - 8 decimals
13: 100000000, // FIRO - 8 decimals
14: 100000000, // NAV - 8 decimals
15: 100000000, // LTC (MWEB) - 8 decimals
17: 100000000, // BCH - 8 decimals
18: 100000000 // DOGE - 8 decimals
7: 100000000, // PART (Blind) - 8 decimals
8: 100000000, // PART (Anon) - 8 decimals
9: 100000000000, // WOW - 11 decimals
11: 100000000, // PIVX - 8 decimals
12: 100000000, // DASH - 8 decimals
13: 100000000, // FIRO - 8 decimals
14: 100000000, // NAV - 8 decimals
15: 100000000, // LTC (MWEB) - 8 decimals
17: 100000000, // BCH - 8 decimals
18: 100000000 // DOGE - 8 decimals
};
const divisor = divisors[coinId] || 100000000;
@@ -358,7 +353,7 @@ function ensureToastContainer() {
testToasts: function() {
if (!this.createToast) return;
setTimeout(() => {
CleanupManager.setTimeout(() => {
this.createToast(
'+0.05000000 PART',
'balance_change',
@@ -366,7 +361,7 @@ function ensureToastContainer() {
);
}, 500);
setTimeout(() => {
CleanupManager.setTimeout(() => {
this.createToast(
'+0.00123456 XMR',
'balance_change',
@@ -374,7 +369,7 @@ function ensureToastContainer() {
);
}, 1000);
setTimeout(() => {
CleanupManager.setTimeout(() => {
this.createToast(
'-29.86277595 PART',
'balance_change',
@@ -382,7 +377,7 @@ function ensureToastContainer() {
);
}, 1500);
setTimeout(() => {
CleanupManager.setTimeout(() => {
this.createToast(
'-0.05000000 PART (Anon)',
'balance_change',
@@ -390,7 +385,7 @@ function ensureToastContainer() {
);
}, 2000);
setTimeout(() => {
CleanupManager.setTimeout(() => {
this.createToast(
'+1.23456789 PART (Anon)',
'balance_change',
@@ -398,33 +393,37 @@ function ensureToastContainer() {
);
}, 2500);
setTimeout(() => {
CleanupManager.setTimeout(() => {
const btcIcon = getCoinIcon('BTC');
const xmrIcon = getCoinIcon('XMR');
this.createToast(
'<img src="/static/images/coins/bitcoin.svg" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display=\'none\'">1.00000000 BTC → <img src="/static/images/coins/monero.svg" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display=\'none\'">15.50000000 XMR',
'New Network Offer',
'new_offer',
{
offerId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
subtitle: 'New offer • Rate: 1 BTC = 15.50000000 XMR',
subtitle: `<img src="/static/images/coins/${btcIcon}" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display='none'">1.00000000 BTC → <img src="/static/images/coins/${xmrIcon}" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display='none'">15.50000000 XMR<br>Rate: 1 BTC = 15.50000000 XMR`,
coinFrom: 2,
coinTo: 6
}
);
}, 3000);
setTimeout(() => {
CleanupManager.setTimeout(() => {
const btcIcon = getCoinIcon('BTC');
const xmrIcon = getCoinIcon('XMR');
this.createToast(
'<img src="/static/images/coins/bitcoin.svg" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display=\'none\'">0.50000000 BTC → <img src="/static/images/coins/monero.svg" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display=\'none\'">7.75000000 XMR',
'New Bid Received',
'new_bid',
{
bidId: '000000006873f4ef17d4f220730400f4fdd57157492289c5d414ea66',
subtitle: 'New bid • Rate: 1 BTC = 15.50000000 XMR',
subtitle: `<img src="/static/images/coins/${btcIcon}" class="w-4 h-4 inline mr-1" alt="BTC" onerror="this.style.display='none'">0.50000000 BTC → <img src="/static/images/coins/${xmrIcon}" class="w-4 h-4 inline mr-1" alt="XMR" onerror="this.style.display='none'">7.75000000 XMR<br>Rate: 1 BTC = 15.50000000 XMR`,
coinFrom: 2,
coinTo: 6
}
);
}, 3500);
setTimeout(() => {
CleanupManager.setTimeout(() => {
this.createToast(
'Swap completed successfully',
'swap_completed',
@@ -435,25 +434,68 @@ function ensureToastContainer() {
);
}, 4000);
setTimeout(() => {
this.createToast(
'Update Available: v0.15.0',
'update_available',
{
subtitle: 'Current: v0.14.6 • Click to view release',
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
CleanupManager.setTimeout(async () => {
try {
const response = await fetch('/json/checkupdates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (data.error) {
console.warn('Test notification - API returned error, using fallback:', data.error);
this.createToast(
'Update Available: v0.15.0',
'update_available',
{
subtitle: 'Current: v0.14.6 • Click to view release',
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
}
);
return;
}
);
const currentVer = (data.current_version && String(data.current_version) !== 'null' && String(data.current_version) !== 'None')
? String(data.current_version)
: '0.14.6';
const latestVer = (data.latest_version && String(data.latest_version) !== 'null' && String(data.latest_version) !== 'None')
? String(data.latest_version)
: currentVer;
this.createToast(
`Update Available: v${latestVer}`,
'update_available',
{
subtitle: `Current: v${currentVer} • Click to view release`,
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
releaseNotes: `New version v${latestVer} is available. Click to view details on GitHub.`
}
);
} catch (error) {
console.error('Test notification - API error:', error);
this.createToast(
'Update Available: v0.15.0',
'update_available',
{
subtitle: 'Current: v0.14.6 • Click to view release',
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
}
);
}
}, 4500);
},
initializeBalanceTracking: function() {
initializeBalanceTracking: async function() {
this.checkAndResetStaleBalanceTracking();
fetch('/json/walletbalances')
.then(response => response.json())
const fetchBalances = window.ApiManager
? window.ApiManager.makeRequest('/json/walletbalances', 'GET')
: fetch('/json/walletbalances').then(response => response.json());
fetchBalances
.then(balanceData => {
if (Array.isArray(balanceData)) {
balanceData.forEach(coin => {
@@ -533,7 +575,6 @@ function ensureToastContainer() {
coinIconHtml = `<img src="/static/images/coins/${coinIcon}" class="w-5 h-5 mr-2" alt="${options.coinSymbol}" onerror="this.style.display='none'">`;
}
let clickAction = '';
let cursorStyle = 'cursor-default';
@@ -585,10 +626,10 @@ function ensureToastContainer() {
messages.appendChild(message);
if (!isPersistent) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
if (message.parentNode) {
message.classList.add('toast-slide-out');
setTimeout(() => {
CleanupManager.setTimeout(() => {
if (message.parentNode) {
message.parentNode.removeChild(message);
}
@@ -613,12 +654,12 @@ function ensureToastContainer() {
const amountTo = formatCoinAmount(data.amount_to, data.coin_to);
const coinFromIcon = getCoinIcon(coinFromName);
const coinToIcon = getCoinIcon(coinToName);
toastTitle = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${amountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${amountTo} ${coinToName}`;
toastOptions.subtitle = `New offer • Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
toastTitle = `New Network Offer`;
toastOptions.subtitle = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${amountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${amountTo} ${coinToName}<br>Rate: 1 ${coinFromName} = ${(data.amount_to / data.amount_from).toFixed(8)} ${coinToName}`;
toastOptions.coinFrom = data.coin_from;
toastOptions.coinTo = data.coin_to;
} else {
toastTitle = `New network offer`;
toastTitle = `New Network Offer`;
toastOptions.subtitle = 'Click to view offer';
}
toastType = 'new_offer';
@@ -633,12 +674,12 @@ function ensureToastContainer() {
const bidAmountTo = formatCoinAmount(data.bid_amount_to, data.coin_to);
const coinFromIcon = getCoinIcon(coinFromName);
const coinToIcon = getCoinIcon(coinToName);
toastTitle = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${bidAmountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${bidAmountTo} ${coinToName}`;
toastOptions.subtitle = `New bid • Rate: 1 ${coinFromName} = ${(data.bid_amount_to / data.bid_amount).toFixed(8)} ${coinToName}`;
toastTitle = `New Bid Received`;
toastOptions.subtitle = `<img src="/static/images/coins/${coinFromIcon}" class="w-4 h-4 inline mr-1" alt="${coinFromName}" onerror="this.style.display='none'">${bidAmountFrom} ${coinFromName} → <img src="/static/images/coins/${coinToIcon}" class="w-4 h-4 inline mr-1" alt="${coinToName}" onerror="this.style.display='none'">${bidAmountTo} ${coinToName}<br>Rate: 1 ${coinFromName} = ${(data.bid_amount_to / data.bid_amount).toFixed(8)} ${coinToName}`;
toastOptions.coinFrom = data.coin_from;
toastOptions.coinTo = data.coin_to;
} else {
toastTitle = `New bid received`;
toastTitle = `New Bid Received`;
toastOptions.subtitle = 'Click to view bid';
}
toastOptions.bidId = data.bid_id;
@@ -696,14 +737,17 @@ function ensureToastContainer() {
this.balanceTimeouts = {};
}
this.balanceTimeouts[balanceKey] = setTimeout(() => {
this.balanceTimeouts[balanceKey] = CleanupManager.setTimeout(() => {
this.fetchAndShowBalanceChange(data.coin);
}, 2000);
},
fetchAndShowBalanceChange: function(coinSymbol) {
fetch('/json/walletbalances')
.then(response => response.json())
const fetchBalances = window.ApiManager
? window.ApiManager.makeRequest('/json/walletbalances', 'GET')
: fetch('/json/walletbalances').then(response => response.json());
fetchBalances
.then(balanceData => {
if (Array.isArray(balanceData)) {
@@ -748,13 +792,10 @@ function ensureToastContainer() {
const pendingIncrease = currentPending - prevPending;
const pendingDecrease = prevPending - currentPending;
const totalChange = Math.abs(balanceIncrease) + Math.abs(pendingIncrease);
const maxReasonableChange = Math.max(currentBalance, prevBalance) * 0.5;
if (totalChange > maxReasonableChange && totalChange > 1.0) {
console.log(`Detected potentially stale balance data for ${coinData.name}, resetting tracking`);
localStorage.setItem(storageKey, currentBalance.toString());
localStorage.setItem(pendingStorageKey, currentPending.toString());
return;
@@ -782,7 +823,6 @@ function ensureToastContainer() {
const isPendingToConfirmed = pendingDecrease > 0.00000001 && balanceIncrease > 0.00000001;
const displaySymbol = originalCoinSymbol;
let variantInfo = '';
@@ -871,8 +911,6 @@ function ensureToastContainer() {
}
},
updateConfig: function(newConfig) {
Object.assign(config, newConfig);
return this;

View File

@@ -42,7 +42,7 @@ const PriceManager = (function() {
});
}
setTimeout(() => this.getPrices(), 1500);
CleanupManager.setTimeout(() => this.getPrices(), 1500);
isInitialized = true;
console.log('PriceManager initialized');
return this;
@@ -60,7 +60,6 @@ const PriceManager = (function() {
return fetchPromise;
}
lastFetchTime = Date.now();
fetchPromise = this.fetchPrices()
.then(prices => {
@@ -90,8 +89,6 @@ const PriceManager = (function() {
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
//console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', '));
if (!coinSymbols.length) {
throw new Error('No valid coins configured');
}
@@ -133,15 +130,15 @@ const PriceManager = (function() {
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
if (coin) {
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
} else if (window.CoinUtils) {
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
normalizedCoinId = coinId.toLowerCase();
}
} else if (window.CoinUtils) {
normalizedCoinId = window.CoinUtils.normalizeCoinName(coinId);
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
}
if (coinId.toLowerCase() === 'zcoin') {
normalizedCoinId = 'firo';
normalizedCoinId = coinId.toLowerCase();
}
processedData[normalizedCoinId] = {
@@ -230,5 +227,3 @@ document.addEventListener('DOMContentLoaded', function() {
window.priceManagerInitialized = true;
}
});

View File

@@ -0,0 +1,79 @@
(function() {
'use strict';
const QRCodeManager = {
defaultOptions: {
width: 200,
height: 200,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.L
},
initialize: function() {
const qrElements = document.querySelectorAll('[data-qrcode]');
qrElements.forEach(element => {
this.generateQRCode(element);
});
},
generateQRCode: function(element) {
const address = element.getAttribute('data-address');
const width = parseInt(element.getAttribute('data-width')) || this.defaultOptions.width;
const height = parseInt(element.getAttribute('data-height')) || this.defaultOptions.height;
if (!address) {
console.error('QRCodeManager: No address provided for element', element);
return;
}
element.innerHTML = '';
try {
new QRCode(element, {
text: address,
width: width,
height: height,
colorDark: this.defaultOptions.colorDark,
colorLight: this.defaultOptions.colorLight,
correctLevel: this.defaultOptions.correctLevel
});
} catch (error) {
console.error('QRCodeManager: Failed to generate QR code', error);
}
},
generateById: function(elementId, address, options = {}) {
const element = window.DOMCache
? window.DOMCache.get(elementId)
: document.getElementById(elementId);
if (!element) {
console.error('QRCodeManager: Element not found:', elementId);
return;
}
element.setAttribute('data-address', address);
if (options.width) element.setAttribute('data-width', options.width);
if (options.height) element.setAttribute('data-height', options.height);
this.generateQRCode(element);
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
QRCodeManager.initialize();
});
} else {
QRCodeManager.initialize();
}
window.QRCodeManager = QRCodeManager;
})();

View File

@@ -166,7 +166,7 @@ const SummaryManager = (function() {
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
setTimeout(() => {
CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
@@ -205,8 +205,16 @@ const SummaryManager = (function() {
}
function fetchSummaryDataWithTimeout() {
if (window.ApiManager) {
return window.ApiManager.makeRequest(config.summaryEndpoint, 'GET', {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
});
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
const timeoutId = CleanupManager.setTimeout(() => controller.abort(), config.requestTimeout);
return fetch(config.summaryEndpoint, {
signal: controller.signal,
@@ -217,7 +225,11 @@ const SummaryManager = (function() {
}
})
.then(response => {
clearTimeout(timeoutId);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
@@ -226,7 +238,11 @@ const SummaryManager = (function() {
return response.json();
})
.catch(error => {
clearTimeout(timeoutId);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(timeoutId);
} else {
clearTimeout(timeoutId);
}
throw error;
});
}
@@ -275,7 +291,7 @@ const SummaryManager = (function() {
};
webSocket.onclose = () => {
setTimeout(setupWebSocket, 5000);
CleanupManager.setTimeout(setupWebSocket, 5000);
};
}
@@ -303,7 +319,7 @@ const SummaryManager = (function() {
.then(() => {})
.catch(() => {});
refreshTimer = setInterval(() => {
refreshTimer = CleanupManager.setInterval(() => {
publicAPI.fetchSummaryData()
.then(() => {})
.catch(() => {});
@@ -386,7 +402,7 @@ const SummaryManager = (function() {
}
return new Promise(resolve => {
setTimeout(() => {
CleanupManager.setTimeout(() => {
resolve(this.fetchSummaryData());
}, config.retryDelay);
});
@@ -446,5 +462,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
console.log('SummaryManager initialized');

View File

@@ -14,6 +14,9 @@ const TooltipManager = (function() {
this.debug = false;
this.tooltipData = new WeakMap();
this.resources = {};
this.creationQueue = [];
this.batchSize = 5;
this.isProcessingQueue = false;
if (window.CleanupManager) {
CleanupManager.registerResource(
@@ -48,40 +51,69 @@ const TooltipManager = (function() {
this.performPeriodicCleanup(true);
}
const createTooltip = () => {
if (!document.body.contains(element)) return;
this.creationQueue.push({ element, content, options });
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.createTooltipInstance(element, content, options);
} else {
let retryCount = 0;
const maxRetries = 3;
if (!this.isProcessingQueue) {
this.processCreationQueue();
}
const retryCreate = () => {
const newRect = element.getBoundingClientRect();
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
if (newRect.width > 0 && newRect.height > 0) {
this.createTooltipInstance(element, content, options);
}
} else {
retryCount++;
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.requestAnimationFrame(createTooltip);
return null;
}
processCreationQueue() {
if (this.creationQueue.length === 0) {
this.isProcessingQueue = false;
return;
}
this.isProcessingQueue = true;
const batch = this.creationQueue.splice(0, this.batchSize);
CleanupManager.requestAnimationFrame(() => {
batch.forEach(({ element, content, options }) => {
this.createTooltipSync(element, content, options);
});
if (this.creationQueue.length > 0) {
CleanupManager.setTimeout(() => {
this.processCreationQueue();
}, 0);
} else {
this.isProcessingQueue = false;
}
});
}
createTooltipSync(element, content, options) {
if (!document.body.contains(element)) return;
const rect = element.getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) {
this.createTooltipInstance(element, content, options);
} else {
let retryCount = 0;
const maxRetries = 3;
const retryCreate = () => {
const newRect = element.getBoundingClientRect();
if ((newRect.width > 0 && newRect.height > 0) || retryCount >= maxRetries) {
if (newRect.width > 0 && newRect.height > 0) {
this.createTooltipInstance(element, content, options);
}
} else {
retryCount++;
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
};
CleanupManager.setTimeout(() => {
CleanupManager.requestAnimationFrame(retryCreate);
}, 100);
}
}
createTooltipInstance(element, content, options = {}) {
if (!element || !document.body.contains(element)) {
return null;
@@ -191,6 +223,9 @@ const TooltipManager = (function() {
return null;
} catch (error) {
if (window.ErrorHandler) {
return window.ErrorHandler.handleError(error, 'TooltipManager.createTooltipInstance', null);
}
console.error('Error creating tooltip:', error);
return null;
}
@@ -199,7 +234,7 @@ const TooltipManager = (function() {
destroy(element) {
if (!element) return;
try {
const destroyFn = () => {
const tooltipId = element.getAttribute('data-tooltip-trigger-id');
if (!tooltipId) return;
@@ -224,8 +259,16 @@ const TooltipManager = (function() {
this.tooltipData.delete(element);
tooltipInstanceMap.delete(element);
} catch (error) {
console.error('Error destroying tooltip:', error);
};
if (window.ErrorHandler) {
window.ErrorHandler.safeExecute(destroyFn, 'TooltipManager.destroy', null);
} else {
try {
destroyFn();
} catch (error) {
console.error('Error destroying tooltip:', error);
}
}
}
@@ -738,10 +781,40 @@ const TooltipManager = (function() {
}
}
initializeLazyTooltips(selector = '[data-tooltip-target]') {
const initializedTooltips = new Set();
const initializeTooltip = (element) => {
const targetId = element.getAttribute('data-tooltip-target');
if (!targetId || initializedTooltips.has(targetId)) return;
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
this.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
initializedTooltips.add(targetId);
}
};
document.addEventListener('mouseover', (e) => {
const target = e.target.closest(selector);
if (target) {
initializeTooltip(target);
}
}, { passive: true, capture: true });
this.log('Lazy tooltip initialization enabled');
}
dispose() {
this.log('Disposing TooltipManager');
try {
this.creationQueue = [];
this.isProcessingQueue = false;
this.cleanup();
Object.values(this.resources).forEach(resourceId => {
@@ -830,6 +903,11 @@ const TooltipManager = (function() {
return manager.initializeTooltips(...args);
},
initializeLazyTooltips: function(...args) {
const manager = this.getInstance();
return manager.initializeLazyTooltips(...args);
},
setDebugMode: function(enabled) {
const manager = this.getInstance();
return manager.setDebugMode(enabled);

View File

@@ -0,0 +1,196 @@
(function() {
'use strict';
const WalletAmountManager = {
coinConfigs: {
1: {
types: ['plain', 'blind', 'anon'],
hasSubfee: true,
hasSweepAll: false
},
3: {
types: ['plain', 'mweb'],
hasSubfee: true,
hasSweepAll: false
},
6: {
types: ['default'],
hasSubfee: false,
hasSweepAll: true
},
9: {
types: ['default'],
hasSubfee: false,
hasSweepAll: true
}
},
safeParseFloat: function(value) {
const numValue = Number(value);
if (!isNaN(numValue) && numValue > 0) {
return numValue;
}
console.warn('WalletAmountManager: Invalid balance value:', value);
return 0;
},
getBalance: function(coinId, balances, selectedType) {
const cid = parseInt(coinId);
if (cid === 1) {
switch(selectedType) {
case 'plain':
return this.safeParseFloat(balances.main || balances.balance);
case 'blind':
return this.safeParseFloat(balances.blind);
case 'anon':
return this.safeParseFloat(balances.anon);
default:
return this.safeParseFloat(balances.main || balances.balance);
}
}
if (cid === 3) {
switch(selectedType) {
case 'plain':
return this.safeParseFloat(balances.main || balances.balance);
case 'mweb':
return this.safeParseFloat(balances.mweb);
default:
return this.safeParseFloat(balances.main || balances.balance);
}
}
return this.safeParseFloat(balances.main || balances.balance);
},
calculateAmount: function(balance, percent, coinId) {
const cid = parseInt(coinId);
if (percent === 1) {
return balance;
}
if (cid === 1) {
return Math.max(0, Math.floor(balance * percent * 100000000) / 100000000);
}
const calculatedAmount = balance * percent;
if (calculatedAmount < 0.00000001) {
console.warn('WalletAmountManager: Calculated amount too small, setting to zero');
return 0;
}
return calculatedAmount;
},
setAmount: function(percent, balances, coinId) {
const amountInput = window.DOMCache
? window.DOMCache.get('amount')
: document.getElementById('amount');
const typeSelect = window.DOMCache
? window.DOMCache.get('withdraw_type')
: document.getElementById('withdraw_type');
if (!amountInput) {
console.error('WalletAmountManager: Amount input not found');
return;
}
const cid = parseInt(coinId);
const selectedType = typeSelect ? typeSelect.value : 'plain';
const balance = this.getBalance(cid, balances, selectedType);
const calculatedAmount = this.calculateAmount(balance, percent, cid);
const specialCids = [6, 9];
if (specialCids.includes(cid) && percent === 1) {
amountInput.setAttribute('data-hidden', 'true');
amountInput.placeholder = 'Sweep All';
amountInput.value = '';
amountInput.disabled = true;
const sweepAllCheckbox = window.DOMCache
? window.DOMCache.get('sweepall')
: document.getElementById('sweepall');
if (sweepAllCheckbox) {
sweepAllCheckbox.checked = true;
}
} else {
amountInput.value = calculatedAmount.toFixed(8);
amountInput.setAttribute('data-hidden', 'false');
amountInput.placeholder = '';
amountInput.disabled = false;
const sweepAllCheckbox = window.DOMCache
? window.DOMCache.get('sweepall')
: document.getElementById('sweepall');
if (sweepAllCheckbox) {
sweepAllCheckbox.checked = false;
}
}
const subfeeCheckbox = document.querySelector(`[name="subfee_${cid}"]`);
if (subfeeCheckbox) {
subfeeCheckbox.checked = (percent === 1);
}
},
initialize: function() {
const amountButtons = document.querySelectorAll('[data-set-amount]');
amountButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
const percent = parseFloat(button.getAttribute('data-set-amount'));
const balancesJson = button.getAttribute('data-balances');
const coinId = button.getAttribute('data-coin-id');
if (!balancesJson || !coinId) {
console.error('WalletAmountManager: Missing data attributes on button', button);
return;
}
try {
const balances = JSON.parse(balancesJson);
this.setAmount(percent, balances, coinId);
} catch (error) {
console.error('WalletAmountManager: Failed to parse balances', error);
}
});
});
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
WalletAmountManager.initialize();
});
} else {
WalletAmountManager.initialize();
}
window.WalletAmountManager = WalletAmountManager;
window.setAmount = function(percent, balance, coinId, balance2, balance3) {
const balances = {
main: balance || balance,
balance: balance,
blind: balance2,
anon: balance3,
mweb: balance2
};
WalletAmountManager.setAmount(percent, balances, coinId);
};
})();

View File

@@ -95,22 +95,32 @@ const WalletManager = (function() {
const fetchCoinsString = coinsToFetch.join(',');
const mainResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
let mainData;
if (window.ApiManager) {
mainData = await window.ApiManager.makeRequest("/json/coinprices", "POST", {}, {
coins: fetchCoinsString,
source: currentSource,
ttl: config.defaultTTL
})
});
});
} else {
const mainResponse = await fetch("/json/coinprices", {
method: "POST",
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
coins: fetchCoinsString,
source: currentSource,
ttl: config.defaultTTL
})
});
if (!mainResponse.ok) {
throw new Error(`HTTP error: ${mainResponse.status}`);
if (!mainResponse.ok) {
throw new Error(`HTTP error: ${mainResponse.status}`);
}
mainData = await mainResponse.json();
}
const mainData = await mainResponse.json();
if (mainData && mainData.rates) {
document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
@@ -154,7 +164,7 @@ const WalletManager = (function() {
if (attempt < config.maxRetries - 1) {
const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
}
}
}
@@ -428,7 +438,7 @@ const WalletManager = (function() {
clearTimeout(state.toggleDebounceTimer);
}
state.toggleDebounceTimer = window.setTimeout(async () => {
state.toggleDebounceTimer = CleanupManager.setTimeout(async () => {
state.toggleInProgress = false;
if (newVisibility) {
await updatePrices(true);
@@ -539,7 +549,6 @@ const WalletManager = (function() {
}
}
// Public API
const publicAPI = {
initialize: async function(options) {
if (state.initialized) {
@@ -579,7 +588,7 @@ const WalletManager = (function() {
clearInterval(state.priceUpdateInterval);
}
state.priceUpdateInterval = setInterval(() => {
state.priceUpdateInterval = CleanupManager.setInterval(() => {
if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
updatePrices(false);
}
@@ -661,5 +670,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
console.log('WalletManager initialized');

View File

@@ -32,26 +32,24 @@ const WebSocketManager = (function() {
}
function determineWebSocketPort() {
let wsPort;
if (window.ConfigManager && window.ConfigManager.wsPort) {
return window.ConfigManager.wsPort.toString();
}
if (window.config && window.config.wsPort) {
wsPort = window.config.wsPort;
return wsPort;
return window.config.wsPort.toString();
}
if (window.ws_port) {
wsPort = window.ws_port.toString();
return wsPort;
return window.ws_port.toString();
}
if (typeof getWebSocketConfig === 'function') {
const wsConfig = getWebSocketConfig();
wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
return wsPort;
return (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
}
wsPort = '11700';
return wsPort;
return '11700';
}
const publicAPI = {
@@ -77,7 +75,11 @@ const WebSocketManager = (function() {
}
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
@@ -96,13 +98,17 @@ const WebSocketManager = (function() {
ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
setupEventHandlers();
state.connectTimeout = setTimeout(() => {
const timeoutFn = () => {
if (state.isConnecting) {
log('Connection timeout, cleaning up');
cleanup();
handleReconnect();
}
}, 5000);
};
state.connectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(timeoutFn, 5000)
: setTimeout(timeoutFn, 5000);
return true;
} catch (error) {
@@ -159,18 +165,25 @@ const WebSocketManager = (function() {
cleanup: function() {
log('Cleaning up WebSocket resources');
clearTimeout(state.connectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.connectTimeout);
} else {
clearTimeout(state.connectTimeout);
}
stopHealthCheck();
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
state.isConnecting = false;
state.messageHandlers = {};
if (ws) {
ws.onopen = null;
ws.onmessage = null;
@@ -228,7 +241,11 @@ const WebSocketManager = (function() {
ws.onopen = () => {
state.isConnecting = false;
config.reconnectAttempts = 0;
clearTimeout(state.connectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.connectTimeout);
} else {
clearTimeout(state.connectTimeout);
}
state.lastHealthCheck = Date.now();
window.ws = ws;
@@ -311,24 +328,37 @@ const WebSocketManager = (function() {
state.isPageHidden = false;
state.isIntentionallyClosed = false;
setTimeout(() => {
const resumeFn = () => {
if (!publicAPI.isConnected()) {
publicAPI.connect();
}
startHealthCheck();
}, 0);
};
if (window.CleanupManager) {
window.CleanupManager.setTimeout(resumeFn, 0);
} else {
setTimeout(resumeFn, 0);
}
}
function startHealthCheck() {
stopHealthCheck();
state.healthCheckInterval = setInterval(() => {
const healthCheckFn = () => {
performHealthCheck();
}, 30000);
};
state.healthCheckInterval = window.CleanupManager
? window.CleanupManager.setInterval(healthCheckFn, 30000)
: setInterval(healthCheckFn, 30000);
}
function stopHealthCheck() {
if (state.healthCheckInterval) {
clearInterval(state.healthCheckInterval);
if (window.CleanupManager) {
window.CleanupManager.clearInterval(state.healthCheckInterval);
} else {
clearInterval(state.healthCheckInterval);
}
state.healthCheckInterval = null;
}
}
@@ -356,7 +386,11 @@ const WebSocketManager = (function() {
function handleReconnect() {
if (state.reconnectTimeout) {
clearTimeout(state.reconnectTimeout);
if (window.CleanupManager) {
window.CleanupManager.clearTimeout(state.reconnectTimeout);
} else {
clearTimeout(state.reconnectTimeout);
}
state.reconnectTimeout = null;
}
@@ -369,23 +403,31 @@ const WebSocketManager = (function() {
log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
state.reconnectTimeout = setTimeout(() => {
const reconnectFn = () => {
state.reconnectTimeout = null;
if (!state.isIntentionallyClosed) {
publicAPI.connect();
}
}, delay);
};
state.reconnectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(reconnectFn, delay)
: setTimeout(reconnectFn, delay);
} else {
log('Max reconnect attempts reached');
if (typeof updateConnectionStatus === 'function') {
updateConnectionStatus('error');
}
state.reconnectTimeout = setTimeout(() => {
const resetFn = () => {
state.reconnectTimeout = null;
config.reconnectAttempts = 0;
publicAPI.connect();
}, 60000);
};
state.reconnectTimeout = window.CleanupManager
? window.CleanupManager.setTimeout(resetFn, 60000)
: setTimeout(resetFn, 60000);
}
}
@@ -442,5 +484,4 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
console.log('WebSocketManager initialized');

View File

@@ -0,0 +1,294 @@
(function() {
'use strict';
const AMMConfigTabs = {
init: function() {
const jsonTab = document.getElementById('json-tab');
const settingsTab = document.getElementById('settings-tab');
const overviewTab = document.getElementById('overview-tab');
const jsonContent = document.getElementById('json-content');
const settingsContent = document.getElementById('settings-content');
const overviewContent = document.getElementById('overview-content');
if (!jsonTab || !settingsTab || !overviewTab || !jsonContent || !settingsContent || !overviewContent) {
return;
}
const activeConfigTab = localStorage.getItem('amm_active_config_tab');
const switchConfigTab = (tabId) => {
jsonContent.classList.add('hidden');
jsonContent.classList.remove('block');
settingsContent.classList.add('hidden');
settingsContent.classList.remove('block');
overviewContent.classList.add('hidden');
overviewContent.classList.remove('block');
jsonTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
settingsTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
overviewTab.classList.remove('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
if (tabId === 'json-tab') {
jsonContent.classList.remove('hidden');
jsonContent.classList.add('block');
jsonTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
localStorage.setItem('amm_active_config_tab', 'json-tab');
} else if (tabId === 'settings-tab') {
settingsContent.classList.remove('hidden');
settingsContent.classList.add('block');
settingsTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
localStorage.setItem('amm_active_config_tab', 'settings-tab');
this.loadSettingsFromJson();
} else if (tabId === 'overview-tab') {
overviewContent.classList.remove('hidden');
overviewContent.classList.add('block');
overviewTab.classList.add('bg-gray-100', 'text-gray-900', 'dark:bg-gray-600', 'dark:text-white');
localStorage.setItem('amm_active_config_tab', 'overview-tab');
}
};
jsonTab.addEventListener('click', () => switchConfigTab('json-tab'));
settingsTab.addEventListener('click', () => switchConfigTab('settings-tab'));
overviewTab.addEventListener('click', () => switchConfigTab('overview-tab'));
const returnToTab = localStorage.getItem('amm_return_to_tab');
if (returnToTab && (returnToTab === 'json-tab' || returnToTab === 'settings-tab' || returnToTab === 'overview-tab')) {
localStorage.removeItem('amm_return_to_tab');
switchConfigTab(returnToTab);
} else if (activeConfigTab === 'settings-tab') {
switchConfigTab('settings-tab');
} else if (activeConfigTab === 'overview-tab') {
switchConfigTab('overview-tab');
} else {
switchConfigTab('json-tab');
}
const globalSettingsForm = document.getElementById('global-settings-form');
if (globalSettingsForm) {
globalSettingsForm.addEventListener('submit', () => {
this.updateJsonFromSettings();
});
}
this.setupCollapsibles();
this.setupConfigForm();
this.setupCreateDefaultButton();
this.handleCreateDefaultRefresh();
},
loadSettingsFromJson: function() {
const configTextarea = document.querySelector('textarea[name="config_content"]');
if (!configTextarea) return;
try {
const configText = configTextarea.value.trim();
if (!configText) return;
const config = JSON.parse(configText);
document.getElementById('min_seconds_between_offers').value = config.min_seconds_between_offers || 15;
document.getElementById('max_seconds_between_offers').value = config.max_seconds_between_offers || 60;
document.getElementById('main_loop_delay').value = config.main_loop_delay || 60;
const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
const pruneStateDelayEl = document.getElementById('prune_state_delay');
const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
if (minSecondsBetweenBidsEl) minSecondsBetweenBidsEl.value = config.min_seconds_between_bids || 15;
if (maxSecondsBetweenBidsEl) maxSecondsBetweenBidsEl.value = config.max_seconds_between_bids || 60;
if (pruneStateDelayEl) pruneStateDelayEl.value = config.prune_state_delay || 120;
if (pruneStateAfterSecondsEl) pruneStateAfterSecondsEl.value = config.prune_state_after_seconds || 604800;
document.getElementById('auth').value = config.auth || '';
} catch (error) {
console.error('Error loading settings from JSON:', error);
}
},
updateJsonFromSettings: function() {
const configTextarea = document.querySelector('textarea[name="config_content"]');
if (!configTextarea) return;
try {
const configText = configTextarea.value.trim();
let config = {};
if (configText) {
config = JSON.parse(configText);
}
config.min_seconds_between_offers = parseInt(document.getElementById('min_seconds_between_offers').value) || 15;
config.max_seconds_between_offers = parseInt(document.getElementById('max_seconds_between_offers').value) || 60;
config.main_loop_delay = parseInt(document.getElementById('main_loop_delay').value) || 60;
const minSecondsBetweenBidsEl = document.getElementById('min_seconds_between_bids');
const maxSecondsBetweenBidsEl = document.getElementById('max_seconds_between_bids');
const pruneStateDelayEl = document.getElementById('prune_state_delay');
const pruneStateAfterSecondsEl = document.getElementById('prune_state_after_seconds');
if (minSecondsBetweenBidsEl) config.min_seconds_between_bids = parseInt(minSecondsBetweenBidsEl.value) || 15;
if (maxSecondsBetweenBidsEl) config.max_seconds_between_bids = parseInt(maxSecondsBetweenBidsEl.value) || 60;
if (pruneStateDelayEl) config.prune_state_delay = parseInt(pruneStateDelayEl.value) || 120;
if (pruneStateAfterSecondsEl) config.prune_state_after_seconds = parseInt(pruneStateAfterSecondsEl.value) || 604800;
config.auth = document.getElementById('auth').value || '';
configTextarea.value = JSON.stringify(config, null, 2);
localStorage.setItem('amm_return_to_tab', 'settings-tab');
} catch (error) {
console.error('Error updating JSON from settings:', error);
alert('Error updating configuration: ' + error.message);
}
},
setupCollapsibles: function() {
const collapsibleHeaders = document.querySelectorAll('.collapsible-header');
if (collapsibleHeaders.length === 0) return;
let collapsibleStates = {};
try {
const storedStates = localStorage.getItem('amm_collapsible_states');
if (storedStates) {
collapsibleStates = JSON.parse(storedStates);
}
} catch (e) {
console.error('Error parsing stored collapsible states:', e);
collapsibleStates = {};
}
const toggleCollapsible = (header) => {
const targetId = header.getAttribute('data-target');
const content = document.getElementById(targetId);
const arrow = header.querySelector('svg');
if (content) {
if (content.classList.contains('hidden')) {
content.classList.remove('hidden');
arrow.classList.add('rotate-180');
collapsibleStates[targetId] = 'open';
} else {
content.classList.add('hidden');
arrow.classList.remove('rotate-180');
collapsibleStates[targetId] = 'closed';
}
localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
}
};
collapsibleHeaders.forEach(header => {
const targetId = header.getAttribute('data-target');
const content = document.getElementById(targetId);
const arrow = header.querySelector('svg');
if (content) {
if (collapsibleStates[targetId] === 'open') {
content.classList.remove('hidden');
arrow.classList.add('rotate-180');
} else {
content.classList.add('hidden');
arrow.classList.remove('rotate-180');
collapsibleStates[targetId] = 'closed';
}
}
header.addEventListener('click', () => toggleCollapsible(header));
});
localStorage.setItem('amm_collapsible_states', JSON.stringify(collapsibleStates));
},
setupConfigForm: function() {
const configForm = document.querySelector('form[method="post"]');
const saveConfigBtn = document.getElementById('save_config_btn');
if (configForm && saveConfigBtn) {
configForm.addEventListener('submit', (e) => {
if (e.submitter && e.submitter.name === 'save_config') {
localStorage.setItem('amm_update_tables', 'true');
}
});
if (localStorage.getItem('amm_update_tables') === 'true') {
localStorage.removeItem('amm_update_tables');
CleanupManager.setTimeout(() => {
if (window.ammTablesManager && window.ammTablesManager.updateTables) {
window.ammTablesManager.updateTables();
}
}, 500);
}
}
},
setupCreateDefaultButton: function() {
const createDefaultBtn = document.getElementById('create_default_btn');
const configForm = document.querySelector('form[method="post"]');
if (createDefaultBtn && configForm) {
createDefaultBtn.addEventListener('click', (e) => {
e.preventDefault();
const title = 'Create Default Configuration';
const message = 'This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?';
if (window.showConfirmModal) {
window.showConfirmModal(title, message, () => {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'create_default';
hiddenInput.value = 'true';
configForm.appendChild(hiddenInput);
localStorage.setItem('amm_create_default_refresh', 'true');
configForm.submit();
});
} else {
if (confirm('This will overwrite your current configuration with a default template.\n\nAre you sure you want to continue?')) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'create_default';
hiddenInput.value = 'true';
configForm.appendChild(hiddenInput);
localStorage.setItem('amm_create_default_refresh', 'true');
configForm.submit();
}
}
});
}
},
handleCreateDefaultRefresh: function() {
if (localStorage.getItem('amm_create_default_refresh') === 'true') {
localStorage.removeItem('amm_create_default_refresh');
CleanupManager.setTimeout(() => {
window.location.href = window.location.pathname + window.location.search;
}, 500);
}
},
cleanup: function() {
}
};
document.addEventListener('DOMContentLoaded', function() {
AMMConfigTabs.init();
if (window.CleanupManager) {
CleanupManager.registerResource('ammConfigTabs', AMMConfigTabs, (tabs) => {
if (tabs.cleanup) tabs.cleanup();
});
}
});
window.AMMConfigTabs = AMMConfigTabs;
})();

View File

@@ -16,13 +16,7 @@ const AmmCounterManager = (function() {
}
function debugLog(message, data) {
// if (isDebugEnabled()) {
// if (data) {
// console.log(`[AmmCounter] ${message}`, data);
// } else {
// console.log(`[AmmCounter] ${message}`);
// }
// }
}
function updateAmmCounter(count, status) {
@@ -103,7 +97,7 @@ const AmmCounterManager = (function() {
}
if (window.TooltipManager && typeof window.TooltipManager.initializeTooltips === 'function') {
setTimeout(() => {
CleanupManager.setTimeout(() => {
window.TooltipManager.initializeTooltips(`[data-tooltip-target="${tooltipId}"]`);
debugLog(`Re-initialized tooltips for ${tooltipId}`);
}, 50);
@@ -148,7 +142,7 @@ const AmmCounterManager = (function() {
debugLog(`Retrying AMM status fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
return new Promise(resolve => {
setTimeout(() => {
CleanupManager.setTimeout(() => {
resolve(fetchAmmStatus());
}, config.retryDelay);
});
@@ -168,7 +162,7 @@ const AmmCounterManager = (function() {
.then(() => {})
.catch(() => {});
refreshTimer = setInterval(() => {
refreshTimer = CleanupManager.setInterval(() => {
fetchAmmStatus()
.then(() => {})
.catch(() => {});
@@ -251,5 +245,11 @@ document.addEventListener('DOMContentLoaded', function() {
if (!window.ammCounterManagerInitialized) {
window.AmmCounterManager = AmmCounterManager.initialize();
window.ammCounterManagerInitialized = true;
if (window.CleanupManager) {
CleanupManager.registerResource('ammCounter', window.AmmCounterManager, (mgr) => {
if (mgr && mgr.dispose) mgr.dispose();
});
}
}
});

View File

@@ -0,0 +1,573 @@
(function() {
'use strict';
const AMMPage = {
init: function() {
this.loadDebugSetting();
this.setupAutostartCheckbox();
this.setupStartupValidation();
this.setupDebugCheckbox();
this.setupModals();
this.setupClearStateButton();
this.setupWebSocketBalanceUpdates();
this.setupCleanup();
},
saveDebugSetting: function() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
localStorage.setItem('amm_debug_enabled', debugCheckbox.checked);
}
},
loadDebugSetting: function() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
const savedSetting = localStorage.getItem('amm_debug_enabled');
if (savedSetting !== null) {
debugCheckbox.checked = savedSetting === 'true';
}
}
},
setupDebugCheckbox: function() {
const debugCheckbox = document.getElementById('debug-mode');
if (debugCheckbox) {
debugCheckbox.addEventListener('change', this.saveDebugSetting.bind(this));
}
},
saveAutostartSetting: function(checked) {
const bodyData = `autostart=${checked ? 'true' : 'false'}`;
fetch('/amm/autostart', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: bodyData
})
.then(response => response.json())
.then(data => {
if (data.success) {
localStorage.setItem('amm_autostart_enabled', checked);
if (data.autostart !== checked) {
console.warn('WARNING: API returned different autostart value than expected!', {
sent: checked,
received: data.autostart
});
}
} else {
console.error('Failed to save autostart setting:', data.error);
const autostartCheckbox = document.getElementById('autostart-amm');
if (autostartCheckbox) {
autostartCheckbox.checked = !checked;
}
}
})
.catch(error => {
console.error('Error saving autostart setting:', error);
const autostartCheckbox = document.getElementById('autostart-amm');
if (autostartCheckbox) {
autostartCheckbox.checked = !checked;
}
});
},
setupAutostartCheckbox: function() {
const autostartCheckbox = document.getElementById('autostart-amm');
if (autostartCheckbox) {
autostartCheckbox.addEventListener('change', () => {
this.saveAutostartSetting(autostartCheckbox.checked);
});
}
},
showErrorModal: function(title, message) {
document.getElementById('errorTitle').textContent = title || 'Error';
document.getElementById('errorMessage').textContent = message || 'An error occurred';
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.remove('hidden');
}
},
hideErrorModal: function() {
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.add('hidden');
}
},
showConfirmModal: function(title, message, callback) {
document.getElementById('confirmTitle').textContent = title || 'Confirm Action';
document.getElementById('confirmMessage').textContent = message || 'Are you sure?';
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
window.confirmCallback = callback;
},
hideConfirmModal: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
window.confirmCallback = null;
},
setupModals: function() {
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
}
const errorModal = document.getElementById('errorModal');
if (errorModal) {
errorModal.addEventListener('click', (e) => {
if (e.target === errorModal) {
this.hideErrorModal();
}
});
}
const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) {
confirmYesBtn.addEventListener('click', () => {
if (window.confirmCallback && typeof window.confirmCallback === 'function') {
window.confirmCallback();
}
this.hideConfirmModal();
});
}
const confirmNoBtn = document.getElementById('confirmNo');
if (confirmNoBtn) {
confirmNoBtn.addEventListener('click', this.hideConfirmModal.bind(this));
}
const confirmModal = document.getElementById('confirmModal');
if (confirmModal) {
confirmModal.addEventListener('click', (e) => {
if (e.target === confirmModal) {
this.hideConfirmModal();
}
});
}
},
setupStartupValidation: function() {
const controlForm = document.querySelector('form[method="post"]');
if (!controlForm) return;
const startButton = controlForm.querySelector('input[name="start"]');
if (!startButton) return;
startButton.addEventListener('click', (e) => {
e.preventDefault();
this.performStartupValidation();
});
},
performStartupValidation: function() {
const feedbackDiv = document.getElementById('startup-feedback');
const titleEl = document.getElementById('startup-title');
const messageEl = document.getElementById('startup-message');
const progressBar = document.getElementById('startup-progress-bar');
feedbackDiv.classList.remove('hidden');
const steps = [
{ message: 'Checking configuration...', progress: 20 },
{ message: 'Validating offers and bids...', progress: 40 },
{ message: 'Checking wallet balances...', progress: 60 },
{ message: 'Verifying API connection...', progress: 80 },
{ message: 'Starting AMM process...', progress: 100 }
];
let currentStep = 0;
const runNextStep = () => {
if (currentStep >= steps.length) {
this.submitStartForm();
return;
}
const step = steps[currentStep];
messageEl.textContent = step.message;
progressBar.style.width = step.progress + '%';
CleanupManager.setTimeout(() => {
this.validateStep(currentStep).then(result => {
if (result.success) {
currentStep++;
runNextStep();
} else {
this.showStartupError(result.error);
}
}).catch(error => {
this.showStartupError('Validation failed: ' + error.message);
});
}, 500);
};
runNextStep();
},
validateStep: async function(stepIndex) {
try {
switch (stepIndex) {
case 0:
return await this.validateConfiguration();
case 1:
return await this.validateOffersAndBids();
case 2:
return await this.validateWalletBalances();
case 3:
return await this.validateApiConnection();
case 4:
return { success: true };
default:
return { success: true };
}
} catch (error) {
return { success: false, error: error.message };
}
},
validateConfiguration: async function() {
const configData = window.ammTablesConfig?.configData;
if (!configData) {
return { success: false, error: 'No configuration found. Please save a configuration first.' };
}
if (!configData.min_seconds_between_offers || !configData.max_seconds_between_offers) {
return { success: false, error: 'Missing timing configuration. Please check your settings.' };
}
return { success: true };
},
validateOffersAndBids: async function() {
const configData = window.ammTablesConfig?.configData;
if (!configData) {
return { success: false, error: 'Configuration not available for validation.' };
}
const offers = configData.offers || [];
const bids = configData.bids || [];
const enabledOffers = offers.filter(o => o.enabled);
const enabledBids = bids.filter(b => b.enabled);
if (enabledOffers.length === 0 && enabledBids.length === 0) {
return { success: false, error: 'No enabled offers or bids found. Please enable at least one offer or bid before starting.' };
}
for (const offer of enabledOffers) {
if (!offer.amount_step) {
return { success: false, error: `Offer "${offer.name}" is missing required Amount Step (privacy feature).` };
}
const amountStep = parseFloat(offer.amount_step);
const amount = parseFloat(offer.amount);
if (amountStep <= 0 || amountStep < 0.001) {
return { success: false, error: `Offer "${offer.name}" has invalid Amount Step. Must be >= 0.001.` };
}
if (amountStep > amount) {
return { success: false, error: `Offer "${offer.name}" Amount Step (${amountStep}) cannot be greater than offer amount (${amount}).` };
}
}
return { success: true };
},
validateWalletBalances: async function() {
const configData = window.ammTablesConfig?.configData;
if (!configData) return { success: true };
const offers = configData.offers || [];
const enabledOffers = offers.filter(o => o.enabled);
for (const offer of enabledOffers) {
if (!offer.min_coin_from_amt || parseFloat(offer.min_coin_from_amt) <= 0) {
return { success: false, error: `Offer "${offer.name}" needs a minimum coin amount to protect your wallet balance.` };
}
}
return { success: true };
},
validateApiConnection: async function() {
return { success: true };
},
showStartupError: function(errorMessage) {
const feedbackDiv = document.getElementById('startup-feedback');
feedbackDiv.classList.add('hidden');
if (window.showErrorModal) {
window.showErrorModal('AMM Startup Failed', errorMessage);
} else {
alert('AMM Startup Failed: ' + errorMessage);
}
},
submitStartForm: function() {
const feedbackDiv = document.getElementById('startup-feedback');
const titleEl = document.getElementById('startup-title');
const messageEl = document.getElementById('startup-message');
titleEl.textContent = 'Starting AMM...';
messageEl.textContent = 'AMM process is starting. Please wait...';
const controlForm = document.querySelector('form[method="post"]');
if (controlForm) {
const formData = new FormData(controlForm);
formData.append('start', 'Start');
fetch(window.location.pathname, {
method: 'POST',
body: formData
}).then(response => {
if (response.ok) {
window.location.reload();
} else {
throw new Error('Failed to start AMM');
}
}).catch(error => {
this.showStartupError('Failed to start AMM: ' + error.message);
});
}
},
setupClearStateButton: function() {
const clearStateBtn = document.getElementById('clearStateBtn');
if (clearStateBtn) {
clearStateBtn.addEventListener('click', () => {
this.showConfirmModal(
'Clear AMM State',
'This will clear the AMM state file. All running offers/bids will be lost. Are you sure?',
() => {
const form = clearStateBtn.closest('form');
if (form) {
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'prune_state';
hiddenInput.value = 'true';
form.appendChild(hiddenInput);
form.submit();
}
}
);
});
}
},
setAmmAmount: function(percent, fieldId) {
const amountInput = document.getElementById(fieldId);
let coinSelect;
let modalType = null;
if (fieldId.includes('add-amm')) {
const addModal = document.getElementById('add-amm-modal');
modalType = addModal ? addModal.getAttribute('data-amm-type') : null;
} else if (fieldId.includes('edit-amm')) {
const editModal = document.getElementById('edit-amm-modal');
modalType = editModal ? editModal.getAttribute('data-amm-type') : null;
}
if (fieldId.includes('add-amm')) {
const isBidModal = modalType === 'bid';
coinSelect = document.getElementById(isBidModal ? 'add-amm-coin-to' : 'add-amm-coin-from');
} else if (fieldId.includes('edit-amm')) {
const isBidModal = modalType === 'bid';
coinSelect = document.getElementById(isBidModal ? 'edit-amm-coin-to' : 'edit-amm-coin-from');
}
if (!amountInput || !coinSelect) {
console.error('Required elements not found');
return;
}
const selectedOption = coinSelect.options[coinSelect.selectedIndex];
if (!selectedOption) {
if (window.showErrorModal) {
window.showErrorModal('Validation Error', 'Please select a coin first');
} else {
alert('Please select a coin first');
}
return;
}
const balance = selectedOption.getAttribute('data-balance');
if (!balance) {
console.error('Balance not found for selected coin');
return;
}
const floatBalance = parseFloat(balance);
if (isNaN(floatBalance) || floatBalance <= 0) {
if (window.showErrorModal) {
window.showErrorModal('Invalid Balance', 'The selected coin has no available balance. Please select a coin with a positive balance.');
} else {
alert('Invalid balance for selected coin');
}
return;
}
const calculatedAmount = floatBalance * percent;
amountInput.value = calculatedAmount.toFixed(8);
const event = new Event('input', { bubbles: true });
amountInput.dispatchEvent(event);
},
updateAmmModalBalances: function(balanceData) {
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let modalType = null;
if (addModalVisible) {
modalType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
modalType = editModal.getAttribute('data-amm-type');
}
if (modalType === 'offer') {
this.updateOfferDropdownBalances(balanceData);
} else if (modalType === 'bid') {
this.updateBidDropdownBalances(balanceData);
}
},
setupWebSocketBalanceUpdates: function() {
window.BalanceUpdatesManager.setup({
contextKey: 'amm',
balanceUpdateCallback: this.updateAmmModalBalances.bind(this),
swapEventCallback: this.updateAmmModalBalances.bind(this),
errorContext: 'AMM',
enablePeriodicRefresh: true,
periodicInterval: 120000
});
},
updateAmmDropdownBalances: function(balanceData) {
const balanceMap = {};
const pendingMap = {};
balanceData.forEach(coin => {
balanceMap[coin.name] = coin.balance;
pendingMap[coin.name] = coin.pending || '0.0';
});
const dropdownIds = ['add-amm-coin-from', 'edit-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-to'];
dropdownIds.forEach(dropdownId => {
const select = document.getElementById(dropdownId);
if (!select) {
return;
}
Array.from(select.options).forEach(option => {
const coinName = option.value;
const balance = balanceMap[coinName] || '0.0';
const pending = pendingMap[coinName] || '0.0';
option.setAttribute('data-balance', balance);
option.setAttribute('data-pending-balance', pending);
});
});
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
const editModalVisible = editModal && !editModal.classList.contains('hidden');
let currentModalType = null;
if (addModalVisible) {
currentModalType = addModal.getAttribute('data-amm-type');
} else if (editModalVisible) {
currentModalType = editModal.getAttribute('data-amm-type');
}
if (currentModalType && window.ammTablesManager) {
if (currentModalType === 'offer' && typeof window.ammTablesManager.refreshOfferDropdownBalanceDisplay === 'function') {
window.ammTablesManager.refreshOfferDropdownBalanceDisplay();
} else if (currentModalType === 'bid' && typeof window.ammTablesManager.refreshBidDropdownBalanceDisplay === 'function') {
window.ammTablesManager.refreshBidDropdownBalanceDisplay();
}
}
},
updateOfferDropdownBalances: function(balanceData) {
this.updateAmmDropdownBalances(balanceData);
},
updateBidDropdownBalances: function(balanceData) {
this.updateAmmDropdownBalances(balanceData);
},
cleanupAmmBalanceUpdates: function() {
window.BalanceUpdatesManager.cleanup('amm');
if (window.ammDropdowns) {
window.ammDropdowns.forEach(dropdown => {
if (dropdown.parentNode) {
dropdown.parentNode.removeChild(dropdown);
}
});
window.ammDropdowns = [];
}
},
setupCleanup: function() {
if (window.CleanupManager) {
window.CleanupManager.registerResource('ammBalanceUpdates', null, this.cleanupAmmBalanceUpdates.bind(this));
}
const beforeUnloadHandler = this.cleanupAmmBalanceUpdates.bind(this);
window.addEventListener('beforeunload', beforeUnloadHandler);
if (window.CleanupManager) {
CleanupManager.registerResource('ammBeforeUnload', beforeUnloadHandler, () => {
window.removeEventListener('beforeunload', beforeUnloadHandler);
});
}
},
cleanup: function() {
const debugCheckbox = document.getElementById('amm_debug');
const autostartCheckbox = document.getElementById('amm_autostart');
const errorOkBtn = document.getElementById('errorOk');
const confirmYesBtn = document.getElementById('confirmYes');
const confirmNoBtn = document.getElementById('confirmNo');
const startButton = document.getElementById('startAMM');
const clearStateBtn = document.getElementById('clearAmmState');
this.cleanupAmmBalanceUpdates();
}
};
document.addEventListener('DOMContentLoaded', function() {
AMMPage.init();
if (window.BalanceUpdatesManager) {
window.BalanceUpdatesManager.initialize();
}
});
window.AMMPage = AMMPage;
window.showErrorModal = AMMPage.showErrorModal.bind(AMMPage);
window.hideErrorModal = AMMPage.hideErrorModal.bind(AMMPage);
window.showConfirmModal = AMMPage.showConfirmModal.bind(AMMPage);
window.hideConfirmModal = AMMPage.hideConfirmModal.bind(AMMPage);
window.setAmmAmount = AMMPage.setAmmAmount.bind(AMMPage);
})();

View File

@@ -61,53 +61,8 @@ const AmmTablesManager = (function() {
}
function getCoinDisplayName(coinId) {
if (config.debug) {
console.log('[AMM Tables] getCoinDisplayName called with:', coinId, typeof coinId);
}
if (typeof coinId === 'string') {
const lowerCoinId = coinId.toLowerCase();
if (lowerCoinId === 'part_anon' ||
lowerCoinId === 'particl_anon' ||
lowerCoinId === 'particl anon') {
if (config.debug) {
console.log('[AMM Tables] Matched Particl Anon variant:', coinId);
}
return 'Particl Anon';
}
if (lowerCoinId === 'part_blind' ||
lowerCoinId === 'particl_blind' ||
lowerCoinId === 'particl blind') {
if (config.debug) {
console.log('[AMM Tables] Matched Particl Blind variant:', coinId);
}
return 'Particl Blind';
}
if (lowerCoinId === 'ltc_mweb' ||
lowerCoinId === 'litecoin_mweb' ||
lowerCoinId === 'litecoin mweb') {
if (config.debug) {
console.log('[AMM Tables] Matched Litecoin MWEB variant:', coinId);
}
return 'Litecoin MWEB';
}
}
if (window.CoinManager && window.CoinManager.getDisplayName) {
const displayName = window.CoinManager.getDisplayName(coinId);
if (displayName) {
if (config.debug) {
console.log('[AMM Tables] CoinManager returned:', displayName);
}
return displayName;
}
}
if (config.debug) {
console.log('[AMM Tables] Returning coin name as-is:', coinId);
return window.CoinManager.getDisplayName(coinId) || coinId;
}
return coinId;
}
@@ -303,7 +258,6 @@ const AmmTablesManager = (function() {
`;
});
if (offersBody.innerHTML.trim() !== tableHtml.trim()) {
offersBody.innerHTML = tableHtml;
}
@@ -438,7 +392,6 @@ const AmmTablesManager = (function() {
`;
});
if (bidsBody.innerHTML.trim() !== tableHtml.trim()) {
bidsBody.innerHTML = tableHtml;
}
@@ -540,7 +493,6 @@ const AmmTablesManager = (function() {
coinPrice = window.latestPrices[coinName.toUpperCase()];
}
if (!coinPrice || isNaN(coinPrice)) {
return null;
}
@@ -550,6 +502,9 @@ const AmmTablesManager = (function() {
function formatUSDPrice(usdValue) {
if (!usdValue || isNaN(usdValue)) return '';
if (window.config && window.config.utils && window.config.utils.formatPrice) {
return `($${window.config.utils.formatPrice('USD', usdValue)} USD)`;
}
return `($${usdValue.toFixed(2)} USD)`;
}
@@ -728,7 +683,6 @@ const AmmTablesManager = (function() {
const isMakerDropdown = select.id.includes('coin-from');
const isTakerDropdown = select.id.includes('coin-to');
const addModal = document.getElementById('add-amm-modal');
const editModal = document.getElementById('edit-amm-modal');
const addModalVisible = addModal && !addModal.classList.contains('hidden');
@@ -755,7 +709,6 @@ const AmmTablesManager = (function() {
}
}
const result = isBidModal ? isTakerDropdown : isMakerDropdown;
console.log(`[DEBUG] shouldDropdownOptionsShowBalance: ${select.id}, isBidModal=${isBidModal}, isMaker=${isMakerDropdown}, isTaker=${isTakerDropdown}, result=${result}`);
@@ -773,22 +726,18 @@ const AmmTablesManager = (function() {
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
const dropdown = wrapper.querySelector('[role="listbox"]');
if (!dropdown) return;
const options = dropdown.querySelectorAll('[data-value]');
options.forEach(optionElement => {
const coinValue = optionElement.getAttribute('data-value');
const originalOption = Array.from(select.options).find(opt => opt.value === coinValue);
if (!originalOption) return;
const textContainer = optionElement.querySelector('div.flex.flex-col, div.flex.items-center');
if (!textContainer) return;
textContainer.innerHTML = '';
const shouldShowBalance = shouldDropdownOptionsShowBalance(select);
@@ -828,7 +777,6 @@ const AmmTablesManager = (function() {
});
}
function refreshDropdownBalances() {
const dropdownIds = ['add-amm-coin-from', 'add-amm-coin-to', 'edit-amm-coin-from', 'edit-amm-coin-to'];
@@ -839,7 +787,6 @@ const AmmTablesManager = (function() {
const wrapper = select.parentNode.querySelector('.relative');
if (!wrapper) return;
const dropdownItems = wrapper.querySelectorAll('[data-value]');
dropdownItems.forEach(item => {
const value = item.getAttribute('data-value');
@@ -852,7 +799,6 @@ const AmmTablesManager = (function() {
if (balanceDiv) {
balanceDiv.textContent = `Balance: ${balance}`;
let pendingDiv = item.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
@@ -880,7 +826,6 @@ const AmmTablesManager = (function() {
balanceDiv.textContent = `Balance: ${balance}`;
let pendingDiv = textContainer.querySelector('.text-green-500');
if (pendingBalance && parseFloat(pendingBalance) > 0) {
if (!pendingDiv) {
@@ -940,10 +885,8 @@ const AmmTablesManager = (function() {
if (!coinFromSelect || !coinToSelect) return;
const balanceData = {};
Array.from(coinFromSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
@@ -951,7 +894,6 @@ const AmmTablesManager = (function() {
}
});
Array.from(coinToSelect.options).forEach(option => {
const balance = option.getAttribute('data-balance');
if (balance) {
@@ -959,7 +901,6 @@ const AmmTablesManager = (function() {
}
});
updateDropdownOptions(coinFromSelect, balanceData);
updateDropdownOptions(coinToSelect, balanceData);
}
@@ -970,11 +911,9 @@ const AmmTablesManager = (function() {
const balance = balanceData[coinName] || '0.00000000';
const pending = pendingData[coinName] || '0.0';
option.setAttribute('data-balance', balance);
option.setAttribute('data-pending-balance', pending);
option.textContent = coinName;
});
}
@@ -982,7 +921,6 @@ const AmmTablesManager = (function() {
function createSimpleDropdown(select, showBalance = false) {
if (!select) return;
const existingWrapper = select.parentNode.querySelector('.relative');
if (existingWrapper) {
existingWrapper.remove();
@@ -994,13 +932,11 @@ const AmmTablesManager = (function() {
const wrapper = document.createElement('div');
wrapper.className = 'relative';
const button = document.createElement('button');
button.type = 'button';
button.className = 'flex items-center justify-between w-full p-2.5 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:text-white';
button.style.minHeight = '60px';
const displayContent = document.createElement('div');
displayContent.className = 'flex items-center';
@@ -1019,11 +955,9 @@ const AmmTablesManager = (function() {
button.appendChild(displayContent);
button.appendChild(arrow);
const dropdown = document.createElement('div');
dropdown.className = 'absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-lg shadow-lg hidden dark:bg-gray-700 dark:border-gray-600 max-h-60 overflow-y-auto';
Array.from(select.options).forEach(option => {
const item = document.createElement('div');
item.className = 'flex items-center p-2 hover:bg-gray-100 dark:hover:bg-gray-600 cursor-pointer';
@@ -1047,7 +981,6 @@ const AmmTablesManager = (function() {
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${balance}</div>
`;
if (pendingBalance && parseFloat(pendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${pendingBalance} pending</div>`;
}
@@ -1061,11 +994,9 @@ const AmmTablesManager = (function() {
item.appendChild(itemIcon);
item.appendChild(itemText);
item.addEventListener('click', function() {
select.value = this.getAttribute('data-value');
const selectedOption = select.options[select.selectedIndex];
const selectedCoinName = selectedOption.textContent.trim();
const selectedBalance = selectedOption.getAttribute('data-balance') || '0.00000000';
@@ -1079,7 +1010,6 @@ const AmmTablesManager = (function() {
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
@@ -1093,7 +1023,6 @@ const AmmTablesManager = (function() {
dropdown.classList.add('hidden');
const event = new Event('change', { bubbles: true });
select.dispatchEvent(event);
});
@@ -1101,7 +1030,6 @@ const AmmTablesManager = (function() {
dropdown.appendChild(item);
});
const selectedOption = select.options[select.selectedIndex];
if (selectedOption) {
const selectedCoinName = selectedOption.textContent.trim();
@@ -1116,7 +1044,6 @@ const AmmTablesManager = (function() {
<div class="text-gray-500 dark:text-gray-400 text-xs">Balance: ${selectedBalance}</div>
`;
if (selectedPendingBalance && parseFloat(selectedPendingBalance) > 0) {
html += `<div class="text-green-500 text-xs">+${selectedPendingBalance} pending</div>`;
}
@@ -1129,12 +1056,10 @@ const AmmTablesManager = (function() {
}
}
button.addEventListener('click', function() {
dropdown.classList.toggle('hidden');
});
document.addEventListener('click', function(e) {
if (!wrapper.contains(e.target)) {
dropdown.classList.add('hidden');
@@ -1267,7 +1192,6 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Add New ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
const modal = document.getElementById('add-amm-modal');
if (modal) {
modal.classList.remove('hidden');
@@ -1275,17 +1199,14 @@ const AmmTablesManager = (function() {
modal.setAttribute('data-amm-type', type);
}
setTimeout(() => {
updateDropdownsForModalType('add');
initializeCustomSelects(type);
refreshDropdownBalanceDisplay(type);
if (typeof fetchBalanceData === 'function') {
fetchBalanceData()
.then(balanceData => {
@@ -1721,7 +1642,6 @@ const AmmTablesManager = (function() {
modalTitle.textContent = `Edit ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
const modal = document.getElementById('edit-amm-modal');
if (modal) {
modal.classList.remove('hidden');
@@ -1729,17 +1649,14 @@ const AmmTablesManager = (function() {
modal.setAttribute('data-amm-type', type);
}
setTimeout(() => {
updateDropdownsForModalType('edit');
initializeCustomSelects(type);
refreshDropdownBalanceDisplay(type);
if (typeof fetchBalanceData === 'function') {
fetchBalanceData()
.then(balanceData => {
@@ -1873,7 +1790,6 @@ const AmmTablesManager = (function() {
});
}
function closeEditModal() {
const modal = document.getElementById('edit-amm-modal');
if (modal) {
@@ -2306,14 +2222,11 @@ const AmmTablesManager = (function() {
document.getElementById('edit-offer-swap-type')
];
function createSwapTypeDropdown(select) {
if (!select) return;
if (select.style.display === 'none' && select.parentNode.querySelector('.relative')) {
return; // Custom dropdown already exists
return;
}
const wrapper = document.createElement('div');
@@ -2416,9 +2329,9 @@ const AmmTablesManager = (function() {
let showBalance = false;
if (modalType === 'offer' && select.id.includes('coin-from')) {
showBalance = true; // OFFER: maker shows balance
showBalance = true;
} else if (modalType === 'bid' && select.id.includes('coin-to')) {
showBalance = true; // BID: taker shows balance
showBalance = true;
}
createSimpleDropdown(select, showBalance);
@@ -2720,7 +2633,7 @@ const AmmTablesManager = (function() {
icon.classList.remove('animate-spin');
}
refreshButton.disabled = false;
}, 500); // Reduced from 1000ms to 500ms
}, 500);
}
});
}

View File

@@ -53,9 +53,9 @@ const getTimeStrokeColor = (expireTime) => {
const now = Math.floor(Date.now() / 1000);
const timeLeft = expireTime - now;
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
return '#10B981'; // More than 30 minutes
if (timeLeft <= 300) return '#9CA3AF';
if (timeLeft <= 1800) return '#3B82F6';
return '#10B981';
};
const createTimeTooltip = (bid) => {
@@ -249,7 +249,7 @@ const updateLoadingState = (isLoading) => {
const refreshText = elements.refreshBidsButton.querySelector('#refreshText');
if (refreshIcon) {
// Add CSS transition for smoother animation
refreshIcon.style.transition = 'transform 0.3s ease';
refreshIcon.classList.toggle('animate-spin', isLoading);
}
@@ -631,7 +631,7 @@ if (elements.refreshBidsButton) {
updateLoadingState(true);
await new Promise(resolve => setTimeout(resolve, 500));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 500));
try {
await updateBidsTable({ resetPage: true, refreshData: true });

View File

@@ -66,7 +66,7 @@ const BidExporter = {
link.click();
document.body.removeChild(link);
setTimeout(() => {
CleanupManager.setTimeout(() => {
URL.revokeObjectURL(url);
}, 100);
} catch (error) {
@@ -104,7 +104,7 @@ const BidExporter = {
};
document.addEventListener('DOMContentLoaded', function() {
setTimeout(function() {
CleanupManager.setTimeout(function() {
if (typeof state !== 'undefined' && typeof EventManager !== 'undefined') {
const exportAllButton = document.getElementById('exportAllBids');
if (exportAllButton) {

View File

@@ -32,7 +32,7 @@ document.addEventListener('tabactivated', function(event) {
if (event.detail && event.detail.tabId) {
const tabType = event.detail.type || (event.detail.tabId === '#all' ? 'all' :
(event.detail.tabId === '#sent' ? 'sent' : 'received'));
//console.log('Tab activation event received for:', tabType);
state.currentTab = tabType;
updateBidsTable();
}
@@ -190,8 +190,7 @@ const EventManager = {
};
function cleanup() {
//console.log('Starting comprehensive cleanup process for bids table');
try {
if (searchTimeout) {
clearTimeout(searchTimeout);
@@ -326,8 +325,7 @@ window.cleanupBidsTable = cleanup;
CleanupManager.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
//console.log('Page hidden - pausing WebSocket and optimizing memory');
if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
WebSocketManager.pause();
} else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
@@ -351,7 +349,7 @@ CleanupManager.addListener(document, 'visibilitychange', () => {
const lastUpdateTime = state.lastRefresh || 0;
const now = Date.now();
const refreshInterval = 5 * 60 * 1000; // 5 minutes
const refreshInterval = 5 * 60 * 1000;
if (now - lastUpdateTime > refreshInterval) {
setTimeout(() => {
@@ -490,13 +488,7 @@ function coinMatches(offerCoin, filterCoin) {
if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
}
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
if (window.CoinUtils && window.CoinUtils.isSameCoin(offerCoin, filterCoin)) {
return true;
}
@@ -1012,7 +1004,7 @@ const forceTooltipDOMCleanup = () => {
});
}
if (removedCount > 0) {
// console.log(`Tooltip cleanup: found ${foundCount}, removed ${removedCount} detached tooltips`);
}
}
@@ -1323,8 +1315,6 @@ async function fetchBids(type = state.currentTab) {
const withExpiredSelect = document.getElementById('with_expired');
const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true;
//console.log(`Fetching ${type} bids, include expired:`, includeExpired);
const timeoutId = setTimeout(() => {
if (activeFetchController) {
activeFetchController.abort();
@@ -1372,8 +1362,6 @@ async function fetchBids(type = state.currentTab) {
}
}
//console.log(`Received raw ${type} data:`, data.length, 'bids');
state.filters.with_expired = includeExpired;
let processedData;
@@ -1405,12 +1393,16 @@ const updateTableContent = async (type) => {
const tbody = elements[`${type}BidsBody`];
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading bids...</div></td></tr>';
if (window.TooltipManager) {
window.TooltipManager.cleanup();
requestAnimationFrame(() => window.TooltipManager.cleanup());
}
cleanupTooltips();
forceTooltipDOMCleanup();
requestAnimationFrame(() => {
cleanupTooltips();
forceTooltipDOMCleanup();
});
tooltipIdsToCleanup.clear();
@@ -1421,14 +1413,6 @@ const updateTableContent = async (type) => {
const currentPageData = filteredData.slice(startIndex, endIndex);
//console.log('Updating table content:', {
// type: type,
// totalFilteredBids: filteredData.length,
// currentPageBids: currentPageData.length,
// startIndex: startIndex,
// endIndex: endIndex
//});
try {
if (currentPageData.length > 0) {
const BATCH_SIZE = 10;
@@ -1440,9 +1424,6 @@ const updateTableContent = async (type) => {
const rows = await Promise.all(rowPromises);
allRows = allRows.concat(rows);
if (i + BATCH_SIZE < currentPageData.length) {
await new Promise(resolve => setTimeout(resolve, 5));
}
}
const scrollPosition = tbody.parentElement?.scrollTop || 0;
@@ -1495,7 +1476,7 @@ const initializeTooltips = () => {
const tooltipTriggers = document.querySelectorAll(selector);
const tooltipCount = tooltipTriggers.length;
if (tooltipCount > 50) {
//console.log(`Optimizing ${tooltipCount} tooltips`);
const viewportMargin = 200;
const viewportTooltips = Array.from(tooltipTriggers).filter(trigger => {
const rect = trigger.getBoundingClientRect();
@@ -1595,13 +1576,6 @@ const updatePaginationControls = (type) => {
const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`];
const bidsCount = elements[`${type}BidsCount`];
//console.log('Pagination controls update:', {
// type: type,
// totalBids: data.length,
// totalPages: totalPages,
// currentPage: state.currentPage[type]
//});
if (state.currentPage[type] > totalPages) {
state.currentPage[type] = totalPages > 0 ? totalPages : 1;
}
@@ -2077,7 +2051,7 @@ const setupEventListeners = () => {
function setupMemoryMonitoring() {
const MEMORY_CHECK_INTERVAL = 2 * 60 * 1000;
const intervalId = setInterval(() => {
const intervalId = CleanupManager.setInterval(() => {
if (document.hidden) {
console.log('Tab hidden - running memory optimization');
@@ -2110,9 +2084,9 @@ function setupMemoryMonitoring() {
}
}, MEMORY_CHECK_INTERVAL);
document.addEventListener('beforeunload', () => {
CleanupManager.registerResource('bidsMemoryMonitoring', intervalId, () => {
clearInterval(intervalId);
}, { once: true });
});
}
function initialize() {

View File

@@ -7,7 +7,7 @@
originalOnload();
}
setTimeout(function() {
CleanupManager.setTimeout(function() {
initBidsTabNavigation();
handleInitialNavigation();
}, 100);
@@ -15,6 +15,12 @@
document.addEventListener('DOMContentLoaded', function() {
initBidsTabNavigation();
if (window.CleanupManager) {
CleanupManager.registerResource('bidsTabHashChange', handleHashChange, () => {
window.removeEventListener('hashchange', handleHashChange);
});
}
});
window.addEventListener('hashchange', handleHashChange);
@@ -43,7 +49,7 @@
});
window.bidsTabNavigationInitialized = true;
//console.log('Bids tab navigation initialized');
}
function handleInitialNavigation() {
@@ -97,15 +103,13 @@
if (!tabButton) {
if (retryCount < 5) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
activateTabWithRetry(normalizedTabId, retryCount + 1);
}, 100);
}
return;
}
tabButton.click();
if (window.Tabs) {
@@ -160,7 +164,7 @@
}
function triggerDataLoad(tabId) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
if (window.state) {
window.state.currentTab = tabId === '#all' ? 'all' :
(tabId === '#sent' ? 'sent' : 'received');
@@ -181,7 +185,7 @@
document.dispatchEvent(event);
if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
setTimeout(() => {
CleanupManager.setTimeout(() => {
window.TooltipManager.cleanup();
if (typeof window.initializeTooltips === 'function') {
window.initializeTooltips();
@@ -196,7 +200,7 @@
activateTabWithRetry(tabId);
setTimeout(function() {
CleanupManager.setTimeout(function() {
window.scrollTo(0, oldScrollPosition);
}, 0);
}

View File

@@ -16,6 +16,30 @@ const DOM = {
queryAll: (selector) => document.querySelectorAll(selector)
};
const ErrorModal = {
show: function(title, message) {
const errorTitle = document.getElementById('errorTitle');
const errorMessage = document.getElementById('errorMessage');
const modal = document.getElementById('errorModal');
if (errorTitle) errorTitle.textContent = title || 'Error';
if (errorMessage) errorMessage.textContent = message || 'An error occurred';
if (modal) modal.classList.remove('hidden');
},
hide: function() {
const modal = document.getElementById('errorModal');
if (modal) modal.classList.add('hidden');
},
init: function() {
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hide.bind(this));
}
}
};
const Storage = {
get: (key) => {
try {
@@ -450,20 +474,17 @@ const UIEnhancer = {
const coinName = parts[0];
const balanceInfo = parts[1] || '';
selectNameElement.innerHTML = '';
selectNameElement.style.display = 'flex';
selectNameElement.style.flexDirection = 'column';
selectNameElement.style.alignItems = 'flex-start';
selectNameElement.style.lineHeight = '1.2';
const coinNameDiv = document.createElement('div');
coinNameDiv.textContent = coinName;
coinNameDiv.style.fontWeight = 'normal';
coinNameDiv.style.color = 'inherit';
const balanceDiv = document.createElement('div');
balanceDiv.textContent = `Balance: ${balanceInfo}`;
balanceDiv.style.fontSize = '0.75rem';
@@ -473,8 +494,6 @@ const UIEnhancer = {
selectNameElement.appendChild(coinNameDiv);
selectNameElement.appendChild(balanceDiv);
} else {
selectNameElement.textContent = name;
@@ -575,6 +594,8 @@ function initializeApp() {
UIEnhancer.handleErrorHighlighting();
UIEnhancer.updateDisabledStyles();
UIEnhancer.setupCustomSelects();
ErrorModal.init();
}
if (document.readyState === 'loading') {
@@ -582,3 +603,6 @@ if (document.readyState === 'loading') {
} else {
initializeApp();
}
window.showErrorModal = ErrorModal.show.bind(ErrorModal);
window.hideErrorModal = ErrorModal.hide.bind(ErrorModal);

View File

@@ -0,0 +1,364 @@
(function() {
'use strict';
const OfferPage = {
xhr_rates: null,
xhr_bid_params: null,
init: function() {
this.xhr_rates = new XMLHttpRequest();
this.xhr_bid_params = new XMLHttpRequest();
this.setupXHRHandlers();
this.setupEventListeners();
this.handleBidsPageAddress();
},
setupXHRHandlers: function() {
this.xhr_rates.onload = () => {
if (this.xhr_rates.status == 200) {
const obj = JSON.parse(this.xhr_rates.response);
const inner_html = '<h4 class="bold">Rates</h4><pre><code>' + JSON.stringify(obj, null, ' ') + '</code></pre>';
const ratesDisplay = document.getElementById('rates_display');
if (ratesDisplay) {
ratesDisplay.innerHTML = inner_html;
}
}
};
this.xhr_bid_params.onload = () => {
if (this.xhr_bid_params.status == 200) {
const obj = JSON.parse(this.xhr_bid_params.response);
const bidAmountSendInput = document.getElementById('bid_amount_send');
if (bidAmountSendInput) {
bidAmountSendInput.value = obj['amount_to'];
}
this.updateModalValues();
}
};
},
setupEventListeners: function() {
const sendBidBtn = document.querySelector('button[name="sendbid"][value="Send Bid"]');
if (sendBidBtn) {
sendBidBtn.onclick = this.showConfirmModal.bind(this);
}
const modalCancelBtn = document.querySelector('#confirmModal .flex button:last-child');
if (modalCancelBtn) {
modalCancelBtn.onclick = this.hideConfirmModal.bind(this);
}
const mainCancelBtn = document.querySelector('button[name="cancel"]');
if (mainCancelBtn) {
mainCancelBtn.onclick = this.handleCancelClick.bind(this);
}
const validMinsInput = document.querySelector('input[name="validmins"]');
if (validMinsInput) {
validMinsInput.addEventListener('input', this.updateModalValues.bind(this));
}
const addrFromSelect = document.querySelector('select[name="addr_from"]');
if (addrFromSelect) {
addrFromSelect.addEventListener('change', this.updateModalValues.bind(this));
}
const errorOkBtn = document.getElementById('errorOk');
if (errorOkBtn) {
errorOkBtn.addEventListener('click', this.hideErrorModal.bind(this));
}
},
lookup_rates: function() {
const coin_from = document.getElementById('coin_from')?.value;
const coin_to = document.getElementById('coin_to')?.value;
if (!coin_from || !coin_to || coin_from === '-1' || coin_to === '-1') {
alert('Coins from and to must be set first.');
return;
}
const ratesDisplay = document.getElementById('rates_display');
if (ratesDisplay) {
ratesDisplay.innerHTML = '<h4>Rates</h4><p>Updating...</p>';
}
this.xhr_rates.open('POST', '/json/rates');
this.xhr_rates.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
this.xhr_rates.send(`coin_from=${coin_from}&coin_to=${coin_to}`);
},
resetForm: function() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
const bidRateInput = document.getElementById('bid_rate');
const validMinsInput = document.querySelector('input[name="validmins"]');
const amtVar = document.getElementById('amt_var')?.value === 'True';
if (bidAmountSendInput) {
bidAmountSendInput.value = amtVar ? '' : bidAmountSendInput.getAttribute('max');
}
if (bidAmountInput) {
bidAmountInput.value = amtVar ? '' : bidAmountInput.getAttribute('max');
}
if (bidRateInput && !bidRateInput.disabled) {
const defaultRate = document.getElementById('offer_rate')?.value || '';
bidRateInput.value = defaultRate;
}
if (validMinsInput) {
validMinsInput.value = "60";
}
if (!amtVar) {
this.updateBidParams('rate');
}
this.updateModalValues();
const errorMessages = document.querySelectorAll('.error-message');
errorMessages.forEach(msg => msg.remove());
const inputs = document.querySelectorAll('input');
inputs.forEach(input => {
input.classList.remove('border-red-500', 'focus:border-red-500');
});
},
roundUpToDecimals: function(value, decimals) {
const factor = Math.pow(10, decimals);
return Math.ceil(value * factor) / factor;
},
updateBidParams: function(value_changed) {
const coin_from = document.getElementById('coin_from')?.value;
const coin_to = document.getElementById('coin_to')?.value;
const coin_from_exp = parseInt(document.getElementById('coin_from_exp')?.value || '8');
const coin_to_exp = parseInt(document.getElementById('coin_to_exp')?.value || '8');
const amt_var = document.getElementById('amt_var')?.value;
const rate_var = document.getElementById('rate_var')?.value;
const bidAmountInput = document.getElementById('bid_amount');
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidRateInput = document.getElementById('bid_rate');
const offerRateInput = document.getElementById('offer_rate');
if (!coin_from || !coin_to || !amt_var || !rate_var) return;
const rate = rate_var === 'True' && bidRateInput ?
parseFloat(bidRateInput.value) || 0 :
parseFloat(offerRateInput?.value || '0');
if (!rate) return;
if (value_changed === 'rate') {
if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount;
}
} else if (value_changed === 'sending') {
if (bidAmountSendInput && bidAmountInput) {
const sendAmount = parseFloat(bidAmountSendInput.value) || 0;
const receiveAmount = (sendAmount / rate).toFixed(coin_from_exp);
bidAmountInput.value = receiveAmount;
}
} else if (value_changed === 'receiving') {
if (bidAmountInput && bidAmountSendInput) {
const receiveAmount = parseFloat(bidAmountInput.value) || 0;
const sendAmount = this.roundUpToDecimals(receiveAmount * rate, coin_to_exp).toFixed(coin_to_exp);
bidAmountSendInput.value = sendAmount;
}
}
this.validateAmountsAfterChange();
this.xhr_bid_params.open('POST', '/json/rate');
this.xhr_bid_params.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
this.xhr_bid_params.send(`coin_from=${coin_from}&coin_to=${coin_to}&rate=${rate}&amt_from=${bidAmountInput?.value || '0'}`);
this.updateModalValues();
},
validateAmountsAfterChange: function() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
if (bidAmountSendInput) {
const maxSend = parseFloat(bidAmountSendInput.getAttribute('max'));
this.validateMaxAmount(bidAmountSendInput, maxSend);
}
if (bidAmountInput) {
const maxReceive = parseFloat(bidAmountInput.getAttribute('max'));
this.validateMaxAmount(bidAmountInput, maxReceive);
}
},
validateMaxAmount: function(input, maxAmount) {
if (!input) return;
const value = parseFloat(input.value) || 0;
if (value > maxAmount) {
input.value = maxAmount;
}
},
showErrorModal: function(title, message) {
document.getElementById('errorTitle').textContent = title || 'Error';
document.getElementById('errorMessage').textContent = message || 'An error occurred';
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.remove('hidden');
}
},
hideErrorModal: function() {
const modal = document.getElementById('errorModal');
if (modal) {
modal.classList.add('hidden');
}
},
showConfirmModal: function() {
const bidAmountSendInput = document.getElementById('bid_amount_send');
const bidAmountInput = document.getElementById('bid_amount');
const validMinsInput = document.querySelector('input[name="validmins"]');
const addrFromSelect = document.querySelector('select[name="addr_from"]');
let sendAmount = 0;
let receiveAmount = 0;
if (bidAmountSendInput && bidAmountSendInput.value) {
sendAmount = parseFloat(bidAmountSendInput.value) || 0;
}
if (bidAmountInput && bidAmountInput.value) {
receiveAmount = parseFloat(bidAmountInput.value) || 0;
}
if (sendAmount <= 0 || receiveAmount <= 0) {
this.showErrorModal('Validation Error', 'Please enter valid amounts for both sending and receiving.');
return false;
}
const coinFrom = document.getElementById('coin_from_name')?.value || '';
const coinTo = document.getElementById('coin_to_name')?.value || '';
const tlaFrom = document.getElementById('tla_from')?.value || '';
const tlaTo = document.getElementById('tla_to')?.value || '';
const validMins = validMinsInput ? validMinsInput.value : '60';
const addrFrom = addrFromSelect ? addrFromSelect.value : '';
const modalAmtReceive = document.getElementById('modal-amt-receive');
const modalReceiveCurrency = document.getElementById('modal-receive-currency');
const modalAmtSend = document.getElementById('modal-amt-send');
const modalSendCurrency = document.getElementById('modal-send-currency');
const modalAddrFrom = document.getElementById('modal-addr-from');
const modalValidMins = document.getElementById('modal-valid-mins');
if (modalAmtReceive) modalAmtReceive.textContent = receiveAmount.toFixed(8);
if (modalReceiveCurrency) modalReceiveCurrency.textContent = ` ${tlaFrom}`;
if (modalAmtSend) modalAmtSend.textContent = sendAmount.toFixed(8);
if (modalSendCurrency) modalSendCurrency.textContent = ` ${tlaTo}`;
if (modalAddrFrom) modalAddrFrom.textContent = addrFrom || 'Default';
if (modalValidMins) modalValidMins.textContent = validMins;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
},
hideConfirmModal: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
return false;
},
updateModalValues: function() {
},
handleBidsPageAddress: function() {
const selectElement = document.querySelector('select[name="addr_from"]');
const STORAGE_KEY = 'lastUsedAddressBids';
if (!selectElement) return;
const loadInitialAddress = () => {
const savedAddressJSON = localStorage.getItem(STORAGE_KEY);
if (savedAddressJSON) {
try {
const savedAddress = JSON.parse(savedAddressJSON);
selectElement.value = savedAddress.value;
} catch (e) {
selectFirstAddress();
}
} else {
selectFirstAddress();
}
};
const selectFirstAddress = () => {
if (selectElement.options.length > 1) {
const firstOption = selectElement.options[1];
if (firstOption) {
selectElement.value = firstOption.value;
this.saveAddress(firstOption.value, firstOption.text);
}
}
};
selectElement.addEventListener('change', (event) => {
this.saveAddress(event.target.value, event.target.selectedOptions[0].text);
});
loadInitialAddress();
},
saveAddress: function(value, text) {
const addressData = {
value: value,
text: text
};
localStorage.setItem('lastUsedAddressBids', JSON.stringify(addressData));
},
confirmPopup: function() {
return confirm("Are you sure?");
},
handleCancelClick: function(event) {
if (event) event.preventDefault();
const pathParts = window.location.pathname.split('/');
const offerId = pathParts[pathParts.indexOf('offer') + 1];
window.location.href = `/offer/${offerId}`;
},
cleanup: function() {
}
};
document.addEventListener('DOMContentLoaded', function() {
OfferPage.init();
if (window.CleanupManager) {
CleanupManager.registerResource('offerPage', OfferPage, (page) => {
if (page.cleanup) page.cleanup();
});
}
});
window.OfferPage = OfferPage;
window.lookup_rates = OfferPage.lookup_rates.bind(OfferPage);
window.resetForm = OfferPage.resetForm.bind(OfferPage);
window.updateBidParams = OfferPage.updateBidParams.bind(OfferPage);
window.validateMaxAmount = OfferPage.validateMaxAmount.bind(OfferPage);
window.showConfirmModal = OfferPage.showConfirmModal.bind(OfferPage);
window.hideConfirmModal = OfferPage.hideConfirmModal.bind(OfferPage);
window.showErrorModal = OfferPage.showErrorModal.bind(OfferPage);
window.hideErrorModal = OfferPage.hideErrorModal.bind(OfferPage);
window.confirmPopup = OfferPage.confirmPopup.bind(OfferPage);
window.handleBidsPageAddress = OfferPage.handleBidsPageAddress.bind(OfferPage);
})();

View File

@@ -5,8 +5,8 @@ let jsonData = [];
let originalJsonData = [];
let currentSortColumn = 0;
let currentSortDirection = 'desc';
let filterTimeout = null;
let isPaginationInProgress = false;
let autoRefreshInterval = null;
const isSentOffers = window.offersTableConfig.isSentOffers;
const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
@@ -28,6 +28,9 @@ window.tableRateModule = {
processedOffers: new Set(),
getCachedValue(key) {
if (window.CacheManager) {
return window.CacheManager.get(key);
}
const cachedItem = localStorage.getItem(key);
if (cachedItem) {
const parsedItem = JSON.parse(cachedItem);
@@ -41,6 +44,14 @@ window.tableRateModule = {
},
setCachedValue(key, value, resourceType = null) {
if (window.CacheManager) {
const ttl = resourceType ?
window.config.cacheConfig.ttlSettings[resourceType] ||
window.config.cacheConfig.defaultTTL :
900000;
window.CacheManager.set(key, value, ttl);
return;
}
const ttl = resourceType ?
window.config.cacheConfig.ttlSettings[resourceType] ||
window.config.cacheConfig.defaultTTL :
@@ -65,26 +76,6 @@ window.tableRateModule = {
return true;
},
formatUSD(value) {
if (Math.abs(value) < 0.000001) {
return value.toExponential(8) + ' USD';
} else if (Math.abs(value) < 0.01) {
return value.toFixed(8) + ' USD';
} else {
return value.toFixed(2) + ' USD';
}
},
formatNumber(value, decimals) {
if (Math.abs(value) < 0.000001) {
return value.toExponential(decimals);
} else if (Math.abs(value) < 0.01) {
return value.toFixed(decimals);
} else {
return value.toFixed(Math.min(2, decimals));
}
},
getFallbackValue(coinSymbol) {
if (!coinSymbol) return null;
const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase();
@@ -151,6 +142,41 @@ function initializeTooltips() {
}
}
function initializeTooltipsInBatches() {
if (!window.TooltipManager) return;
const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
const BATCH_SIZE = 5;
let currentIndex = 0;
function processBatch() {
const endIndex = Math.min(currentIndex + BATCH_SIZE, tooltipElements.length);
for (let i = currentIndex; i < endIndex; i++) {
const element = tooltipElements[i];
const targetId = element.getAttribute('data-tooltip-target');
if (!targetId) continue;
const tooltipContent = document.getElementById(targetId);
if (tooltipContent) {
window.TooltipManager.create(element, tooltipContent.innerHTML, {
placement: element.getAttribute('data-tooltip-placement') || 'top'
});
}
}
currentIndex = endIndex;
if (currentIndex < tooltipElements.length) {
CleanupManager.setTimeout(processBatch, 0);
}
}
if (tooltipElements.length > 0) {
CleanupManager.setTimeout(processBatch, 0);
}
}
function getValidOffers() {
if (!jsonData) {
return [];
@@ -180,7 +206,6 @@ function saveFilterSettings() {
}));
}
function getSelectedCoins(filterType) {
const dropdown = document.getElementById(`${filterType}_dropdown`);
@@ -188,7 +213,6 @@ function getSelectedCoins(filterType) {
return ['any'];
}
const allCheckboxes = dropdown.querySelectorAll('input[type="checkbox"]');
const selected = [];
@@ -252,7 +276,6 @@ function updateFilterButtonText(filterType) {
textSpan.textContent = `Filter ${filterLabel} (${selected.length} selected)`;
}
button.style.width = '210px';
}
@@ -270,7 +293,6 @@ function updateCoinBadges(filterType) {
const coinName = getCoinNameFromValue(coinValue, filterType);
const badge = document.createElement('span');
const isBidsFilter = filterType === 'coin_to' && !isSentOffers;
const isOffersFilter = filterType === 'coin_from' && !isSentOffers;
const isReceivingFilter = filterType === 'coin_to' && isSentOffers;
@@ -285,7 +307,6 @@ function updateCoinBadges(filterType) {
badge.className = badgeClass + ' cursor-pointer hover:opacity-80';
const coinImage = getCoinImage(coinName);
badge.innerHTML = `
@@ -350,13 +371,7 @@ function coinMatches(offerCoin, filterCoins) {
return true;
}
if ((normalizedOfferCoin === 'firo' || normalizedOfferCoin === 'zcoin') &&
(normalizedFilterCoin === 'firo' || normalizedFilterCoin === 'zcoin')) {
return true;
}
if ((normalizedOfferCoin === 'bitcoincash' && normalizedFilterCoin === 'bitcoin cash') ||
(normalizedOfferCoin === 'bitcoin cash' && normalizedFilterCoin === 'bitcoincash')) {
if (window.CoinUtils && window.CoinUtils.isSameCoin(normalizedOfferCoin, normalizedFilterCoin)) {
return true;
}
@@ -467,7 +482,6 @@ function removeCoinFilter(filterType, coinValue) {
if (checkbox) {
checkbox.checked = false;
updateFilterButtonText(filterType);
updateCoinBadges(filterType);
applyFilters();
@@ -475,7 +489,6 @@ function removeCoinFilter(filterType, coinValue) {
}
}
window.removeCoinFilter = removeCoinFilter;
function filterAndSortData() {
@@ -510,7 +523,6 @@ function filterAndSortData() {
return false;
}
if (selectedCoinTo.length > 0 && !(selectedCoinTo.length === 1 && selectedCoinTo[0] === 'any')) {
const coinNames = selectedCoinTo.map(value => getCoinNameFromValue(value, 'coin_to'));
const matches = coinMatches(offer.coin_to, coinNames);
@@ -519,7 +531,6 @@ function filterAndSortData() {
}
}
if (selectedCoinFrom.length > 0 && !(selectedCoinFrom.length === 1 && selectedCoinFrom[0] === 'any')) {
const coinNames = selectedCoinFrom.map(value => getCoinNameFromValue(value, 'coin_from'));
const matches = coinMatches(offer.coin_from, coinNames);
@@ -674,10 +685,9 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
if (window.CoinManager) {
normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin;
} else {
if (normalizedCoin === 'zcoin') normalizedCoin = 'firo';
if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash')
normalizedCoin = 'bitcoin-cash';
if (normalizedCoin.includes('particl')) normalizedCoin = 'particl';
if (window.CoinUtils) {
normalizedCoin = window.CoinUtils.normalizeCoinName(normalizedCoin, latestPrices);
}
}
let price = null;
if (latestPrices && latestPrices[normalizedCoin]) {
@@ -739,6 +749,23 @@ async function fetchOffers() {
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
const fetchWithRetry = async (url, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Fetch retry ${i + 1}/${maxRetries} for ${url}`);
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
}
}
};
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
@@ -752,14 +779,10 @@ async function fetchOffers() {
}
const [offersResponse, pricesData] = await Promise.all([
fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'),
fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers'),
fetchLatestPrices()
]);
if (!offersResponse.ok) {
throw new Error(`HTTP error! status: ${offersResponse.status}`);
}
const data = await offersResponse.json();
if (data.error) {
@@ -871,27 +894,37 @@ function updateConnectionStatus(status) {
}
function updateRowTimes() {
requestAnimationFrame(() => {
const rows = document.querySelectorAll('[data-offer-id]');
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
if (!offer) return;
const rows = document.querySelectorAll('[data-offer-id]');
const updates = [];
const newPostedTime = formatTime(offer.created_at, true);
const newExpiresIn = formatTimeLeft(offer.expire_at);
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
if (!offer) return;
const postedElement = row.querySelector('.text-xs:first-child');
const expiresElement = row.querySelector('.text-xs:last-child');
const newPostedTime = formatTime(offer.created_at, true);
const newExpiresIn = formatTimeLeft(offer.expire_at);
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
postedElement.textContent = `Posted: ${newPostedTime}`;
}
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
}
const postedElement = row.querySelector('.text-xs:first-child');
const expiresElement = row.querySelector('.text-xs:last-child');
updates.push({
postedElement,
expiresElement,
newPostedTime,
newExpiresIn
});
});
updates.forEach(({ postedElement, expiresElement, newPostedTime, newExpiresIn }) => {
if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
postedElement.textContent = `Posted: ${newPostedTime}`;
}
if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
expiresElement.textContent = `Expires in: ${newExpiresIn}`;
}
});
}
function updateLastRefreshTime() {
@@ -1097,8 +1130,13 @@ async function updateOffersTable(options = {}) {
return;
}
const isIncrementalUpdate = options.incremental === true;
if (!options.skipSkeleton && !isIncrementalUpdate && offersBody) {
offersBody.innerHTML = '<tr><td colspan="10" class="text-center py-8 text-gray-500 dark:text-gray-400"><div class="animate-pulse">Loading offers...</div></td></tr>';
}
if (window.TooltipManager) {
window.TooltipManager.cleanup();
requestAnimationFrame(() => window.TooltipManager.cleanup());
}
const validOffers = getValidOffers();
@@ -1138,28 +1176,72 @@ async function updateOffersTable(options = {}) {
if (row) fragment.appendChild(row);
});
if (i + BATCH_SIZE < itemsToDisplay.length) {
await new Promise(resolve => setTimeout(resolve, 16));
}
}
if (offersBody) {
const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => cleanupRow(row));
offersBody.textContent = '';
offersBody.appendChild(fragment);
if (isIncrementalUpdate && offersBody.children.length > 0) {
const existingRows = Array.from(offersBody.querySelectorAll('tr[data-offer-id]'));
const newRows = Array.from(fragment.querySelectorAll('tr[data-offer-id]'));
const existingMap = new Map(existingRows.map(row => [row.getAttribute('data-offer-id'), row]));
const newMap = new Map(newRows.map(row => [row.getAttribute('data-offer-id'), row]));
existingRows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
if (!newMap.has(offerId)) {
cleanupRow(row);
row.remove();
}
});
newRows.forEach((newRow, index) => {
const offerId = newRow.getAttribute('data-offer-id');
const existingRow = existingMap.get(offerId);
if (existingRow) {
const currentIndex = Array.from(offersBody.children).indexOf(existingRow);
if (currentIndex !== index) {
if (index >= offersBody.children.length) {
offersBody.appendChild(existingRow);
} else {
offersBody.insertBefore(existingRow, offersBody.children[index]);
}
}
} else {
if (index >= offersBody.children.length) {
offersBody.appendChild(newRow);
} else {
offersBody.insertBefore(newRow, offersBody.children[index]);
}
}
});
} else {
const existingRows = offersBody.querySelectorAll('tr');
existingRows.forEach(row => cleanupRow(row));
offersBody.textContent = '';
offersBody.appendChild(fragment);
}
}
initializeTooltips();
initializeTooltipsInBatches();
requestAnimationFrame(() => {
CleanupManager.setTimeout(() => {
updateRowTimes();
updatePaginationInfo();
updateProfitLossDisplays();
}, 10);
CleanupManager.setTimeout(() => {
if (tableRateModule?.initializeTable) {
tableRateModule.initializeTable();
}
});
}, 50);
lastRefreshTime = Date.now();
updateLastRefreshTime();
@@ -1171,7 +1253,10 @@ async function updateOffersTable(options = {}) {
}
function updateProfitLossDisplays() {
const rows = document.querySelectorAll('[data-offer-id]');
const updates = [];
rows.forEach(row => {
const offerId = row.getAttribute('data-offer-id');
const offer = jsonData.find(o => o.offer_id === offerId);
@@ -1179,6 +1264,17 @@ function updateProfitLossDisplays() {
const fromAmount = parseFloat(offer.amount_from) || 0;
const toAmount = parseFloat(offer.amount_to) || 0;
updates.push({
row,
offerId,
offer,
fromAmount,
toAmount
});
});
updates.forEach(({ row, offerId, offer, fromAmount, toAmount }) => {
updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer);
const rateTooltipId = `tooltip-rate-${offerId}`;
@@ -1494,7 +1590,6 @@ function createRateColumn(offer, coinFrom, coinTo) {
`;
}
function createPercentageColumn(offer) {
return `
<td class="py-3 px-2 bold text-sm text-center monospace items-center rate-table-info">
@@ -1731,45 +1826,10 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
const getPriceKey = (coin) => {
if (!coin) return null;
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'zcoin') return 'firo';
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
}
if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) {
return 'part';
}
if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) {
const symbol = window.config.coinMappings.nameToSymbol[coin];
if (symbol) {
if (symbol.toUpperCase() === 'BCH') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
}
if (symbol.toUpperCase() === 'PART') {
return 'part';
}
return symbol.toLowerCase();
}
}
return lowerCoin;
return coin.toLowerCase();
};
const fromSymbol = getPriceKey(coinFrom);
@@ -1849,44 +1909,10 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
const getPriceKey = (coin) => {
if (!coin) return null;
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'zcoin') return 'firo';
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coin, latestPrices);
}
if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) {
return 'part';
}
if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) {
const symbol = window.config.coinMappings.nameToSymbol[coin];
if (symbol) {
if (symbol.toUpperCase() === 'BCH') {
if (latestPrices && latestPrices['bitcoin-cash']) {
return 'bitcoin-cash';
} else if (latestPrices && latestPrices['bch']) {
return 'bch';
}
return 'bitcoin-cash';
}
if (symbol.toUpperCase() === 'PART') {
return 'part';
}
return symbol.toLowerCase();
}
}
return lowerCoin;
return coin.toLowerCase();
};
const fromSymbol = getPriceKey(coinFrom);
@@ -1958,23 +1984,23 @@ function updateTooltipTargets(row, uniqueId) {
});
}
function applyFilters() {
if (filterTimeout) {
clearTimeout(filterTimeout);
filterTimeout = null;
function applyFilters(options = {}) {
if (window.filterTimeout) {
clearTimeout(window.filterTimeout);
window.filterTimeout = null;
}
try {
filterTimeout = setTimeout(() => {
window.filterTimeout = CleanupManager.setTimeout(() => {
currentPage = 1;
jsonData = filterAndSortData();
updateOffersTable();
updateOffersTable(options);
updateClearFiltersButton();
filterTimeout = null;
window.filterTimeout = null;
}, 250);
} catch (error) {
console.error('Error in filter timeout:', error);
filterTimeout = null;
window.filterTimeout = null;
}
}
@@ -2037,13 +2063,10 @@ function formatTimeLeft(timestamp) {
}
function getDisplayName(coinName) {
if (window.CoinManager) {
if (window.CoinManager && window.CoinManager.getDisplayName) {
return window.CoinManager.getDisplayName(coinName) || coinName;
}
if (coinName.toLowerCase() === 'zcoin') {
return 'Firo';
}
return window.config.coinMappings.nameToDisplayName[coinName] || coinName;
return coinName;
}
function getCoinSymbolLowercase(coin) {
@@ -2085,38 +2108,23 @@ function escapeHtml(unsafe) {
}
function getPriceKey(coin) {
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coin);
}
if (!coin) return null;
const lowerCoin = coin.toLowerCase();
if (lowerCoin === 'zcoin') {
return 'firo';
if (window.CoinUtils) {
return window.CoinUtils.normalizeCoinName(coin);
}
if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') {
return 'bitcoin-cash';
}
if (lowerCoin === 'part' || lowerCoin === 'particl' ||
lowerCoin.includes('particl')) {
return 'particl';
}
return lowerCoin;
return coin ? coin.toLowerCase() : null;
}
function getCoinSymbol(fullName) {
if (window.CoinManager) {
return window.CoinManager.getSymbol(fullName) || fullName;
}
return window.config.coinMappings.nameToSymbol[fullName] || fullName;
if (window.CoinUtils) {
return window.CoinUtils.getCoinSymbol(fullName);
}
return fullName;
}
function initializeTableEvents() {
@@ -2140,7 +2148,6 @@ function initializeTableEvents() {
const statusSelect = document.getElementById('status');
const sentFromSelect = document.getElementById('sent_from');
if (coinToButton && coinToDropdown) {
CleanupManager.addListener(coinToButton, 'click', (e) => {
e.stopPropagation();
@@ -2155,7 +2162,6 @@ function initializeTableEvents() {
});
}
if (coinFromButton && coinFromDropdown) {
CleanupManager.addListener(coinFromButton, 'click', (e) => {
e.stopPropagation();
@@ -2220,15 +2226,16 @@ function initializeTableEvents() {
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);
if (window.countdownInterval) clearInterval(window.countdownInterval);
countdownInterval = setInterval(() => {
window.countdownInterval = CleanupManager.setInterval(() => {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
if (remainingTime <= 0) {
clearInterval(countdownInterval);
clearInterval(window.countdownInterval);
window.countdownInterval = null;
refreshText.textContent = 'Refresh';
refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
@@ -2240,7 +2247,6 @@ function initializeTableEvents() {
return;
}
console.log('Manual refresh initiated');
lastRefreshTime = now;
const refreshIcon = document.getElementById('refreshIcon');
const refreshText = document.getElementById('refreshText');
@@ -2267,10 +2273,10 @@ function initializeTableEvents() {
if (!priceData && previousPrices) {
console.log('Using previous price data after failed refresh');
latestPrices = previousPrices;
applyFilters();
applyFilters({ incremental: false });
} else if (priceData) {
latestPrices = priceData;
applyFilters();
applyFilters({ incremental: false });
} else {
throw new Error('Unable to fetch price data');
}
@@ -2278,8 +2284,6 @@ function initializeTableEvents() {
lastRefreshTime = now;
updateLastRefreshTime();
console.log('Manual refresh completed successfully');
} catch (error) {
console.error('Error during manual refresh:', error);
NetworkManager.handleNetworkError(error);
@@ -2320,7 +2324,7 @@ function initializeTableEvents() {
await updateOffersTable({ fromPaginationClick: true });
updatePaginationInfo();
} finally {
setTimeout(() => {
CleanupManager.setTimeout(() => {
isPaginationInProgress = false;
}, 100);
}
@@ -2340,7 +2344,7 @@ function initializeTableEvents() {
await updateOffersTable({ fromPaginationClick: true });
updatePaginationInfo();
} finally {
setTimeout(() => {
CleanupManager.setTimeout(() => {
isPaginationInProgress = false;
}, 100);
}
@@ -2416,18 +2420,44 @@ function handleTableSort(columnIndex, header) {
clearTimeout(window.sortTimeout);
}
window.sortTimeout = setTimeout(() => {
window.sortTimeout = CleanupManager.setTimeout(() => {
applyFilters();
}, 100);
}
function startAutoRefresh() {
const REFRESH_INTERVAL = 2 * 60 * 1000; // 2 minutes
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
autoRefreshInterval = CleanupManager.setInterval(async () => {
try {
const response = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
if (response.ok) {
}
} catch (error) {
console.error('[Auto-refresh] Error during background refresh:', error);
}
}, REFRESH_INTERVAL);
}
function stopAutoRefresh() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
}
async function initializeTableAndData() {
loadSavedSettings();
updateClearFiltersButton();
initializeTableEvents();
initializeTooltips();
updateFilterButtonText('coin_to');
updateFilterButtonText('coin_from');
updateCoinBadges('coin_to');
@@ -2527,24 +2557,44 @@ document.addEventListener('DOMContentLoaded', async function() {
if (window.WebSocketManager) {
WebSocketManager.addMessageHandler('message', async (data) => {
if (data.event === 'new_offer' || data.event === 'offer_revoked') {
//console.log('WebSocket event received:', data.event);
try {
const fetchWithRetry = async (url, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => CleanupManager.setTimeout(resolve, 100 * Math.pow(2, i)));
}
}
};
const previousPrices = latestPrices;
const offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers');
if (!offersResponse.ok) {
throw new Error(`HTTP error! status: ${offersResponse.status}`);
}
const offersResponse = await fetchWithRetry(isSentOffers ? '/json/sentoffers' : '/json/offers');
const newData = await offersResponse.json();
const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
jsonData = formatInitialData(processedNewData);
const newFormattedData = formatInitialData(processedNewData);
const oldOfferIds = originalJsonData.map(o => o.offer_id).sort().join(',');
const newOfferIds = newFormattedData.map(o => o.offer_id).sort().join(',');
const dataChanged = oldOfferIds !== newOfferIds;
if (!dataChanged) {
return;
}
jsonData = newFormattedData;
originalJsonData = [...jsonData];
const previousPrices = latestPrices;
let priceData;
if (window.PriceManager) {
priceData = await window.PriceManager.getPrices(true);
priceData = await window.PriceManager.getPrices(false);
} else {
priceData = await fetchLatestPrices();
}
@@ -2553,12 +2603,10 @@ document.addEventListener('DOMContentLoaded', async function() {
latestPrices = priceData;
CacheManager.set('prices_coingecko', priceData, 'prices');
} else if (previousPrices) {
console.log('Using previous price data after failed refresh');
latestPrices = previousPrices;
}
applyFilters();
applyFilters({ incremental: true, skipSkeleton: true });
updateProfitLossDisplays();
document.querySelectorAll('.usd-value').forEach(usdValue => {
@@ -2569,8 +2617,14 @@ document.addEventListener('DOMContentLoaded', async function() {
if (price !== undefined && price !== null) {
const amount = parseFloat(usdValue.getAttribute('data-amount') || '0');
if (!isNaN(amount) && amount > 0) {
const usdValue = amount * price;
usdValue.textContent = tableRateModule.formatUSD(usdValue);
const calculatedUSD = amount * price;
const formattedUSD = calculatedUSD < 0.01
? calculatedUSD.toFixed(8) + ' USD'
: calculatedUSD.toFixed(2) + ' USD';
if (usdValue.textContent !== formattedUSD) {
usdValue.textContent = formattedUSD;
}
}
}
}
@@ -2578,7 +2632,6 @@ document.addEventListener('DOMContentLoaded', async function() {
updatePaginationInfo();
//console.log('WebSocket-triggered refresh completed successfully');
} catch (error) {
console.error('Error during WebSocket-triggered refresh:', error);
NetworkManager.handleNetworkError(error);
@@ -2613,9 +2666,7 @@ document.addEventListener('DOMContentLoaded', async function() {
});
}
if (window.config.autoRefreshEnabled) {
startAutoRefresh();
}
startAutoRefresh();
const filterForm = document.getElementById('filterForm');
if (filterForm) {
@@ -2649,20 +2700,10 @@ document.addEventListener('DOMContentLoaded', async function() {
}
});
const rowTimeInterval = setInterval(updateRowTimes, 30000);
if (CleanupManager.registerResource) {
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
clearInterval(rowTimeInterval);
});
} else if (CleanupManager.addResource) {
CleanupManager.addResource('rowTimeInterval', rowTimeInterval, () => {
clearInterval(rowTimeInterval);
});
} else {
window._cleanupIntervals = window._cleanupIntervals || [];
window._cleanupIntervals.push(rowTimeInterval);
}
const rowTimeInterval = CleanupManager.setInterval(updateRowTimes, 30000);
CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => {
clearInterval(rowTimeInterval);
});
} catch (error) {
console.error('Error during initialization:', error);
@@ -2694,6 +2735,8 @@ function cleanup() {
window.countdownInterval = null;
}
stopAutoRefresh();
if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) {
window._cleanupIntervals.forEach(interval => {
clearInterval(interval);
@@ -2739,7 +2782,6 @@ function cleanup() {
}
}
//console.log('Offers.js cleanup completed');
} catch (error) {
console.error('Error during cleanup:', error);
}

View File

@@ -2,46 +2,6 @@ const chartConfig = window.config.chartConfig;
const coins = window.config.coins;
const apiKeys = window.config.getAPIKeys();
const utils = {
formatNumber: (number, decimals = 2) => {
if (typeof number !== 'number' || isNaN(number)) {
return '0';
}
try {
return new Intl.NumberFormat('en-US', {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals
}).format(number);
} catch (e) {
return '0';
}
},
formatDate: (timestamp, resolution) => {
const date = new Date(timestamp);
const options = {
day: { hour: '2-digit', minute: '2-digit', hour12: true },
week: { month: 'short', day: 'numeric' },
month: { year: 'numeric', month: 'short', day: 'numeric' }
};
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
},
debounce: (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
};
class AppError extends Error {
constructor(message, type = 'AppError') {
super(message);
this.name = type;
}
}
const logger = {
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
@@ -94,29 +54,6 @@ const api = {
}
},
fetchCryptoCompareDataXHR: (coin) => {
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
}
return Api.fetchCryptoCompareData(coin, {
cryptoCompare: apiKeys.cryptoCompare
});
} catch (error) {
logger.error(`CryptoCompare request failed for ${coin}:`, error);
NetworkManager.handleNetworkError(error);
const cachedData = CacheManager.get(`coinData_${coin}`);
if (cachedData) {
logger.info(`Using cached data for ${coin}`);
return cachedData.value;
}
return { error: error.message };
}
},
fetchCoinGeckoDataXHR: async () => {
try {
const priceData = await window.PriceManager.getPrices();
@@ -242,7 +179,7 @@ const rateLimiter = {
const executeRequest = async () => {
const waitTime = this.getWaitTime(apiName);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
await new Promise(resolve => CleanupManager.setTimeout(resolve, waitTime));
}
try {
@@ -252,7 +189,7 @@ const rateLimiter = {
if (error.message.includes('429') && retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
console.log(`Rate limit hit, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -260,7 +197,7 @@ const rateLimiter = {
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
logger.warn(`Request failed, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
await new Promise(resolve => CleanupManager.setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -303,7 +240,7 @@ const ui = {
if (isError || volume24h === null || volume24h === undefined) {
volumeElement.textContent = 'N/A';
} else {
volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`;
volumeElement.textContent = `${window.config.utils.formatNumber(volume24h, 0)} USD`;
}
volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
}
@@ -345,7 +282,7 @@ const ui = {
}
priceChange1d = data.price_change_percentage_24h || 0;
volume24h = data.total_volume || 0;
volume24h = (data.total_volume !== undefined && data.total_volume !== null) ? data.total_volume : null;
if (isNaN(priceUSD) || isNaN(priceBTC)) {
throw new Error(`Invalid numeric values in data for ${coin}`);
}
@@ -498,7 +435,7 @@ const ui = {
chartContainer.classList.add('blurred');
if (duration > 0) {
setTimeout(() => {
CleanupManager.setTimeout(() => {
ui.hideErrorMessage();
}, duration);
}
@@ -1199,11 +1136,11 @@ const app = {
if (coinData) {
coinData.displayName = coin.displayName || coin.symbol;
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
if (volumeData[backendId]) {
coinData.total_volume = volumeData[backendId].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
const volumeKey = coin.symbol.toLowerCase();
if (volumeData[volumeKey]) {
coinData.total_volume = volumeData[volumeKey].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
}
}
@@ -1231,11 +1168,7 @@ const app = {
} else {
try {
ui.showCoinLoader(coin.symbol);
if (coin.usesCoinGecko) {
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
} else {
data = await api.fetchCryptoCompareDataXHR(coin.symbol);
}
data = await api.fetchCoinGeckoDataXHR(coin.symbol);
if (data.error) {
throw new Error(data.error);
}
@@ -1382,7 +1315,7 @@ const app = {
}
const timeUntilRefresh = nextRefreshTime - now;
app.nextRefreshTime = nextRefreshTime;
app.autoRefreshInterval = setTimeout(() => {
app.autoRefreshInterval = CleanupManager.setTimeout(() => {
if (NetworkManager.isOnline()) {
app.refreshAllData();
} else {
@@ -1394,8 +1327,7 @@ const app = {
},
refreshAllData: async function() {
//console.log('Price refresh started at', new Date().toLocaleTimeString());
if (app.isRefreshing) {
console.log('Refresh already in progress, skipping...');
return;
@@ -1415,7 +1347,7 @@ refreshAllData: async function() {
ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
let remainingTime = seconds;
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
remainingTime--;
if (remainingTime > 0) {
ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
@@ -1428,7 +1360,6 @@ refreshAllData: async function() {
return;
}
//console.log('Starting refresh of all data...');
app.isRefreshing = true;
app.updateNextRefreshTime();
ui.showLoader();
@@ -1443,7 +1374,7 @@ refreshAllData: async function() {
console.warn('BTC price update failed, continuing with cached or default value');
}
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
const allCoinData = await api.fetchCoinGeckoDataXHR();
if (allCoinData.error) {
@@ -1468,11 +1399,11 @@ refreshAllData: async function() {
coinData.displayName = coin.displayName || coin.symbol;
const backendId = getCoinBackendId ? getCoinBackendId(coin.name) : coin.name;
if (volumeData[backendId]) {
coinData.total_volume = volumeData[backendId].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[backendId].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[backendId].price_change_percentage_24h;
const volumeKey = coin.symbol.toLowerCase();
if (volumeData[volumeKey]) {
coinData.total_volume = volumeData[volumeKey].total_volume;
if (!coinData.price_change_percentage_24h && volumeData[volumeKey].price_change_percentage_24h) {
coinData.price_change_percentage_24h = volumeData[volumeKey].price_change_percentage_24h;
}
} else {
try {
@@ -1495,15 +1426,13 @@ refreshAllData: async function() {
const cacheKey = `coinData_${coin.symbol}`;
CacheManager.set(cacheKey, coinData, 'prices');
//console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`);
} catch (coinError) {
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
failedCoins.push(coin.symbol);
}
}
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise(resolve => CleanupManager.setTimeout(resolve, 1000));
if (chartModule.currentCoin) {
try {
@@ -1525,7 +1454,7 @@ refreshAllData: async function() {
let countdown = 5;
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
countdown--;
if (countdown > 0) {
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
@@ -1535,8 +1464,7 @@ refreshAllData: async function() {
}
}, 1000);
}
//console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`);
} catch (error) {
console.error('Critical error during refresh:', error);
NetworkManager.handleNetworkError(error);
@@ -1544,7 +1472,7 @@ refreshAllData: async function() {
let countdown = 10;
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
const countdownInterval = setInterval(() => {
const countdownInterval = CleanupManager.setInterval(() => {
countdown--;
if (countdown > 0) {
ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
@@ -1566,7 +1494,6 @@ refreshAllData: async function() {
app.scheduleNextRefresh();
}
//console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
}
},
@@ -1590,7 +1517,7 @@ refreshAllData: async function() {
const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) {
svg.classList.add('animate-spin');
setTimeout(() => {
CleanupManager.setTimeout(() => {
svg.classList.remove('animate-spin');
}, 2000);
}

View File

@@ -0,0 +1,332 @@
(function() {
'use strict';
const SettingsPage = {
confirmCallback: null,
triggerElement: null,
init: function() {
this.setupTabs();
this.setupCoinHeaders();
this.setupConfirmModal();
this.setupNotificationSettings();
},
setupTabs: function() {
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
const switchTab = (targetTab) => {
tabButtons.forEach(btn => {
if (btn.dataset.tab === targetTab) {
btn.className = 'tab-button border-b-2 border-blue-500 text-blue-600 dark:text-blue-400 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
} else {
btn.className = 'tab-button border-b-2 border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600 py-4 px-1 text-sm font-medium focus:outline-none focus:ring-0';
}
});
tabContents.forEach(content => {
if (content.id === targetTab) {
content.classList.remove('hidden');
} else {
content.classList.add('hidden');
}
});
};
tabButtons.forEach(btn => {
btn.addEventListener('click', () => {
switchTab(btn.dataset.tab);
});
});
},
setupCoinHeaders: function() {
const coinHeaders = document.querySelectorAll('.coin-header');
coinHeaders.forEach(header => {
header.addEventListener('click', function() {
const coinName = this.dataset.coin;
const details = document.getElementById(`details-${coinName}`);
const arrow = this.querySelector('.toggle-arrow');
if (details.classList.contains('hidden')) {
details.classList.remove('hidden');
arrow.style.transform = 'rotate(180deg)';
} else {
details.classList.add('hidden');
arrow.style.transform = 'rotate(0deg)';
}
});
});
},
setupConfirmModal: function() {
const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) {
confirmYesBtn.addEventListener('click', () => {
if (typeof this.confirmCallback === 'function') {
this.confirmCallback();
}
this.hideConfirmDialog();
});
}
const confirmNoBtn = document.getElementById('confirmNo');
if (confirmNoBtn) {
confirmNoBtn.addEventListener('click', () => {
this.hideConfirmDialog();
});
}
},
showConfirmDialog: function(title, message, callback) {
this.confirmCallback = callback;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
return false;
},
hideConfirmDialog: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
this.confirmCallback = null;
return false;
},
confirmDisableCoin: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Disable Coin",
"Are you sure you want to disable this coin?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
setupNotificationSettings: function() {
const notificationsTab = document.getElementById('notifications-tab');
if (notificationsTab) {
notificationsTab.addEventListener('click', () => {
CleanupManager.setTimeout(() => this.syncNotificationSettings(), 100);
});
}
document.addEventListener('change', (e) => {
if (e.target.closest('#notifications')) {
this.syncNotificationSettings();
}
});
this.syncNotificationSettings();
},
syncNotificationSettings: function() {
if (window.NotificationManager && typeof window.NotificationManager.updateSettings === 'function') {
const backendSettings = {
showNewOffers: document.getElementById('notifications_new_offers')?.checked || false,
showNewBids: document.getElementById('notifications_new_bids')?.checked || false,
showBidAccepted: document.getElementById('notifications_bid_accepted')?.checked || false,
showBalanceChanges: document.getElementById('notifications_balance_changes')?.checked || false,
showOutgoingTransactions: document.getElementById('notifications_outgoing_transactions')?.checked || false,
showSwapCompleted: document.getElementById('notifications_swap_completed')?.checked || false,
showUpdateNotifications: document.getElementById('check_updates')?.checked || false,
notificationDuration: parseInt(document.getElementById('notifications_duration')?.value || '5') * 1000
};
window.NotificationManager.updateSettings(backendSettings);
}
},
testUpdateNotification: function() {
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Available: v0.15.0',
'update_available',
{
subtitle: 'Current: v0.14.6 • Click to view release (Test/Dummy)',
releaseUrl: 'https://github.com/basicswap/basicswap/releases/tag/v0.15.0',
releaseNotes: 'New version v0.15.0 is available. Click to view details on GitHub.'
}
);
}
},
testLiveUpdateCheck: function(event) {
const button = event?.target || event?.currentTarget || document.querySelector('[onclick*="testLiveUpdateCheck"]');
if (!button) return;
const originalText = button.textContent;
button.textContent = 'Checking...';
button.disabled = true;
fetch('/json/checkupdates', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (window.NotificationManager) {
const currentVer = data.current_version || 'Unknown';
const latestVer = data.latest_version || currentVer;
if (data.update_available) {
window.NotificationManager.createToast(
`Live Update Available: v${latestVer}`,
'update_available',
{
latest_version: latestVer,
current_version: currentVer,
subtitle: `Current: v${currentVer} • Click to view release`,
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
releaseNotes: 'This is a real update check from GitHub API.'
}
);
} else {
window.NotificationManager.createToast(
'No Updates Available',
'success',
{
subtitle: `Current version v${currentVer} is up to date`
}
);
}
}
})
.catch(error => {
console.error('Update check failed:', error);
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Check Failed',
'error',
{
subtitle: 'Could not check for updates. See console for details.'
}
);
}
})
.finally(() => {
if (button) {
button.textContent = originalText;
button.disabled = false;
}
});
},
checkForUpdatesNow: function(event) {
const button = event?.target || event?.currentTarget || document.querySelector('[data-check-updates]');
if (!button) return;
const originalText = button.textContent;
button.textContent = 'Checking...';
button.disabled = true;
fetch('/json/checkupdates', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Check Failed',
'error',
{
subtitle: data.error
}
);
}
return;
}
if (window.NotificationManager) {
const currentVer = data.current_version || 'Unknown';
const latestVer = data.latest_version || currentVer;
if (data.update_available) {
window.NotificationManager.createToast(
`Update Available: v${latestVer}`,
'update_available',
{
latest_version: latestVer,
current_version: currentVer,
subtitle: `Current: v${currentVer} • Click to view release`,
releaseUrl: `https://github.com/basicswap/basicswap/releases/tag/v${latestVer}`,
releaseNotes: `New version v${latestVer} is available. Click to view details on GitHub.`
}
);
} else {
window.NotificationManager.createToast(
'You\'re Up to Date!',
'success',
{
subtitle: `Current version v${currentVer} is the latest`
}
);
}
}
})
.catch(error => {
console.error('Update check failed:', error);
if (window.NotificationManager) {
window.NotificationManager.createToast(
'Update Check Failed',
'error',
{
subtitle: 'Network error. Please try again later.'
}
);
}
})
.finally(() => {
if (button) {
button.textContent = originalText;
button.disabled = false;
}
});
}
};
SettingsPage.cleanup = function() {
};
document.addEventListener('DOMContentLoaded', function() {
SettingsPage.init();
if (window.CleanupManager) {
CleanupManager.registerResource('settingsPage', SettingsPage, (page) => {
if (page.cleanup) page.cleanup();
});
}
});
window.SettingsPage = SettingsPage;
window.syncNotificationSettings = SettingsPage.syncNotificationSettings.bind(SettingsPage);
window.testUpdateNotification = SettingsPage.testUpdateNotification.bind(SettingsPage);
window.testLiveUpdateCheck = SettingsPage.testLiveUpdateCheck.bind(SettingsPage);
window.checkForUpdatesNow = SettingsPage.checkForUpdatesNow.bind(SettingsPage);
window.showConfirmDialog = SettingsPage.showConfirmDialog.bind(SettingsPage);
window.hideConfirmDialog = SettingsPage.hideConfirmDialog.bind(SettingsPage);
window.confirmDisableCoin = SettingsPage.confirmDisableCoin.bind(SettingsPage);
})();

View File

@@ -127,9 +127,9 @@ const getTimeStrokeColor = (expireTime) => {
const now = Math.floor(Date.now() / 1000);
const timeLeft = expireTime - now;
if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less
if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less
return '#10B981'; // More than 30 minutes
if (timeLeft <= 300) return '#9CA3AF';
if (timeLeft <= 1800) return '#3B82F6';
return '#10B981';
};
const updateConnectionStatus = (status) => {
@@ -520,8 +520,6 @@ const createSwapTableRow = async (swap) => {
async function updateSwapsTable(options = {}) {
const { resetPage = false, refreshData = true } = options;
//console.log('Updating swaps table:', { resetPage, refreshData });
if (state.refreshPromise) {
await state.refreshPromise;
return;
@@ -547,19 +545,17 @@ async function updateSwapsTable(options = {}) {
}
const data = await response.json();
//console.log('Received swap data:', data);
state.swapsData = Array.isArray(data)
? data.filter(swap => {
const isActive = isActiveSwap(swap);
//console.log(`Swap ${swap.bid_id}: ${isActive ? 'Active' : 'Inactive'}`, swap.bid_state);
return isActive;
})
: [];
//console.log('Filtered active swaps:', state.swapsData);
} catch (error) {
//console.error('Error fetching swap data:', error);
state.swapsData = [];
} finally {
state.refreshPromise = null;
@@ -585,8 +581,6 @@ async function updateSwapsTable(options = {}) {
const endIndex = startIndex + PAGE_SIZE;
const currentPageSwaps = state.swapsData.slice(startIndex, endIndex);
//console.log('Current page swaps:', currentPageSwaps);
if (elements.swapsBody) {
if (currentPageSwaps.length > 0) {
const rowPromises = currentPageSwaps.map(swap => createSwapTableRow(swap));
@@ -607,7 +601,7 @@ async function updateSwapsTable(options = {}) {
});
}
} else {
//console.log('No active swaps found, displaying empty state');
elements.swapsBody.innerHTML = `
<tr>
<td colspan="8" class="text-center py-4 text-gray-500 dark:text-white">
@@ -679,7 +673,12 @@ document.addEventListener('DOMContentLoaded', async () => {
WebSocketManager.initialize();
setupEventListeners();
await updateSwapsTable({ resetPage: true, refreshData: true });
const autoRefreshInterval = setInterval(async () => {
const autoRefreshInterval = CleanupManager.setInterval(async () => {
await updateSwapsTable({ resetPage: false, refreshData: true });
}, 10000); // 30 seconds
}, 10000);
CleanupManager.registerResource('swapsAutoRefresh', autoRefreshInterval, () => {
clearInterval(autoRefreshInterval);
});
});

View File

@@ -0,0 +1,372 @@
(function() {
'use strict';
const WalletPage = {
confirmCallback: null,
triggerElement: null,
currentCoinId: '',
activeTooltip: null,
init: function() {
this.setupAddressCopy();
this.setupConfirmModal();
this.setupWithdrawalConfirmation();
this.setupTransactionDisplay();
this.setupWebSocketUpdates();
},
setupAddressCopy: function() {
const copyableElements = [
'main_deposit_address',
'monero_main_address',
'monero_sub_address',
'stealth_address'
];
copyableElements.forEach(id => {
const element = document.getElementById(id);
if (!element) return;
element.classList.add('cursor-pointer', 'hover:bg-gray-100', 'dark:hover:bg-gray-600', 'transition-colors');
if (!element.querySelector('.copy-icon')) {
const copyIcon = document.createElement('span');
copyIcon.className = 'copy-icon absolute right-2 inset-y-0 flex items-center text-gray-500 dark:text-gray-300';
copyIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>`;
element.style.position = 'relative';
element.style.paddingRight = '2.5rem';
element.appendChild(copyIcon);
}
element.addEventListener('click', (e) => {
const textToCopy = element.innerText.trim();
this.copyToClipboard(textToCopy);
element.classList.add('bg-blue-50', 'dark:bg-blue-900');
this.showCopyFeedback(element);
CleanupManager.setTimeout(() => {
element.classList.remove('bg-blue-50', 'dark:bg-blue-900');
}, 1000);
});
});
},
copyToClipboard: function(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
console.log('Address copied to clipboard');
}).catch(err => {
console.error('Failed to copy address:', err);
this.fallbackCopyToClipboard(text);
});
} else {
this.fallbackCopyToClipboard(text);
}
},
fallbackCopyToClipboard: function(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
console.log('Address copied to clipboard (fallback)');
} catch (err) {
console.error('Fallback: Failed to copy address', err);
}
document.body.removeChild(textArea);
},
showCopyFeedback: function(element) {
if (this.activeTooltip && this.activeTooltip.parentNode) {
this.activeTooltip.parentNode.removeChild(this.activeTooltip);
}
const popup = document.createElement('div');
popup.className = 'copy-feedback-popup fixed z-50 bg-blue-600 text-white text-sm py-2 px-3 rounded-md shadow-lg';
popup.innerText = 'Copied!';
document.body.appendChild(popup);
this.activeTooltip = popup;
this.updateTooltipPosition(popup, element);
const scrollHandler = () => {
if (popup.parentNode) {
requestAnimationFrame(() => {
this.updateTooltipPosition(popup, element);
});
}
};
window.addEventListener('scroll', scrollHandler, { passive: true });
popup.style.opacity = '0';
popup.style.transition = 'opacity 0.2s ease-in-out';
CleanupManager.setTimeout(() => {
popup.style.opacity = '1';
}, 10);
CleanupManager.setTimeout(() => {
window.removeEventListener('scroll', scrollHandler);
popup.style.opacity = '0';
CleanupManager.setTimeout(() => {
if (popup.parentNode) {
popup.parentNode.removeChild(popup);
}
if (this.activeTooltip === popup) {
this.activeTooltip = null;
}
}, 200);
}, 1500);
},
updateTooltipPosition: function(tooltip, element) {
const rect = element.getBoundingClientRect();
let top = rect.top - tooltip.offsetHeight - 8;
const left = rect.left + rect.width / 2;
if (top < 10) {
top = rect.bottom + 8;
}
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
tooltip.style.transform = 'translateX(-50%)';
},
setupWithdrawalConfirmation: function() {
const withdrawalClickHandler = (e) => {
const target = e.target.closest('[data-confirm-withdrawal]');
if (target) {
e.preventDefault();
this.triggerElement = target;
this.confirmWithdrawal().catch(() => {
});
}
};
document.addEventListener('click', withdrawalClickHandler);
if (window.CleanupManager) {
CleanupManager.registerResource('walletWithdrawalClick', withdrawalClickHandler, () => {
document.removeEventListener('click', withdrawalClickHandler);
});
}
},
setupConfirmModal: function() {
const confirmYesBtn = document.getElementById('confirmYes');
if (confirmYesBtn) {
confirmYesBtn.addEventListener('click', () => {
if (this.confirmCallback && typeof this.confirmCallback === 'function') {
this.confirmCallback();
}
this.hideConfirmDialog();
});
}
const confirmNoBtn = document.getElementById('confirmNo');
if (confirmNoBtn) {
confirmNoBtn.addEventListener('click', () => {
this.hideConfirmDialog();
});
}
const confirmModal = document.getElementById('confirmModal');
if (confirmModal) {
confirmModal.addEventListener('click', (e) => {
if (e.target === confirmModal) {
this.hideConfirmDialog();
}
});
}
},
showConfirmDialog: function(title, message, callback) {
return new Promise((resolve, reject) => {
this.confirmCallback = () => {
if (callback) callback();
resolve();
};
this.confirmReject = reject;
document.getElementById('confirmTitle').textContent = title;
document.getElementById('confirmMessage').textContent = message;
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.remove('hidden');
}
});
},
hideConfirmDialog: function() {
const modal = document.getElementById('confirmModal');
if (modal) {
modal.classList.add('hidden');
}
if (this.confirmReject) {
this.confirmReject();
}
this.confirmCallback = null;
this.confirmReject = null;
return false;
},
confirmReseed: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Reseed Wallet",
"Are you sure?\nBackup your wallet before and after.\nWon't detect used keys.\nShould only be used for new wallets.",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
confirmWithdrawal: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Withdrawal",
"Are you sure you want to proceed with this withdrawal?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
confirmCreateUTXO: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm Create UTXO",
"Are you sure you want to create this UTXO?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
confirmUTXOResize: function() {
this.triggerElement = document.activeElement;
return this.showConfirmDialog(
"Confirm UTXO Resize",
"Are you sure you want to resize UTXOs?",
() => {
if (this.triggerElement) {
const form = this.triggerElement.form;
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = this.triggerElement.name;
hiddenInput.value = this.triggerElement.value;
form.appendChild(hiddenInput);
form.submit();
}
}
);
},
setupTransactionDisplay: function() {
},
setupWebSocketUpdates: function() {
if (window.BalanceUpdatesManager) {
const coinId = this.getCoinIdFromPage();
if (coinId) {
this.currentCoinId = coinId;
window.BalanceUpdatesManager.setup({
contextKey: 'wallet_' + coinId,
balanceUpdateCallback: this.handleBalanceUpdate.bind(this),
swapEventCallback: this.handleSwapEvent.bind(this),
errorContext: 'Wallet',
enablePeriodicRefresh: true,
periodicInterval: 60000
});
}
}
},
getCoinIdFromPage: function() {
const pathParts = window.location.pathname.split('/');
const walletIndex = pathParts.indexOf('wallet');
if (walletIndex !== -1 && pathParts[walletIndex + 1]) {
return pathParts[walletIndex + 1];
}
return null;
},
handleBalanceUpdate: function(balanceData) {
console.log('Balance updated:', balanceData);
},
handleSwapEvent: function(eventData) {
console.log('Swap event:', eventData);
}
};
document.addEventListener('DOMContentLoaded', function() {
WalletPage.init();
if (window.BalanceUpdatesManager) {
window.BalanceUpdatesManager.initialize();
}
});
window.WalletPage = WalletPage;
window.setupAddressCopy = WalletPage.setupAddressCopy.bind(WalletPage);
window.showConfirmDialog = WalletPage.showConfirmDialog.bind(WalletPage);
window.hideConfirmDialog = WalletPage.hideConfirmDialog.bind(WalletPage);
window.confirmReseed = WalletPage.confirmReseed.bind(WalletPage);
window.confirmWithdrawal = WalletPage.confirmWithdrawal.bind(WalletPage);
window.confirmCreateUTXO = WalletPage.confirmCreateUTXO.bind(WalletPage);
window.confirmUTXOResize = WalletPage.confirmUTXOResize.bind(WalletPage);
window.copyToClipboard = WalletPage.copyToClipboard.bind(WalletPage);
window.showCopyFeedback = WalletPage.showCopyFeedback.bind(WalletPage);
})();

View File

@@ -0,0 +1,344 @@
(function() {
'use strict';
const WalletsPage = {
init: function() {
this.setupWebSocketUpdates();
},
setupWebSocketUpdates: function() {
if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
window.WebSocketManager.initialize();
}
if (window.BalanceUpdatesManager) {
window.BalanceUpdatesManager.setup({
contextKey: 'wallets',
balanceUpdateCallback: this.updateWalletBalances.bind(this),
swapEventCallback: this.updateWalletBalances.bind(this),
errorContext: 'Wallets',
enablePeriodicRefresh: true,
periodicInterval: 60000
});
if (window.WebSocketManager && typeof window.WebSocketManager.addMessageHandler === 'function') {
const priceHandlerId = window.WebSocketManager.addMessageHandler('message', (data) => {
if (data && data.event) {
if (data.event === 'price_updated' || data.event === 'prices_updated') {
clearTimeout(window.walletsPriceUpdateTimeout);
window.walletsPriceUpdateTimeout = CleanupManager.setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 500);
}
}
});
window.walletsPriceHandlerId = priceHandlerId;
}
}
},
updateWalletBalances: function(balanceData) {
if (balanceData) {
balanceData.forEach(coin => {
this.updateWalletDisplay(coin);
});
CleanupManager.setTimeout(() => {
if (window.WalletManager && typeof window.WalletManager.updatePrices === 'function') {
window.WalletManager.updatePrices(true);
}
}, 250);
} else {
window.BalanceUpdatesManager.fetchBalanceData()
.then(data => this.updateWalletBalances(data))
.catch(error => {
console.error('Error updating wallet balances:', error);
});
}
},
updateWalletDisplay: function(coinData) {
if (coinData.name === 'Particl') {
this.updateSpecificBalance('Particl', 'Balance:', coinData.balance, coinData.ticker || 'PART');
} else if (coinData.name === 'Particl Anon') {
this.updateSpecificBalance('Particl', 'Anon Balance:', coinData.balance, coinData.ticker || 'PART');
this.removePendingBalance('Particl', 'Anon Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Anon Balance:', coinData.pending, coinData.ticker || 'PART', 'Anon Pending:', coinData);
}
} else if (coinData.name === 'Particl Blind') {
this.updateSpecificBalance('Particl', 'Blind Balance:', coinData.balance, coinData.ticker || 'PART');
this.removePendingBalance('Particl', 'Blind Balance:');
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingBalance('Particl', 'Blind Balance:', coinData.pending, coinData.ticker || 'PART', 'Blind Unconfirmed:', coinData);
}
} else {
this.updateSpecificBalance(coinData.name, 'Balance:', coinData.balance, coinData.ticker || coinData.name);
if (coinData.name !== 'Particl Anon' && coinData.name !== 'Particl Blind' && coinData.name !== 'Litecoin MWEB') {
if (coinData.pending && parseFloat(coinData.pending) > 0) {
this.updatePendingDisplay(coinData);
} else {
this.removePendingDisplay(coinData.name);
}
}
}
},
updateSpecificBalance: function(coinName, labelText, balance, ticker, isPending = false) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
if (labelElement) {
const currentLabel = labelElement.textContent.trim();
if (currentLabel === labelText) {
if (isPending) {
const cleanBalance = balance.toString().replace(/^\+/, '');
element.textContent = `+${cleanBalance} ${ticker}`;
} else {
element.textContent = `${balance} ${ticker}`;
}
}
}
}
});
},
updatePendingDisplay: function(coinData) {
const walletContainer = this.findWalletContainer(coinData.name);
if (!walletContainer) return;
const existingPendingElements = walletContainer.querySelectorAll('.flex.mb-2.justify-between.items-center');
let staticPendingElement = null;
let staticUsdElement = null;
existingPendingElements.forEach(element => {
const labelElement = element.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('Pending:') && !labelText.includes('USD')) {
staticPendingElement = element;
} else if (labelText.includes('Pending USD value:')) {
staticUsdElement = element;
}
}
});
if (staticPendingElement && staticUsdElement) {
const pendingSpan = staticPendingElement.querySelector('.coinname-value');
if (pendingSpan) {
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${coinData.ticker || coinData.name}`;
}
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinData.name.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdDiv = staticUsdElement.querySelector('.usd-value');
if (usdDiv) {
usdDiv.textContent = initialUSD;
}
return;
}
let pendingContainer = walletContainer.querySelector('.pending-container');
if (!pendingContainer) {
const balanceContainer = walletContainer.querySelector('.flex.mb-2.justify-between.items-center');
if (!balanceContainer) return;
pendingContainer = document.createElement('div');
pendingContainer.className = 'pending-container';
balanceContainer.parentNode.insertBefore(pendingContainer, balanceContainer.nextSibling);
}
pendingContainer.innerHTML = '';
const pendingDiv = document.createElement('div');
pendingDiv.className = 'flex mb-2 justify-between items-center';
const cleanPending = coinData.pending.toString().replace(/^\+/, '');
pendingDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending:</h4>
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinData.name}">+${cleanPending} ${coinData.ticker || coinData.name}</span>
`;
pendingContainer.appendChild(pendingDiv);
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinData.name.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdDiv = document.createElement('div');
usdDiv.className = 'flex mb-2 justify-between items-center';
usdDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Pending USD value:</h4>
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
`;
pendingContainer.appendChild(usdDiv);
},
removePendingDisplay: function(coinName) {
const walletContainer = this.findWalletContainer(coinName);
if (!walletContainer) return;
const pendingContainer = walletContainer.querySelector('.pending-container');
if (pendingContainer) {
pendingContainer.remove();
}
},
findWalletContainer: function(coinName) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
for (const element of balanceElements) {
if (element.getAttribute('data-coinname') === coinName) {
return element.closest('.bg-white, .dark\\:bg-gray-500');
}
}
return null;
},
removePendingBalance: function(coinName, balanceType) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentDiv = element.closest('.flex.mb-2.justify-between.items-center');
const labelElement = parentDiv ? parentDiv.querySelector('h4') : null;
if (labelElement) {
const currentLabel = labelElement.textContent.trim();
if (currentLabel.includes('Pending:') || currentLabel.includes('Unconfirmed:')) {
const nextElement = parentDiv.nextElementSibling;
if (nextElement && nextElement.querySelector('h4')?.textContent.includes('USD value:')) {
nextElement.remove();
}
parentDiv.remove();
}
}
}
});
},
updatePendingBalance: function(coinName, balanceType, pendingAmount, ticker, pendingLabel, coinData) {
const balanceElements = document.querySelectorAll('.coinname-value[data-coinname]');
let targetElement = null;
balanceElements.forEach(element => {
const elementCoinName = element.getAttribute('data-coinname');
if (elementCoinName === coinName) {
const parentElement = element.closest('.flex.mb-2.justify-between.items-center');
if (parentElement) {
const labelElement = parentElement.querySelector('h4');
if (labelElement && labelElement.textContent.includes(balanceType)) {
targetElement = parentElement;
}
}
}
});
if (!targetElement) return;
let insertAfterElement = targetElement;
let nextElement = targetElement.nextElementSibling;
while (nextElement) {
const labelElement = nextElement.querySelector('h4');
if (labelElement) {
const labelText = labelElement.textContent;
if (labelText.includes('USD value:') && !labelText.includes('Pending') && !labelText.includes('Unconfirmed')) {
insertAfterElement = nextElement;
break;
}
if (labelText.includes('Balance:') || labelText.includes('Pending:') || labelText.includes('Unconfirmed:')) {
break;
}
}
nextElement = nextElement.nextElementSibling;
}
let pendingElement = insertAfterElement.nextElementSibling;
while (pendingElement && !pendingElement.querySelector('h4')?.textContent.includes(pendingLabel)) {
pendingElement = pendingElement.nextElementSibling;
if (pendingElement && pendingElement.querySelector('h4')?.textContent.includes('Balance:')) {
pendingElement = null;
break;
}
}
if (!pendingElement) {
const newPendingDiv = document.createElement('div');
newPendingDiv.className = 'flex mb-2 justify-between items-center';
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
newPendingDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel}</h4>
<span class="coinname-value text-sm font-medium text-green-600 dark:text-green-400" data-coinname="${coinName}">+${cleanPending} ${ticker}</span>
`;
insertAfterElement.parentNode.insertBefore(newPendingDiv, insertAfterElement.nextSibling);
let initialUSD = '$0.00';
if (window.WalletManager && window.WalletManager.coinPrices) {
const coinId = coinName.toLowerCase().replace(' ', '-');
const price = window.WalletManager.coinPrices[coinId];
if (price && price.usd) {
const usdValue = (parseFloat(cleanPending) * price.usd).toFixed(2);
initialUSD = `$${usdValue}`;
}
}
const usdDiv = document.createElement('div');
usdDiv.className = 'flex mb-2 justify-between items-center';
usdDiv.innerHTML = `
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300">${pendingLabel.replace(':', '')} USD value:</h4>
<div class="usd-value text-sm font-medium text-green-600 dark:text-green-400">${initialUSD}</div>
`;
newPendingDiv.parentNode.insertBefore(usdDiv, newPendingDiv.nextSibling);
} else {
const pendingSpan = pendingElement.querySelector('.coinname-value');
if (pendingSpan) {
const cleanPending = pendingAmount.toString().replace(/^\+/, '');
pendingSpan.textContent = `+${cleanPending} ${ticker}`;
}
}
}
};
document.addEventListener('DOMContentLoaded', function() {
WalletsPage.init();
});
window.WalletsPage = WalletsPage;
})();

View File

@@ -89,7 +89,7 @@
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.hide();
});
window.addEventListener('scroll', this._handleScroll, true);
window.addEventListener('scroll', this._handleScroll, { passive: true, capture: true });
window.addEventListener('resize', this._handleResize);
}
@@ -170,7 +170,7 @@
destroy() {
document.removeEventListener('click', this._handleOutsideClick);
window.removeEventListener('scroll', this._handleScroll, true);
window.removeEventListener('scroll', this._handleScroll, { passive: true, capture: true });
window.removeEventListener('resize', this._handleResize);
const index = dropdownInstances.indexOf(this);