diff --git a/basicswap/http_server.py b/basicswap/http_server.py
index e56a783..e1e2774 100644
--- a/basicswap/http_server.py
+++ b/basicswap/http_server.py
@@ -179,6 +179,16 @@ class HttpHandler(BaseHTTPRequestHandler):
self.server.msg_id_counter += 1
args_dict["err_messages"] = err_messages_with_ids
+ if self.path:
+ parsed = parse.urlparse(self.path)
+ url_split = parsed.path.split("/")
+ if len(url_split) > 1 and url_split[1]:
+ args_dict["current_page"] = url_split[1]
+ else:
+ args_dict["current_page"] = "index"
+ else:
+ args_dict["current_page"] = "index"
+
shutdown_token = os.urandom(8).hex()
self.server.session_tokens["shutdown"] = shutdown_token
args_dict["shutdown_token"] = shutdown_token
@@ -411,7 +421,6 @@ class HttpHandler(BaseHTTPRequestHandler):
return self.render_template(
template,
{
- "refresh": 30,
"active_swaps": [
(
s[0].hex(),
diff --git a/basicswap/js_server.py b/basicswap/js_server.py
index 233021c..bc47292 100644
--- a/basicswap/js_server.py
+++ b/basicswap/js_server.py
@@ -984,37 +984,49 @@ def js_readurl(self, url_split, post_string, is_json) -> bytes:
def js_active(self, url_split, post_string, is_json) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
- filters = {"sort_by": "created_at", "sort_dir": "desc"}
+
+ filters = {
+ "sort_by": "created_at",
+ "sort_dir": "desc",
+ "with_available_or_active": True,
+ "with_extra_info": True,
+ }
+
EXCLUDED_STATES = [
- "Completed",
- "Expired",
- "Timed-out",
- "Abandoned",
"Failed, refunded",
"Failed, swiped",
"Failed",
"Error",
- "received",
+ "Expired",
+ "Timed-out",
+ "Abandoned",
+ "Completed",
]
+
all_bids = []
processed_bid_ids = set()
-
try:
received_bids = swap_client.listBids(filters=filters)
sent_bids = swap_client.listBids(sent=True, filters=filters)
+
for bid in received_bids + sent_bids:
try:
bid_id_hex = bid[2].hex()
if bid_id_hex in processed_bid_ids:
continue
- bid_state = strBidState(bid[5])
- tx_state_a = strTxState(bid[7])
- tx_state_b = strTxState(bid[8])
- if bid_state in EXCLUDED_STATES:
- continue
+
offer = swap_client.getOffer(bid[3])
if not offer:
continue
+
+ bid_state = strBidState(bid[5])
+
+ if bid_state in EXCLUDED_STATES:
+ continue
+
+ tx_state_a = strTxState(bid[7])
+ tx_state_b = strTxState(bid[8])
+
swap_data = {
"bid_id": bid_id_hex,
"offer_id": bid[3].hex(),
@@ -1041,6 +1053,7 @@ def js_active(self, url_split, post_string, is_json) -> bytes:
continue
except Exception:
return bytes(json.dumps([]), "UTF-8")
+
return bytes(json.dumps(all_bids), "UTF-8")
diff --git a/basicswap/static/js/bids_available.js b/basicswap/static/js/bids_available.js
index 7e31cca..3cb11e8 100644
--- a/basicswap/static/js/bids_available.js
+++ b/basicswap/static/js/bids_available.js
@@ -1,4 +1,3 @@
-// Constants and State
const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
@@ -16,7 +15,6 @@ const COIN_NAME_TO_SYMBOL = {
'Dogecoin': 'DOGE'
};
-// Global state
const state = {
dentities: new Map(),
currentPage: 1,
@@ -27,7 +25,6 @@ const state = {
refreshPromise: null
};
-// DOM
const elements = {
bidsBody: document.getElementById('bids-body'),
prevPageButton: document.getElementById('prevPage'),
@@ -40,125 +37,6 @@ const elements = {
statusText: document.getElementById('status-text')
};
-// Identity Manager
-const IdentityManager = {
- cache: new Map(),
- pendingRequests: new Map(),
- retryDelay: 2000,
- maxRetries: 3,
- cacheTimeout: 5 * 60 * 1000, // 5 minutes
-
- async getIdentityData(address) {
- if (!address) {
- return { address: '' };
- }
-
- const cachedData = this.getCachedIdentity(address);
- if (cachedData) {
- return { ...cachedData, address };
- }
-
- if (this.pendingRequests.has(address)) {
- const pendingData = await this.pendingRequests.get(address);
- return { ...pendingData, address };
- }
-
- const request = this.fetchWithRetry(address);
- this.pendingRequests.set(address, request);
-
- try {
- const data = await request;
- this.cache.set(address, {
- data,
- timestamp: Date.now()
- });
- return { ...data, address };
- } catch (error) {
- console.warn(`Error fetching identity for ${address}:`, error);
- return { address };
- } finally {
- this.pendingRequests.delete(address);
- }
- },
-
- getCachedIdentity(address) {
- const cached = this.cache.get(address);
- if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
- return cached.data;
- }
- if (cached) {
- this.cache.delete(address);
- }
- return null;
- },
-
- async fetchWithRetry(address, attempt = 1) {
- try {
- const response = await fetch(`/json/identities/${address}`, {
- signal: AbortSignal.timeout(5000)
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data = await response.json();
- return {
- ...data,
- address,
- num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
- num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
- num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
- num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
- num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
- num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
- label: data.label || '',
- note: data.note || '',
- automation_override: safeParseInt(data.automation_override)
- };
- } catch (error) {
- if (attempt >= this.maxRetries) {
- console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
- return {
- address,
- num_sent_bids_successful: 0,
- num_recv_bids_successful: 0,
- num_sent_bids_failed: 0,
- num_recv_bids_failed: 0,
- num_sent_bids_rejected: 0,
- num_recv_bids_rejected: 0,
- label: '',
- note: '',
- automation_override: 0
- };
- }
-
- await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
- return this.fetchWithRetry(address, attempt + 1);
- }
- },
-
- clearCache() {
- this.cache.clear();
- this.pendingRequests.clear();
- },
-
- removeFromCache(address) {
- this.cache.delete(address);
- this.pendingRequests.delete(address);
- },
-
- cleanup() {
- const now = Date.now();
- for (const [address, cached] of this.cache.entries()) {
- if (now - cached.timestamp >= this.cacheTimeout) {
- this.cache.delete(address);
- }
- }
- }
-};
-
-// Util
const formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
@@ -342,108 +220,6 @@ const createIdentityTooltip = (identity) => {
`;
};
-// WebSocket Manager
-const WebSocketManager = {
- ws: null,
- processingQueue: false,
- reconnectTimeout: null,
- maxReconnectAttempts: 5,
- reconnectAttempts: 0,
- reconnectDelay: 5000,
-
- initialize() {
- this.connect();
- this.startHealthCheck();
- },
-
- connect() {
- if (this.ws?.readyState === WebSocket.OPEN) return;
-
- try {
- let wsPort;
-
- if (typeof getWebSocketConfig === 'function') {
- const wsConfig = getWebSocketConfig();
- wsPort = wsConfig?.port || wsConfig?.fallbackPort;
- }
- if (!wsPort && window.config?.port) {
- wsPort = window.config.port;
- }
- if (!wsPort) {
- wsPort = window.ws_port || '11700';
- }
- console.log("Using WebSocket port:", wsPort);
- this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
- this.setupEventHandlers();
- } catch (error) {
- console.error('WebSocket connection error:', error);
- this.handleReconnect();
- }
-},
-
- setupEventHandlers() {
- this.ws.onopen = () => {
- state.wsConnected = true;
- this.reconnectAttempts = 0;
- updateConnectionStatus('connected');
- console.log('🟢 WebSocket connection established for Bid Requests');
- updateBidsTable({ resetPage: true, refreshData: true });
- };
-
- this.ws.onmessage = () => {
- if (!this.processingQueue) {
- this.processingQueue = true;
- setTimeout(async () => {
- try {
- if (!state.isRefreshing) {
- await updateBidsTable({ resetPage: false, refreshData: true });
- }
- } finally {
- this.processingQueue = false;
- }
- }, 200);
- }
- };
-
- this.ws.onclose = () => {
- state.wsConnected = false;
- updateConnectionStatus('disconnected');
- this.handleReconnect();
- };
-
- this.ws.onerror = () => {
- updateConnectionStatus('error');
- };
- },
-
- startHealthCheck() {
- setInterval(() => {
- if (this.ws?.readyState !== WebSocket.OPEN) {
- this.handleReconnect();
- }
- }, 30000);
- },
-
- handleReconnect() {
- if (this.reconnectTimeout) {
- clearTimeout(this.reconnectTimeout);
- }
-
- this.reconnectAttempts++;
- if (this.reconnectAttempts <= this.maxReconnectAttempts) {
- const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
- this.reconnectTimeout = setTimeout(() => this.connect(), delay);
- } else {
- updateConnectionStatus('error');
- setTimeout(() => {
- this.reconnectAttempts = 0;
- this.connect();
- }, 60000);
- }
- }
-};
-
-// UI
const updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return;
@@ -864,7 +640,6 @@ async function updateBidsTable(options = {}) {
}
}
-// Event
const setupEventListeners = () => {
if (elements.refreshBidsButton) {
elements.refreshBidsButton.addEventListener('click', async () => {
@@ -904,8 +679,8 @@ if (elements.refreshBidsButton) {
}
};
-// Init
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('DOMContentLoaded', async () => {
WebSocketManager.initialize();
setupEventListeners();
+ await updateBidsTable({ resetPage: true, refreshData: true });
});
diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js
index 2ea8d3b..cdbebca 100644
--- a/basicswap/static/js/bids_sentreceived.js
+++ b/basicswap/static/js/bids_sentreceived.js
@@ -1,4 +1,3 @@
-// Constants and State
const PAGE_SIZE = 50;
const state = {
currentPage: {
@@ -167,262 +166,225 @@ const EventManager = {
};
function cleanup() {
- console.log('Starting cleanup process');
- EventManager.clearAll();
-
- const exportSentButton = document.getElementById('exportSentBids');
- const exportReceivedButton = document.getElementById('exportReceivedBids');
-
- if (exportSentButton) {
- exportSentButton.remove();
- }
-
- if (exportReceivedButton) {
- exportReceivedButton.remove();
- }
-
- if (window.TooltipManager) {
- const originalCleanup = window.TooltipManager.cleanup;
- window.TooltipManager.cleanup = function() {
- originalCleanup.call(window.TooltipManager);
-
- setTimeout(() => {
- forceTooltipDOMCleanup();
-
- const detachedTooltips = document.querySelectorAll('[id^="tooltip-"]');
- detachedTooltips.forEach(tooltip => {
- const tooltipId = tooltip.id;
- const trigger = document.querySelector(`[data-tooltip-target="${tooltipId}"]`);
- if (!trigger || !document.body.contains(trigger)) {
- tooltip.remove();
- }
- });
- }, 10);
- };
-}
-
- WebSocketManager.cleanup();
- if (searchTimeout) {
- clearTimeout(searchTimeout);
- searchTimeout = null;
- }
- state.data = {
- sent: [],
- received: []
- };
- IdentityManager.clearCache();
- Object.keys(elements).forEach(key => {
- elements[key] = null;
- });
+ //console.log('Starting comprehensive cleanup process for bids table');
- console.log('Cleanup completed');
+ try {
+ if (searchTimeout) {
+ clearTimeout(searchTimeout);
+ searchTimeout = null;
+ }
+
+ if (state.refreshPromise) {
+ state.isRefreshing = false;
+ }
+
+ if (window.WebSocketManager) {
+ WebSocketManager.disconnect();
+ }
+
+ cleanupTooltips();
+ forceTooltipDOMCleanup();
+
+ if (window.TooltipManager) {
+ window.TooltipManager.cleanup();
+ }
+
+ tooltipIdsToCleanup.clear();
+
+ const cleanupTableBody = (tableId) => {
+ const tbody = document.getElementById(tableId);
+ if (!tbody) return;
+
+ const rows = tbody.querySelectorAll('tr');
+ rows.forEach(row => {
+ if (window.CleanupManager) {
+ CleanupManager.removeListenersByElement(row);
+ } else {
+ EventManager.removeAll(row);
+ }
+ Array.from(row.attributes).forEach(attr => {
+ if (attr.name.startsWith('data-')) {
+ row.removeAttribute(attr.name);
+ }
+ });
+ });
+ while (tbody.firstChild) {
+ tbody.removeChild(tbody.firstChild);
+ }
+ };
+
+ cleanupTableBody('sent-tbody');
+ cleanupTableBody('received-tbody');
+
+ if (window.CleanupManager) {
+ CleanupManager.clearAll();
+ } else {
+ EventManager.clearAll();
+ }
+
+ const clearAllAnimationFrames = () => {
+ const rafList = window.requestAnimationFrameList;
+ if (Array.isArray(rafList)) {
+ rafList.forEach(id => {
+ cancelAnimationFrame(id);
+ });
+ window.requestAnimationFrameList = [];
+ }
+ };
+ clearAllAnimationFrames();
+
+ state.data = {
+ sent: [],
+ received: []
+ };
+
+ state.currentPage = {
+ sent: 1,
+ received: 1
+ };
+
+ state.isLoading = false;
+ state.isRefreshing = false;
+ state.wsConnected = false;
+ state.refreshPromise = null;
+
+ state.filters = {
+ state: -1,
+ sort_by: 'created_at',
+ sort_dir: 'desc',
+ with_expired: true,
+ searchQuery: '',
+ coin_from: 'any',
+ coin_to: 'any'
+ };
+
+ if (window.IdentityManager) {
+ IdentityManager.clearCache();
+ }
+
+ if (window.CacheManager) {
+ CacheManager.cleanup(true);
+ }
+
+ if (window.MemoryManager) {
+ MemoryManager.forceCleanup();
+ }
+
+ Object.keys(elements).forEach(key => {
+ elements[key] = null;
+ });
+
+ console.log('Comprehensive cleanup completed');
+ } catch (error) {
+ console.error('Error during cleanup process:', error);
+
+ try {
+ if (window.EventManager) EventManager.clearAll();
+ if (window.CleanupManager) CleanupManager.clearAll();
+ if (window.WebSocketManager) WebSocketManager.disconnect();
+
+ state.data = { sent: [], received: [] };
+ state.isLoading = false;
+
+ Object.keys(elements).forEach(key => {
+ elements[key] = null;
+ });
+ } catch (e) {
+ console.error('Failsafe cleanup also failed:', e);
+ }
+ }
}
-document.addEventListener('beforeunload', cleanup);
-document.addEventListener('visibilitychange', () => {
+window.cleanupBidsTable = cleanup;
+
+CleanupManager.addListener(document, 'visibilitychange', () => {
if (document.hidden) {
- WebSocketManager.pause();
+ //console.log('Page hidden - pausing WebSocket and optimizing memory');
+
+ if (WebSocketManager && typeof WebSocketManager.pause === 'function') {
+ WebSocketManager.pause();
+ } else if (WebSocketManager && typeof WebSocketManager.disconnect === 'function') {
+ WebSocketManager.disconnect();
+ }
+
+ if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
+ window.TooltipManager.cleanup();
+ }
+
+ // Run memory optimization
+ if (window.MemoryManager) {
+ MemoryManager.forceCleanup();
+ }
} else {
- WebSocketManager.resume();
+
+ if (WebSocketManager && typeof WebSocketManager.resume === 'function') {
+ WebSocketManager.resume();
+ } else if (WebSocketManager && typeof WebSocketManager.connect === 'function') {
+ WebSocketManager.connect();
+ }
+
+ const lastUpdateTime = state.lastRefresh || 0;
+ const now = Date.now();
+ const refreshInterval = 5 * 60 * 1000; // 5 minutes
+
+ if (now - lastUpdateTime > refreshInterval) {
+ setTimeout(() => {
+ updateBidsTable();
+ }, 500);
+ }
}
});
-// WebSocket Management
-const WebSocketManager = {
- ws: null,
- processingQueue: false,
- reconnectTimeout: null,
- maxReconnectAttempts: 5,
- reconnectAttempts: 0,
- reconnectDelay: 5000,
- healthCheckInterval: null,
- isPaused: false,
- lastMessageTime: Date.now(),
+CleanupManager.addListener(window, 'beforeunload', () => {
+ cleanup();
+});
+
+function cleanupRow(row) {
+ if (!row) return;
+
+ const tooltipTriggers = row.querySelectorAll('[data-tooltip-target]');
+ tooltipTriggers.forEach(trigger => {
+ if (window.TooltipManager) {
+ window.TooltipManager.destroy(trigger);
+ }
+ });
+
+ if (window.CleanupManager) {
+ CleanupManager.removeListenersByElement(row);
+ } else {
+ EventManager.removeAll(row);
+ }
+
+ row.removeAttribute('data-offer-id');
+ row.removeAttribute('data-bid-id');
+
+ while (row.firstChild) {
+ const child = row.firstChild;
+ row.removeChild(child);
+ }
+}
+
+function optimizeMemoryUsage() {
+ const MAX_BIDS_IN_MEMORY = 500;
- initialize() {
- this.connect();
- this.startHealthCheck();
- },
+ ['sent', 'received'].forEach(type => {
+ if (state.data[type] && state.data[type].length > MAX_BIDS_IN_MEMORY) {
+ console.log(`Trimming ${type} bids data from ${state.data[type].length} to ${MAX_BIDS_IN_MEMORY}`);
+ state.data[type] = state.data[type].slice(0, MAX_BIDS_IN_MEMORY);
+ }
+ });
- isConnected() {
- return this.ws?.readyState === WebSocket.OPEN;
- },
+ cleanupOffscreenTooltips();
- connect() {
- if (this.isConnected() || this.isPaused) return;
-
- if (this.ws) {
- this.cleanupConnection();
+ if (window.IdentityManager && typeof IdentityManager.limitCacheSize === 'function') {
+ IdentityManager.limitCacheSize(100);
}
- try {
-
- let wsPort;
-
- if (typeof getWebSocketConfig === 'function') {
- const wsConfig = getWebSocketConfig();
- wsPort = wsConfig?.port || wsConfig?.fallbackPort;
- }
-
- if (!wsPort && window.config?.port) {
- wsPort = window.config.port;
- }
-
- if (!wsPort) {
- wsPort = window.ws_port || '11700';
- }
-
- console.log("Using WebSocket port:", wsPort);
- this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
- this.setupEventHandlers();
- } catch (error) {
- console.error('WebSocket connection error:', error);
- this.handleReconnect();
+ if (window.MemoryManager) {
+ MemoryManager.forceCleanup();
}
-},
+}
- setupEventHandlers() {
- if (!this.ws) return;
-
- this.ws.onopen = () => {
- state.wsConnected = true;
- this.reconnectAttempts = 0;
- this.lastMessageTime = Date.now();
- updateConnectionStatus('connected');
- console.log('🟢 WebSocket connection established for Sent Bids / Received Bids');
- updateBidsTable();
- };
-
- this.ws.onmessage = () => {
- this.lastMessageTime = Date.now();
- if (this.isPaused) return;
-
- if (!this.processingQueue) {
- this.processingQueue = true;
- setTimeout(async () => {
- try {
- if (!state.isRefreshing) {
- await updateBidsTable();
- }
- } finally {
- this.processingQueue = false;
- }
- }, 200);
- }
- };
-
- this.ws.onclose = () => {
- state.wsConnected = false;
- updateConnectionStatus('disconnected');
- if (!this.isPaused) {
- this.handleReconnect();
- }
- };
-
- this.ws.onerror = () => {
- updateConnectionStatus('error');
- };
- },
-
- startHealthCheck() {
- this.stopHealthCheck();
-
- this.healthCheckInterval = setInterval(() => {
- if (this.isPaused) return;
-
- const timeSinceLastMessage = Date.now() - this.lastMessageTime;
- if (timeSinceLastMessage > 120000) {
- console.log('WebSocket connection appears stale. Reconnecting...');
- this.cleanupConnection();
- this.connect();
- return;
- }
-
- if (!this.isConnected()) {
- this.handleReconnect();
- }
- }, 30000);
- },
-
- stopHealthCheck() {
- if (this.healthCheckInterval) {
- clearInterval(this.healthCheckInterval);
- this.healthCheckInterval = null;
- }
- },
-
- handleReconnect() {
- if (this.reconnectTimeout) {
- clearTimeout(this.reconnectTimeout);
- this.reconnectTimeout = null;
- }
-
- if (this.isPaused) return;
-
- this.reconnectAttempts++;
- if (this.reconnectAttempts <= this.maxReconnectAttempts) {
- const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
- //console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
- this.reconnectTimeout = setTimeout(() => this.connect(), delay);
- } else {
- updateConnectionStatus('error');
- //console.log('Maximum reconnection attempts reached. Will try again in 60 seconds.');
- setTimeout(() => {
- this.reconnectAttempts = 0;
- this.connect();
- }, 60000);
- }
- },
-
- cleanupConnection() {
- if (this.ws) {
- this.ws.onopen = null;
- this.ws.onmessage = null;
- this.ws.onclose = null;
- this.ws.onerror = null;
- if (this.ws.readyState === WebSocket.OPEN) {
- try {
- this.ws.close(1000, 'Cleanup');
- } catch (e) {
- console.warn('Error closing WebSocket:', e);
- }
- }
- this.ws = null;
- }
- },
-
- pause() {
- this.isPaused = true;
- //console.log('WebSocket operations paused');
- if (this.reconnectTimeout) {
- clearTimeout(this.reconnectTimeout);
- this.reconnectTimeout = null;
- }
- },
-
- resume() {
- if (!this.isPaused) return;
- this.isPaused = false;
- //console.log('WebSocket operations resumed');
- this.lastMessageTime = Date.now();
- if (!this.isConnected()) {
- this.reconnectAttempts = 0;
- this.connect();
- }
- },
-
- cleanup() {
- this.isPaused = true;
- this.stopHealthCheck();
- if (this.reconnectTimeout) {
- clearTimeout(this.reconnectTimeout);
- this.reconnectTimeout = null;
- }
- this.cleanupConnection();
- }
-};
-
-// Core
const safeParseInt = (value) => {
const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed;
@@ -528,7 +490,6 @@ function coinMatches(offerCoin, filterCoin) {
return false;
}
-// State
function hasActiveFilters() {
const coinFromSelect = document.getElementById('coin_from');
const coinToSelect = document.getElementById('coin_to');
@@ -596,11 +557,58 @@ function filterAndSortData(bids) {
const searchStr = state.filters.searchQuery.toLowerCase();
const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr);
const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr);
- const identity = IdentityManager.cache.get(bid.addr_from);
- const label = identity?.data?.label || '';
+
+ let label = '';
+ try {
+ if (window.IdentityManager) {
+
+ let identity = null;
+
+ if (IdentityManager.cache && typeof IdentityManager.cache.get === 'function') {
+ identity = IdentityManager.cache.get(bid.addr_from);
+ }
+
+ if (identity && identity.label) {
+ label = identity.label;
+ } else if (identity && identity.data && identity.data.label) {
+ label = identity.data.label;
+ }
+
+ if (!label && bid.identity) {
+ label = bid.identity.label || '';
+ }
+ }
+ } catch (e) {
+ console.warn('Error accessing identity for search:', e);
+ }
+
const matchesLabel = label.toLowerCase().includes(searchStr);
- if (!(matchesBidId || matchesIdentity || matchesLabel)) {
+ let matchesDisplayedLabel = false;
+ if (!matchesLabel && document) {
+ try {
+ const tableId = state.currentTab === 'sent' ? 'sent' : 'received';
+ const cells = document.querySelectorAll(`#${tableId} a[href^="/identity/"]`);
+
+ for (const cell of cells) {
+
+ const href = cell.getAttribute('href');
+ const cellAddress = href ? href.split('/').pop() : '';
+
+ if (cellAddress === bid.addr_from) {
+ const cellText = cell.textContent.trim().toLowerCase();
+ if (cellText.includes(searchStr)) {
+ matchesDisplayedLabel = true;
+ break;
+ }
+ }
+ }
+ } catch (e) {
+ console.warn('Error checking displayed labels:', e);
+ }
+ }
+
+ if (!(matchesBidId || matchesIdentity || matchesLabel || matchesDisplayedLabel)) {
return false;
}
}
@@ -615,6 +623,37 @@ function filterAndSortData(bids) {
});
}
+async function preloadIdentitiesForSearch(bids) {
+ if (!window.IdentityManager || typeof IdentityManager.getIdentityData !== 'function') {
+ return;
+ }
+
+ try {
+ const addresses = new Set();
+ bids.forEach(bid => {
+ if (bid.addr_from) {
+ addresses.add(bid.addr_from);
+ }
+ });
+
+ const BATCH_SIZE = 20;
+ const addressArray = Array.from(addresses);
+
+ for (let i = 0; i < addressArray.length; i += BATCH_SIZE) {
+ const batch = addressArray.slice(i, i + BATCH_SIZE);
+ await Promise.all(batch.map(addr => IdentityManager.getIdentityData(addr)));
+
+ if (i + BATCH_SIZE < addressArray.length) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+ }
+
+ console.log(`Preloaded ${addressArray.length} identities for search`);
+ } catch (error) {
+ console.error('Error preloading identities:', error);
+ }
+}
+
function updateCoinFilterImages() {
const coinToSelect = document.getElementById('coin_to');
const coinFromSelect = document.getElementById('coin_from');
@@ -709,108 +748,6 @@ const updateConnectionStatus = (status) => {
});
};
-// Identity
-const IdentityManager = {
- cache: new Map(),
- pendingRequests: new Map(),
- retryDelay: 2000,
- maxRetries: 3,
- cacheTimeout: 5 * 60 * 1000,
- maxCacheSize: 500,
-
- async getIdentityData(address) {
- if (!address) return { address: '' };
-
- const cachedData = this.getCachedIdentity(address);
- if (cachedData) return { ...cachedData, address };
-
- if (this.pendingRequests.has(address)) {
- try {
- const pendingData = await this.pendingRequests.get(address);
- return { ...pendingData, address };
- } catch (error) {
- this.pendingRequests.delete(address);
- }
- }
-
- const request = this.fetchWithRetry(address);
- this.pendingRequests.set(address, request);
-
- try {
- const data = await request;
-
- this.trimCacheIfNeeded();
-
- this.cache.set(address, {
- data,
- timestamp: Date.now()
- });
-
- return { ...data, address };
- } catch (error) {
- console.warn(`Error fetching identity for ${address}:`, error);
- return { address };
- } finally {
- this.pendingRequests.delete(address);
- }
- },
-
- getCachedIdentity(address) {
- const cached = this.cache.get(address);
- if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
- cached.timestamp = Date.now();
- return cached.data;
- }
- if (cached) {
- this.cache.delete(address);
- }
- return null;
- },
-
- trimCacheIfNeeded() {
- if (this.cache.size > this.maxCacheSize) {
-
- const entries = Array.from(this.cache.entries());
- const sortedByAge = entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
-
- const toRemove = Math.ceil(this.maxCacheSize * 0.2);
- for (let i = 0; i < toRemove && i < sortedByAge.length; i++) {
- this.cache.delete(sortedByAge[i][0]);
- }
- console.log(`Trimmed identity cache: removed ${toRemove} oldest entries`);
- }
- },
-
- clearCache() {
- this.cache.clear();
- this.pendingRequests.clear();
- },
-
- async fetchWithRetry(address, attempt = 1) {
- try {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 10000);
-
- const response = await fetch(`/json/identities/${address}`, {
- signal: controller.signal
- });
-
- clearTimeout(timeoutId);
-
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
- return await response.json();
- } catch (error) {
- if (attempt >= this.maxRetries) {
- console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
- return { address };
- }
- await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
- return this.fetchWithRetry(address, attempt + 1);
- }
- }
-};
-
-// Stats
const processIdentityStats = (identity) => {
if (!identity) return null;
@@ -910,7 +847,6 @@ const createIdentityTooltipContent = (identity) => {
`;
};
-// Table
let tooltipIdsToCleanup = new Set();
const cleanupTooltips = () => {
@@ -1097,14 +1033,14 @@ const createTableRow = async (bid) => {
-
+
${bid.bid_state}
|
-
+ |
@@ -1357,7 +1293,6 @@ function implementVirtualizedRows() {
});
}
-// Fetching
let activeFetchController = null;
const fetchBids = async () => {
@@ -1432,21 +1367,20 @@ const fetchBids = async () => {
const updateBidsTable = async () => {
if (state.isLoading) {
- //console.log('Already loading, skipping update');
return;
}
try {
- //console.log('Starting updateBidsTable for tab:', state.currentTab);
- //console.log('Current filters:', state.filters);
-
state.isLoading = true;
updateLoadingState(true);
const bids = await fetchBids();
-
- //console.log('Fetched bids:', bids.length);
-
+
+ // Add identity preloading if we're searching
+ if (state.filters.searchQuery && state.filters.searchQuery.length > 0) {
+ await preloadIdentitiesForSearch(bids);
+ }
+
state.data[state.currentTab] = bids;
state.currentPage[state.currentTab] = 1;
@@ -1503,7 +1437,6 @@ const updatePaginationControls = (type) => {
}
};
-// Filter
let searchTimeout;
function handleSearch(event) {
if (searchTimeout) {
@@ -1708,7 +1641,6 @@ const setupRefreshButtons = () => {
});
};
-// Tabs
const switchTab = (tabId) => {
if (state.isLoading) return;
@@ -1925,15 +1857,22 @@ function setupMemoryMonitoring() {
const intervalId = setInterval(() => {
if (document.hidden) {
console.log('Tab hidden - running memory optimization');
- IdentityManager.trimCacheIfNeeded();
- if (window.TooltipManager) {
+
+ if (window.IdentityManager) {
+ if (typeof IdentityManager.limitCacheSize === 'function') {
+ IdentityManager.limitCacheSize(100);
+ }
+ }
+
+ if (window.TooltipManager && typeof window.TooltipManager.cleanup === 'function') {
window.TooltipManager.cleanup();
}
+
if (state.data.sent.length > 1000) {
console.log('Trimming sent bids data');
state.data.sent = state.data.sent.slice(0, 1000);
}
-
+
if (state.data.received.length > 1000) {
console.log('Trimming received bids data');
state.data.received = state.data.received.slice(0, 1000);
@@ -1942,6 +1881,7 @@ function setupMemoryMonitoring() {
cleanupTooltips();
}
}, MEMORY_CHECK_INTERVAL);
+
document.addEventListener('beforeunload', () => {
clearInterval(intervalId);
}, { once: true });
@@ -1985,6 +1925,12 @@ function initialize() {
updateBidsTable();
}, 100);
+ setInterval(() => {
+ if ((state.data.sent.length + state.data.received.length) > 1000) {
+ optimizeMemoryUsage();
+ }
+ }, 5 * 60 * 1000); // Check every 5 minutes
+
window.cleanupBidsTable = cleanup;
}
diff --git a/basicswap/static/js/bids_export.js b/basicswap/static/js/bids_sentreceived_export.js
similarity index 100%
rename from basicswap/static/js/bids_export.js
rename to basicswap/static/js/bids_sentreceived_export.js
diff --git a/basicswap/static/js/coin_icons.js b/basicswap/static/js/coin_icons.js
deleted file mode 100644
index f8db7e7..0000000
--- a/basicswap/static/js/coin_icons.js
+++ /dev/null
@@ -1,68 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
-
- const selectCache = {};
-
- function updateSelectCache(select) {
- const selectedOption = select.options[select.selectedIndex];
- const image = selectedOption.getAttribute('data-image');
- const name = selectedOption.textContent.trim();
- selectCache[select.id] = { image, name };
- }
-
- function setSelectData(select) {
- const selectedOption = select.options[select.selectedIndex];
- const image = selectedOption.getAttribute('data-image') || '';
- const name = selectedOption.textContent.trim();
- select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
-
- const selectImage = select.nextElementSibling.querySelector('.select-image');
- if (selectImage) {
- selectImage.src = image;
- }
-
- const selectNameElement = select.nextElementSibling.querySelector('.select-name');
- if (selectNameElement) {
- selectNameElement.textContent = name;
- }
-
- updateSelectCache(select);
- }
-
- const selectIcons = document.querySelectorAll('.custom-select .select-icon');
- const selectImages = document.querySelectorAll('.custom-select .select-image');
- const selectNames = document.querySelectorAll('.custom-select .select-name');
-
- selectIcons.forEach(icon => icon.style.display = 'none');
- selectImages.forEach(image => image.style.display = 'none');
- selectNames.forEach(name => name.style.display = 'none');
-
- function setupCustomSelect(select) {
- const options = select.querySelectorAll('option');
- const selectIcon = select.parentElement.querySelector('.select-icon');
- const selectImage = select.parentElement.querySelector('.select-image');
-
- options.forEach(option => {
- const image = option.getAttribute('data-image');
- if (image) {
- option.style.backgroundImage = `url(${image})`;
- }
- });
-
- const storedValue = localStorage.getItem(select.name);
- if (storedValue && select.value == '-1') {
- select.value = storedValue;
- }
-
- select.addEventListener('change', () => {
- setSelectData(select);
- localStorage.setItem(select.name, select.value);
- });
-
- setSelectData(select);
- selectIcon.style.display = 'none';
- selectImage.style.display = 'none';
- }
-
- const customSelects = document.querySelectorAll('.custom-select select');
- customSelects.forEach(setupCustomSelect);
-});
\ No newline at end of file
diff --git a/basicswap/static/js/global.js b/basicswap/static/js/global.js
new file mode 100644
index 0000000..5892e98
--- /dev/null
+++ b/basicswap/static/js/global.js
@@ -0,0 +1,199 @@
+document.addEventListener('DOMContentLoaded', function() {
+ const burger = document.querySelectorAll('.navbar-burger');
+ const menu = document.querySelectorAll('.navbar-menu');
+
+ if (burger.length && menu.length) {
+ for (var i = 0; i < burger.length; i++) {
+ burger[i].addEventListener('click', function() {
+ for (var j = 0; j < menu.length; j++) {
+ menu[j].classList.toggle('hidden');
+ }
+ });
+ }
+ }
+
+ const close = document.querySelectorAll('.navbar-close');
+ const backdrop = document.querySelectorAll('.navbar-backdrop');
+
+ if (close.length) {
+ for (var k = 0; k < close.length; k++) {
+ close[k].addEventListener('click', function() {
+ for (var j = 0; j < menu.length; j++) {
+ menu[j].classList.toggle('hidden');
+ }
+ });
+ }
+ }
+
+ if (backdrop.length) {
+ for (var l = 0; l < backdrop.length; l++) {
+ backdrop[l].addEventListener('click', function() {
+ for (var j = 0; j < menu.length; j++) {
+ menu[j].classList.toggle('hidden');
+ }
+ });
+ }
+ }
+
+ const tooltipManager = TooltipManager.initialize();
+ tooltipManager.initializeTooltips();
+ setupShutdownModal();
+ setupDarkMode();
+ toggleImages();
+});
+
+function setupShutdownModal() {
+ const shutdownButtons = document.querySelectorAll('.shutdown-button');
+ const shutdownModal = document.getElementById('shutdownModal');
+ const closeModalButton = document.getElementById('closeShutdownModal');
+ const confirmShutdownButton = document.getElementById('confirmShutdown');
+ const shutdownWarning = document.getElementById('shutdownWarning');
+
+ function updateShutdownButtons() {
+ const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
+ shutdownButtons.forEach(button => {
+ if (activeSwaps > 0) {
+ button.classList.add('shutdown-disabled');
+ button.setAttribute('data-disabled', 'true');
+ button.setAttribute('title', 'Caution: Swaps in progress');
+ } else {
+ button.classList.remove('shutdown-disabled');
+ button.removeAttribute('data-disabled');
+ button.removeAttribute('title');
+ }
+ });
+ }
+
+ function closeAllDropdowns() {
+
+ const openDropdowns = document.querySelectorAll('.dropdown-menu:not(.hidden)');
+ openDropdowns.forEach(dropdown => {
+ if (dropdown.style.display !== 'none') {
+ dropdown.style.display = 'none';
+ }
+ });
+
+ if (window.Dropdown && window.Dropdown.instances) {
+ window.Dropdown.instances.forEach(instance => {
+ if (instance._visible) {
+ instance.hide();
+ }
+ });
+ }
+ }
+
+ function showShutdownModal() {
+ closeAllDropdowns();
+
+ const activeSwaps = parseInt(shutdownButtons[0].getAttribute('data-active-swaps') || '0');
+ if (activeSwaps > 0) {
+ shutdownWarning.classList.remove('hidden');
+ confirmShutdownButton.textContent = 'Yes, Shut Down Anyway';
+ } else {
+ shutdownWarning.classList.add('hidden');
+ confirmShutdownButton.textContent = 'Yes, Shut Down';
+ }
+ shutdownModal.classList.remove('hidden');
+ document.body.style.overflow = 'hidden';
+ }
+
+ function hideShutdownModal() {
+ shutdownModal.classList.add('hidden');
+ document.body.style.overflow = '';
+ }
+
+ if (shutdownButtons.length) {
+ shutdownButtons.forEach(button => {
+ button.addEventListener('click', function(e) {
+ e.preventDefault();
+ showShutdownModal();
+ });
+ });
+ }
+
+ if (closeModalButton) {
+ closeModalButton.addEventListener('click', hideShutdownModal);
+ }
+
+ if (confirmShutdownButton) {
+ confirmShutdownButton.addEventListener('click', function() {
+ const shutdownToken = document.querySelector('.shutdown-button')
+ .getAttribute('href').split('/').pop();
+ window.location.href = '/shutdown/' + shutdownToken;
+ });
+ }
+
+ if (shutdownModal) {
+ shutdownModal.addEventListener('click', function(e) {
+ if (e.target === this) {
+ hideShutdownModal();
+ }
+ });
+ }
+
+ if (shutdownButtons.length) {
+ updateShutdownButtons();
+ }
+}
+
+function setupDarkMode() {
+ const themeToggle = document.getElementById('theme-toggle');
+ const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
+ const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
+
+ if (themeToggleDarkIcon && themeToggleLightIcon) {
+ if (localStorage.getItem('color-theme') === 'dark' ||
+ (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
+ themeToggleLightIcon.classList.remove('hidden');
+ } else {
+ themeToggleDarkIcon.classList.remove('hidden');
+ }
+ }
+
+ function setTheme(theme) {
+ if (theme === 'light') {
+ document.documentElement.classList.remove('dark');
+ localStorage.setItem('color-theme', 'light');
+ } else {
+ document.documentElement.classList.add('dark');
+ localStorage.setItem('color-theme', 'dark');
+ }
+ }
+
+ if (themeToggle) {
+ themeToggle.addEventListener('click', () => {
+ if (localStorage.getItem('color-theme') === 'dark') {
+ setTheme('light');
+ } else {
+ setTheme('dark');
+ }
+
+ if (themeToggleDarkIcon && themeToggleLightIcon) {
+ themeToggleDarkIcon.classList.toggle('hidden');
+ themeToggleLightIcon.classList.toggle('hidden');
+ }
+
+ toggleImages();
+ });
+ }
+}
+
+function toggleImages() {
+ var html = document.querySelector('html');
+ var darkImages = document.querySelectorAll('.dark-image');
+ var lightImages = document.querySelectorAll('.light-image');
+
+ if (html && html.classList.contains('dark')) {
+ toggleImageDisplay(darkImages, 'block');
+ toggleImageDisplay(lightImages, 'none');
+ } else {
+ toggleImageDisplay(darkImages, 'none');
+ toggleImageDisplay(lightImages, 'block');
+ }
+}
+
+function toggleImageDisplay(images, display) {
+ images.forEach(function(img) {
+ img.style.display = display;
+ });
+}
diff --git a/basicswap/static/js/main.js b/basicswap/static/js/main.js
deleted file mode 100644
index 38f6e4e..0000000
--- a/basicswap/static/js/main.js
+++ /dev/null
@@ -1,40 +0,0 @@
-// Burger menus
-document.addEventListener('DOMContentLoaded', function() {
- // open
- const burger = document.querySelectorAll('.navbar-burger');
- const menu = document.querySelectorAll('.navbar-menu');
-
- if (burger.length && menu.length) {
- for (var i = 0; i < burger.length; i++) {
- burger[i].addEventListener('click', function() {
- for (var j = 0; j < menu.length; j++) {
- menu[j].classList.toggle('hidden');
- }
- });
- }
- }
-
- // close
- const close = document.querySelectorAll('.navbar-close');
- const backdrop = document.querySelectorAll('.navbar-backdrop');
-
- if (close.length) {
- for (var k = 0; k < close.length; k++) {
- close[k].addEventListener('click', function() {
- for (var j = 0; j < menu.length; j++) {
- menu[j].classList.toggle('hidden');
- }
- });
- }
- }
-
- if (backdrop.length) {
- for (var l = 0; l < backdrop.length; l++) {
- backdrop[l].addEventListener('click', function() {
- for (var j = 0; j < menu.length; j++) {
- menu[j].classList.toggle('hidden');
- }
- });
- }
- }
-});
diff --git a/basicswap/static/js/modules/api-manager.js b/basicswap/static/js/modules/api-manager.js
new file mode 100644
index 0000000..2effc12
--- /dev/null
+++ b/basicswap/static/js/modules/api-manager.js
@@ -0,0 +1,389 @@
+const ApiManager = (function() {
+
+ const state = {
+ isInitialized: false
+ };
+
+ const config = {
+ 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],
+
+ canMakeRequest: function(apiName) {
+ const now = Date.now();
+ const lastRequest = this.lastRequestTime[apiName] || 0;
+ return (now - lastRequest) >= this.minRequestInterval[apiName];
+ },
+
+ updateLastRequestTime: function(apiName) {
+ this.lastRequestTime[apiName] = Date.now();
+ },
+
+ getWaitTime: function(apiName) {
+ const now = Date.now();
+ const lastRequest = this.lastRequestTime[apiName] || 0;
+ return Math.max(0, this.minRequestInterval[apiName] - (now - lastRequest));
+ },
+
+ queueRequest: async function(apiName, requestFn, retryCount = 0) {
+ if (!this.requestQueue[apiName]) {
+ this.requestQueue[apiName] = Promise.resolve();
+ }
+
+ try {
+ await this.requestQueue[apiName];
+
+ const executeRequest = async () => {
+ const waitTime = this.getWaitTime(apiName);
+ if (waitTime > 0) {
+ await new Promise(resolve => setTimeout(resolve, waitTime));
+ }
+
+ try {
+ this.updateLastRequestTime(apiName);
+ return await requestFn();
+ } catch (error) {
+ 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));
+ 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];
+ console.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
+ apiName,
+ retryCount,
+ error: error.message
+ });
+ await new Promise(resolve => setTimeout(resolve, delay));
+ return publicAPI.rateLimiter.queueRequest(apiName, requestFn, retryCount + 1);
+ }
+
+ throw error;
+ }
+ };
+
+ this.requestQueue[apiName] = executeRequest();
+ return await this.requestQueue[apiName];
+
+ } catch (error) {
+ if (error.message.includes('429') ||
+ error.message.includes('timeout') ||
+ error.name === 'NetworkError') {
+ const cacheKey = `coinData_${apiName}`;
+ try {
+ const cachedData = JSON.parse(localStorage.getItem(cacheKey));
+ if (cachedData && cachedData.value) {
+ return cachedData.value;
+ }
+ } catch (e) {
+ console.warn('Error accessing cached data:', e);
+ }
+ }
+ throw error;
+ }
+ }
+ };
+
+ const publicAPI = {
+ config,
+ rateLimiter,
+
+ initialize: function(options = {}) {
+ if (state.isInitialized) {
+ console.warn('[ApiManager] Already initialized');
+ return this;
+ }
+
+ 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];
+ }
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('apiManager', this, (mgr) => mgr.dispose());
+ }
+
+ state.isInitialized = true;
+ console.log('ApiManager initialized');
+ return this;
+ },
+
+ makeRequest: async function(url, method = 'GET', headers = {}, body = null) {
+ try {
+ const options = {
+ method: method,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...headers
+ },
+ signal: AbortSignal.timeout(config.requestTimeout)
+ };
+
+ 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();
+ } catch (error) {
+ console.error(`Request failed for ${url}:`, error);
+ throw error;
+ }
+ },
+
+ makePostRequest: async function(url, headers = {}) {
+ return new Promise((resolve, reject) => {
+ fetch('/json/readurl', {
+ method: 'POST',
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ url: url,
+ headers: headers
+ })
+ })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ return response.json();
+ })
+ .then(data => {
+ if (data.Error) {
+ reject(new Error(data.Error));
+ } else {
+ resolve(data);
+ }
+ })
+ .catch(error => {
+ console.error(`Request failed for ${url}:`, error);
+ reject(error);
+ });
+ });
+ },
+
+ fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) {
+ if (!Array.isArray(coins)) {
+ coins = [coins];
+ }
+
+ return this.makeRequest('/json/coinprices', 'POST', {}, {
+ coins: Array.isArray(coins) ? coins.join(',') : coins,
+ source: source,
+ ttl: ttl
+ });
+ },
+
+ fetchCoinGeckoData: async function() {
+ return this.rateLimiter.queueRequest('coingecko', async () => {
+ try {
+ const coins = (window.config && window.config.coins) ?
+ window.config.coins
+ .filter(coin => coin.usesCoinGecko)
+ .map(coin => coin.name)
+ .join(',') :
+ 'bitcoin,monero,particl,bitcoincash,pivx,firo,dash,litecoin,dogecoin,decred';
+
+ //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');
+ }
+
+ if (!response.rates || typeof response.rates !== 'object' || Object.keys(response.rates).length === 0) {
+ throw new Error('No valid rates found in response');
+ }
+
+ return response;
+ } catch (error) {
+ console.error('Error in fetchCoinGeckoData:', {
+ message: error.message,
+ stack: error.stack
+ });
+ throw error;
+ }
+ });
+ },
+
+ fetchVolumeData: async function() {
+ return this.rateLimiter.queueRequest('coingecko', async () => {
+ try {
+ const coins = (window.config && window.config.coins) ?
+ window.config.coins
+ .filter(coin => coin.usesCoinGecko)
+ .map(coin => getCoinBackendId ? getCoinBackendId(coin.name) : coin.name)
+ .join(',') :
+ 'bitcoin,monero,particl,bitcoin-cash,pivx,firo,dash,litecoin,dogecoin,decred';
+
+ const url = `https://api.coingecko.com/api/v3/simple/price?ids=${coins}&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 volumeData = {};
+ Object.entries(response).forEach(([coinId, data]) => {
+ if (data && data.usd_24h_vol) {
+ volumeData[coinId] = {
+ total_volume: data.usd_24h_vol,
+ price_change_percentage_24h: data.usd_24h_change || 0
+ };
+ }
+ });
+
+ return volumeData;
+ } catch (error) {
+ console.error("Error fetching volume data:", error);
+ throw error;
+ }
+ });
+ },
+
+ 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 => {
+ if (coin === 'WOW') {
+ return this.rateLimiter.queueRequest('coingecko', async () => {
+ const url = `https://api.coingecko.com/api/v3/coins/wownero/market_chart?vs_currency=usd&days=1`;
+ try {
+ const response = await this.makePostRequest(url);
+ if (response && response.prices) {
+ results[coin] = response.prices;
+ }
+ } catch (error) {
+ console.error(`Error fetching CoinGecko data for WOW:`, error);
+ throw error;
+ }
+ });
+ } else {
+ return this.rateLimiter.queueRequest('cryptocompare', async () => {
+ try {
+ const apiKey = window.config?.apiKeys?.cryptoCompare || '';
+ let url;
+
+ 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;
+ }
+ });
+ }
+ });
+
+ await Promise.all(fetchPromises);
+ return results;
+ },
+
+ dispose: function() {
+ // Clear any pending requests or resources
+ rateLimiter.requestQueue = {};
+ rateLimiter.lastRequestTime = {};
+ state.isInitialized = false;
+ console.log('ApiManager disposed');
+ }
+ };
+
+ 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;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.apiManagerInitialized) {
+ ApiManager.initialize();
+ window.apiManagerInitialized = true;
+ }
+});
+
+//console.log('ApiManager initialized with methods:', Object.keys(ApiManager));
+console.log('ApiManager initialized');
diff --git a/basicswap/static/js/modules/cache-manager.js b/basicswap/static/js/modules/cache-manager.js
new file mode 100644
index 0000000..1dec899
--- /dev/null
+++ b/basicswap/static/js/modules/cache-manager.js
@@ -0,0 +1,535 @@
+const CacheManager = (function() {
+ const defaults = window.config?.cacheConfig?.storage || {
+ maxSizeBytes: 10 * 1024 * 1024,
+ maxItems: 200,
+ defaultTTL: 5 * 60 * 1000
+ };
+
+ const PRICES_CACHE_KEY = 'crypto_prices_unified';
+
+ const CACHE_KEY_PATTERNS = [
+ 'coinData_',
+ 'chartData_',
+ 'historical_',
+ 'rates_',
+ 'prices_',
+ 'offers_',
+ 'fallback_',
+ 'volumeData'
+ ];
+
+ const isCacheKey = (key) => {
+ return CACHE_KEY_PATTERNS.some(pattern => key.startsWith(pattern)) ||
+ key === 'coinGeckoOneLiner' ||
+ key === PRICES_CACHE_KEY;
+ };
+
+ const isLocalStorageAvailable = () => {
+ try {
+ const testKey = '__storage_test__';
+ localStorage.setItem(testKey, testKey);
+ localStorage.removeItem(testKey);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ };
+
+ let storageAvailable = isLocalStorageAvailable();
+
+ const memoryCache = new Map();
+
+ if (!storageAvailable) {
+ console.warn('localStorage is not available. Using in-memory cache instead.');
+ }
+
+ const cacheAPI = {
+ getTTL: function(resourceType) {
+ const ttlConfig = window.config?.cacheConfig?.ttlSettings || {};
+ return ttlConfig[resourceType] || window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
+ },
+
+ set: function(key, value, resourceTypeOrCustomTtl = null) {
+ try {
+ this.cleanup();
+
+ if (!value) {
+ console.warn('Attempted to cache null/undefined value for key:', key);
+ return false;
+ }
+
+ let ttl;
+ if (typeof resourceTypeOrCustomTtl === 'string') {
+ ttl = this.getTTL(resourceTypeOrCustomTtl);
+ } else if (typeof resourceTypeOrCustomTtl === 'number') {
+ ttl = resourceTypeOrCustomTtl;
+ } else {
+ ttl = window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL;
+ }
+
+ const item = {
+ value: value,
+ timestamp: Date.now(),
+ expiresAt: Date.now() + ttl
+ };
+
+ let serializedItem;
+ try {
+ serializedItem = JSON.stringify(item);
+ } catch (e) {
+ console.error('Failed to serialize cache item:', e);
+ return false;
+ }
+
+ const itemSize = new Blob([serializedItem]).size;
+ if (itemSize > defaults.maxSizeBytes) {
+ console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
+ return false;
+ }
+
+ if (storageAvailable) {
+ try {
+ localStorage.setItem(key, serializedItem);
+ return true;
+ } catch (storageError) {
+ if (storageError.name === 'QuotaExceededError') {
+ this.cleanup(true);
+ try {
+ localStorage.setItem(key, serializedItem);
+ return true;
+ } catch (retryError) {
+ console.error('Storage quota exceeded even after cleanup:', retryError);
+ storageAvailable = false;
+ console.warn('Switching to in-memory cache due to quota issues');
+ memoryCache.set(key, item);
+ return true;
+ }
+ } else {
+ console.error('localStorage error:', storageError);
+ storageAvailable = false;
+ console.warn('Switching to in-memory cache due to localStorage error');
+ memoryCache.set(key, item);
+ return true;
+ }
+ }
+ } else {
+ memoryCache.set(key, item);
+ if (memoryCache.size > defaults.maxItems) {
+ 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%
+
+ keysToDelete.forEach(k => memoryCache.delete(k));
+ }
+
+ return true;
+ }
+ } catch (error) {
+ console.error('Cache set error:', error);
+ try {
+ memoryCache.set(key, {
+ value: value,
+ timestamp: Date.now(),
+ expiresAt: Date.now() + (window.config?.cacheConfig?.defaultTTL || defaults.defaultTTL)
+ });
+ return true;
+ } catch (e) {
+ console.error('Memory cache set error:', e);
+ return false;
+ }
+ }
+ },
+
+ get: function(key) {
+ try {
+ if (storageAvailable) {
+ try {
+ const itemStr = localStorage.getItem(key);
+ if (itemStr) {
+ let item;
+ try {
+ item = JSON.parse(itemStr);
+ } catch (parseError) {
+ console.error('Failed to parse cached item:', parseError);
+ localStorage.removeItem(key);
+ return null;
+ }
+
+ if (!item || typeof item.expiresAt !== 'number' || !Object.prototype.hasOwnProperty.call(item, 'value')) {
+ console.warn('Invalid cache item structure for key:', key);
+ localStorage.removeItem(key);
+ return null;
+ }
+
+ const now = Date.now();
+ if (now < item.expiresAt) {
+ return {
+ value: item.value,
+ remainingTime: item.expiresAt - now
+ };
+ }
+
+ localStorage.removeItem(key);
+ return null;
+ }
+ } catch (error) {
+ console.error("localStorage access error:", error);
+ storageAvailable = false;
+ console.warn('Switching to in-memory cache due to localStorage error');
+ }
+ }
+
+ if (memoryCache.has(key)) {
+ const item = memoryCache.get(key);
+ const now = Date.now();
+
+ if (now < item.expiresAt) {
+ return {
+ value: item.value,
+ remainingTime: item.expiresAt - now
+ };
+ } else {
+
+ memoryCache.delete(key);
+ }
+ }
+
+ return null;
+ } catch (error) {
+ console.error("Cache retrieval error:", error);
+ try {
+ if (storageAvailable) {
+ localStorage.removeItem(key);
+ }
+ memoryCache.delete(key);
+ } catch (removeError) {
+ console.error("Failed to remove invalid cache entry:", removeError);
+ }
+ return null;
+ }
+ },
+
+ isValid: function(key) {
+ return this.get(key) !== null;
+ },
+
+ cleanup: function(aggressive = false) {
+ const now = Date.now();
+ let totalSize = 0;
+ let itemCount = 0;
+ const items = [];
+
+ if (storageAvailable) {
+ try {
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (!isCacheKey(key)) continue;
+
+ try {
+ const itemStr = localStorage.getItem(key);
+ const size = new Blob([itemStr]).size;
+ const item = JSON.parse(itemStr);
+
+ if (now >= item.expiresAt) {
+ localStorage.removeItem(key);
+ continue;
+ }
+
+ items.push({
+ key,
+ size,
+ expiresAt: item.expiresAt,
+ timestamp: item.timestamp
+ });
+
+ totalSize += size;
+ itemCount++;
+ } catch (error) {
+ console.error("Error processing cache item:", error);
+ localStorage.removeItem(key);
+ }
+ }
+
+ if (aggressive || totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) {
+ items.sort((a, b) => b.timestamp - a.timestamp);
+
+ while ((totalSize > defaults.maxSizeBytes || itemCount > defaults.maxItems) && items.length > 0) {
+ const item = items.pop();
+ try {
+ localStorage.removeItem(item.key);
+ totalSize -= item.size;
+ itemCount--;
+ } catch (error) {
+ console.error("Error removing cache item:", error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Error during localStorage cleanup:", error);
+ storageAvailable = false;
+ console.warn('Switching to in-memory cache due to localStorage error');
+ }
+ }
+
+ const expiredKeys = [];
+ memoryCache.forEach((item, key) => {
+ if (now >= item.expiresAt) {
+ expiredKeys.push(key);
+ }
+ });
+
+ expiredKeys.forEach(key => memoryCache.delete(key));
+
+ if (aggressive && memoryCache.size > defaults.maxItems / 2) {
+ 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
+
+ keysToDelete.forEach(key => memoryCache.delete(key));
+ }
+
+ return {
+ totalSize,
+ itemCount,
+ memoryCacheSize: memoryCache.size,
+ cleaned: items.length,
+ storageAvailable
+ };
+ },
+
+ clear: function() {
+
+ if (storageAvailable) {
+ try {
+ const keys = [];
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (isCacheKey(key)) {
+ keys.push(key);
+ }
+ }
+
+ keys.forEach(key => {
+ try {
+ localStorage.removeItem(key);
+ } catch (error) {
+ console.error("Error clearing cache item:", error);
+ }
+ });
+ } catch (error) {
+ console.error("Error clearing localStorage cache:", error);
+ storageAvailable = false;
+ }
+ }
+
+ Array.from(memoryCache.keys())
+ .filter(key => isCacheKey(key))
+ .forEach(key => memoryCache.delete(key));
+
+ console.log("Cache cleared successfully");
+ return true;
+ },
+
+ getStats: function() {
+ let totalSize = 0;
+ let itemCount = 0;
+ let expiredCount = 0;
+ const now = Date.now();
+
+ if (storageAvailable) {
+ try {
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (!isCacheKey(key)) continue;
+
+ try {
+ const itemStr = localStorage.getItem(key);
+ const size = new Blob([itemStr]).size;
+ const item = JSON.parse(itemStr);
+
+ totalSize += size;
+ itemCount++;
+
+ if (now >= item.expiresAt) {
+ expiredCount++;
+ }
+ } catch (error) {
+ console.error("Error getting cache stats:", error);
+ }
+ }
+ } catch (error) {
+ console.error("Error getting localStorage stats:", error);
+ storageAvailable = false;
+ }
+ }
+
+ let memoryCacheSize = 0;
+ let memoryCacheItems = 0;
+ let memoryCacheExpired = 0;
+
+ memoryCache.forEach((item, key) => {
+ if (isCacheKey(key)) {
+ memoryCacheItems++;
+ if (now >= item.expiresAt) {
+ memoryCacheExpired++;
+ }
+ try {
+ memoryCacheSize += new Blob([JSON.stringify(item)]).size;
+ } catch (e) {
+ }
+ }
+ });
+
+ return {
+ totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
+ itemCount,
+ expiredCount,
+ utilization: ((totalSize / defaults.maxSizeBytes) * 100).toFixed(1) + '%',
+ memoryCacheItems,
+ memoryCacheExpired,
+ memoryCacheSizeKB: (memoryCacheSize / 1024).toFixed(2),
+ storageType: storageAvailable ? 'localStorage' : 'memory'
+ };
+ },
+
+ checkStorage: function() {
+ const wasAvailable = storageAvailable;
+ storageAvailable = isLocalStorageAvailable();
+
+ if (storageAvailable && !wasAvailable && memoryCache.size > 0) {
+ console.log('localStorage is now available. Migrating memory cache...');
+ let migratedCount = 0;
+ memoryCache.forEach((item, key) => {
+ if (isCacheKey(key)) {
+ try {
+ localStorage.setItem(key, JSON.stringify(item));
+ memoryCache.delete(key);
+ migratedCount++;
+ } catch (e) {
+ if (e.name === 'QuotaExceededError') {
+ console.warn('Storage quota exceeded during migration. Keeping items in memory cache.');
+ return false;
+ }
+ }
+ }
+ });
+
+ console.log(`Migrated ${migratedCount} items from memory cache to localStorage.`);
+ }
+
+ return {
+ available: storageAvailable,
+ type: storageAvailable ? 'localStorage' : 'memory'
+ };
+ }
+ };
+
+ const publicAPI = {
+ ...cacheAPI,
+
+ setPrices: function(priceData, customTtl = null) {
+ return this.set(PRICES_CACHE_KEY, priceData,
+ customTtl || (typeof customTtl === 'undefined' ? 'prices' : null));
+ },
+
+ getPrices: function() {
+ return this.get(PRICES_CACHE_KEY);
+ },
+
+ getCoinPrice: function(symbol) {
+ const prices = this.getPrices();
+ if (!prices || !prices.value) {
+ return null;
+ }
+
+ const normalizedSymbol = symbol.toLowerCase();
+ return prices.value[normalizedSymbol] || null;
+ },
+
+ getCompatiblePrices: function(format) {
+ const prices = this.getPrices();
+ if (!prices || !prices.value) {
+ return null;
+ }
+
+ switch(format) {
+ case 'rates':
+ const ratesFormat = {};
+ Object.entries(prices.value).forEach(([coin, data]) => {
+ const coinKey = coin.replace(/-/g, ' ')
+ .split(' ')
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ')
+ .toLowerCase()
+ .replace(' ', '-');
+
+ ratesFormat[coinKey] = {
+ usd: data.price || data.usd,
+ btc: data.price_btc || data.btc
+ };
+ });
+ return {
+ value: ratesFormat,
+ remainingTime: prices.remainingTime
+ };
+
+ case 'coinGecko':
+ const geckoFormat = {};
+ Object.entries(prices.value).forEach(([coin, data]) => {
+ const symbol = this.getSymbolFromCoinId(coin);
+ if (symbol) {
+ geckoFormat[symbol.toLowerCase()] = {
+ current_price: data.price || data.usd,
+ price_btc: data.price_btc || data.btc,
+ total_volume: data.total_volume,
+ price_change_percentage_24h: data.price_change_percentage_24h,
+ displayName: symbol
+ };
+ }
+ });
+ return {
+ value: geckoFormat,
+ remainingTime: prices.remainingTime
+ };
+
+ default:
+ return prices;
+ }
+ },
+
+ getSymbolFromCoinId: function(coinId) {
+ const symbolMap = {
+ 'bitcoin': 'BTC',
+ 'litecoin': 'LTC',
+ 'monero': 'XMR',
+ 'particl': 'PART',
+ 'pivx': 'PIVX',
+ 'firo': 'FIRO',
+ 'zcoin': 'FIRO',
+ 'dash': 'DASH',
+ 'decred': 'DCR',
+ 'wownero': 'WOW',
+ 'bitcoin-cash': 'BCH',
+ 'dogecoin': 'DOGE'
+ };
+
+ return symbolMap[coinId] || null;
+ }
+ };
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('cacheManager', publicAPI, (cm) => {
+ cm.clear();
+ });
+ }
+
+ return publicAPI;
+})();
+
+window.CacheManager = CacheManager;
+
+
+//console.log('CacheManager initialized with methods:', Object.keys(CacheManager));
+console.log('CacheManager initialized');
diff --git a/basicswap/static/js/modules/cleanup-manager.js b/basicswap/static/js/modules/cleanup-manager.js
new file mode 100644
index 0000000..0615fca
--- /dev/null
+++ b/basicswap/static/js/modules/cleanup-manager.js
@@ -0,0 +1,270 @@
+const CleanupManager = (function() {
+
+ const state = {
+ eventListeners: [],
+ timeouts: [],
+ intervals: [],
+ animationFrames: [],
+ resources: new Map(),
+ debug: false
+ };
+
+ function log(message, ...args) {
+ if (state.debug) {
+ console.log(`[CleanupManager] ${message}`, ...args);
+ }
+ }
+
+ const publicAPI = {
+ addListener: function(element, type, handler, options = false) {
+ if (!element) {
+ log('Warning: Attempted to add listener to null/undefined element');
+ return handler;
+ }
+
+ element.addEventListener(type, handler, options);
+ state.eventListeners.push({ element, type, handler, options });
+ log(`Added ${type} listener to`, element);
+ return handler;
+ },
+
+ setTimeout: function(callback, delay) {
+ const id = window.setTimeout(callback, delay);
+ state.timeouts.push(id);
+ log(`Created timeout ${id} with ${delay}ms delay`);
+ return id;
+ },
+
+ setInterval: function(callback, delay) {
+ const id = window.setInterval(callback, delay);
+ state.intervals.push(id);
+ log(`Created interval ${id} with ${delay}ms delay`);
+ return id;
+ },
+
+ requestAnimationFrame: function(callback) {
+ const id = window.requestAnimationFrame(callback);
+ state.animationFrames.push(id);
+ log(`Requested animation frame ${id}`);
+ return id;
+ },
+
+ registerResource: function(type, resource, cleanupFn) {
+ const id = `${type}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
+ state.resources.set(id, { resource, cleanupFn });
+ log(`Registered custom resource ${id} of type ${type}`);
+ return id;
+ },
+
+ unregisterResource: function(id) {
+ const resourceInfo = state.resources.get(id);
+ if (resourceInfo) {
+ try {
+ resourceInfo.cleanupFn(resourceInfo.resource);
+ state.resources.delete(id);
+ log(`Unregistered and cleaned up resource ${id}`);
+ return true;
+ } catch (error) {
+ console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
+ return false;
+ }
+ }
+ log(`Resource ${id} not found`);
+ return false;
+ },
+
+ clearTimeout: function(id) {
+ const index = state.timeouts.indexOf(id);
+ if (index !== -1) {
+ window.clearTimeout(id);
+ state.timeouts.splice(index, 1);
+ log(`Cleared timeout ${id}`);
+ }
+ },
+
+ clearInterval: function(id) {
+ const index = state.intervals.indexOf(id);
+ if (index !== -1) {
+ window.clearInterval(id);
+ state.intervals.splice(index, 1);
+ log(`Cleared interval ${id}`);
+ }
+ },
+
+ cancelAnimationFrame: function(id) {
+ const index = state.animationFrames.indexOf(id);
+ if (index !== -1) {
+ window.cancelAnimationFrame(id);
+ state.animationFrames.splice(index, 1);
+ log(`Cancelled animation frame ${id}`);
+ }
+ },
+
+ removeListener: function(element, type, handler, options = false) {
+ if (!element) return;
+
+ try {
+ element.removeEventListener(type, handler, options);
+ log(`Removed ${type} listener from`, element);
+ } catch (error) {
+ console.error(`[CleanupManager] Error removing event listener:`, error);
+ }
+
+ state.eventListeners = state.eventListeners.filter(
+ listener => !(listener.element === element &&
+ listener.type === type &&
+ listener.handler === handler)
+ );
+ },
+
+ removeListenersByElement: function(element) {
+ if (!element) return;
+
+ const listenersToRemove = state.eventListeners.filter(
+ listener => listener.element === element
+ );
+
+ listenersToRemove.forEach(({ element, type, handler, options }) => {
+ try {
+ element.removeEventListener(type, handler, options);
+ log(`Removed ${type} listener from`, element);
+ } catch (error) {
+ console.error(`[CleanupManager] Error removing event listener:`, error);
+ }
+ });
+
+ state.eventListeners = state.eventListeners.filter(
+ listener => listener.element !== element
+ );
+ },
+
+ clearAllTimeouts: function() {
+ state.timeouts.forEach(id => {
+ window.clearTimeout(id);
+ });
+ const count = state.timeouts.length;
+ state.timeouts = [];
+ log(`Cleared all timeouts (${count})`);
+ },
+
+ clearAllIntervals: function() {
+ state.intervals.forEach(id => {
+ window.clearInterval(id);
+ });
+ const count = state.intervals.length;
+ state.intervals = [];
+ log(`Cleared all intervals (${count})`);
+ },
+
+ clearAllAnimationFrames: function() {
+ state.animationFrames.forEach(id => {
+ window.cancelAnimationFrame(id);
+ });
+ const count = state.animationFrames.length;
+ state.animationFrames = [];
+ log(`Cancelled all animation frames (${count})`);
+ },
+
+ clearAllResources: function() {
+ let successCount = 0;
+ let errorCount = 0;
+
+ state.resources.forEach((resourceInfo, id) => {
+ try {
+ resourceInfo.cleanupFn(resourceInfo.resource);
+ successCount++;
+ } catch (error) {
+ console.error(`[CleanupManager] Error cleaning up resource ${id}:`, error);
+ errorCount++;
+ }
+ });
+
+ state.resources.clear();
+ log(`Cleared all custom resources (${successCount} success, ${errorCount} errors)`);
+ },
+
+ clearAllListeners: function() {
+ state.eventListeners.forEach(({ element, type, handler, options }) => {
+ if (element) {
+ try {
+ element.removeEventListener(type, handler, options);
+ } catch (error) {
+ console.error(`[CleanupManager] Error removing event listener:`, error);
+ }
+ }
+ });
+ const count = state.eventListeners.length;
+ state.eventListeners = [];
+ log(`Removed all event listeners (${count})`);
+ },
+
+ clearAll: function() {
+ const counts = {
+ listeners: state.eventListeners.length,
+ timeouts: state.timeouts.length,
+ intervals: state.intervals.length,
+ animationFrames: state.animationFrames.length,
+ resources: state.resources.size
+ };
+
+ this.clearAllListeners();
+ this.clearAllTimeouts();
+ this.clearAllIntervals();
+ this.clearAllAnimationFrames();
+ this.clearAllResources();
+
+ log(`All resources cleaned up:`, counts);
+ return counts;
+ },
+
+ getResourceCounts: function() {
+ return {
+ listeners: state.eventListeners.length,
+ timeouts: state.timeouts.length,
+ intervals: state.intervals.length,
+ animationFrames: state.animationFrames.length,
+ resources: state.resources.size,
+ total: state.eventListeners.length +
+ state.timeouts.length +
+ state.intervals.length +
+ state.animationFrames.length +
+ state.resources.size
+ };
+ },
+
+ setDebugMode: function(enabled) {
+ state.debug = Boolean(enabled);
+ log(`Debug mode ${state.debug ? 'enabled' : 'disabled'}`);
+ return state.debug;
+ },
+
+ dispose: function() {
+ this.clearAll();
+ log('CleanupManager disposed');
+ },
+
+ initialize: function(options = {}) {
+ if (options.debug !== undefined) {
+ this.setDebugMode(options.debug);
+ }
+ log('CleanupManager initialized');
+ return this;
+ }
+ };
+
+ return publicAPI;
+})();
+
+
+window.CleanupManager = CleanupManager;
+
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.cleanupManagerInitialized) {
+ CleanupManager.initialize();
+ window.cleanupManagerInitialized = true;
+ }
+});
+
+//console.log('CleanupManager initialized with methods:', Object.keys(CleanupManager));
+console.log('CleanupManager initialized');
diff --git a/basicswap/static/js/modules/config-manager.js b/basicswap/static/js/modules/config-manager.js
new file mode 100644
index 0000000..e77c55e
--- /dev/null
+++ b/basicswap/static/js/modules/config-manager.js
@@ -0,0 +1,414 @@
+const ConfigManager = (function() {
+ const state = {
+ isInitialized: false
+ };
+
+ function determineWebSocketPort() {
+ const wsPort =
+ window.ws_port ||
+ (typeof getWebSocketConfig === 'function' ? getWebSocketConfig().port : null) ||
+ '11700';
+ return wsPort;
+ }
+
+ const selectedWsPort = determineWebSocketPort();
+
+ const defaultConfig = {
+ cacheDuration: 10 * 60 * 1000,
+ requestTimeout: 60000,
+ wsPort: selectedWsPort,
+
+ cacheConfig: {
+ defaultTTL: 10 * 60 * 1000,
+
+ ttlSettings: {
+ prices: 5 * 60 * 1000,
+ chart: 5 * 60 * 1000,
+ historical: 60 * 60 * 1000,
+ volume: 30 * 60 * 1000,
+ offers: 2 * 60 * 1000,
+ identity: 15 * 60 * 1000
+ },
+
+ storage: {
+ maxSizeBytes: 10 * 1024 * 1024,
+ maxItems: 200
+ },
+
+ fallbackTTL: 24 * 60 * 60 * 1000
+ },
+
+ itemsPerPage: 50,
+
+ apiEndpoints: {
+ cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
+ coinGecko: 'https://api.coingecko.com/api/v3',
+ cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday',
+ cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour',
+ volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
+ },
+
+ rateLimits: {
+ coingecko: {
+ requestsPerMinute: 50,
+ minInterval: 1200
+ },
+ cryptocompare: {
+ requestsPerMinute: 30,
+ minInterval: 2000
+ }
+ },
+
+ retryDelays: [5000, 15000, 30000],
+
+ coins: [
+ { 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: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
+ ],
+
+ coinMappings: {
+ nameToSymbol: {
+ 'Bitcoin': 'BTC',
+ 'Litecoin': 'LTC',
+ 'Monero': 'XMR',
+ 'Particl': 'PART',
+ 'Particl Blind': 'PART',
+ 'Particl Anon': 'PART',
+ 'PIVX': 'PIVX',
+ 'Firo': 'FIRO',
+ 'Zcoin': 'FIRO',
+ 'Dash': 'DASH',
+ 'Decred': 'DCR',
+ 'Wownero': 'WOW',
+ 'Bitcoin Cash': 'BCH',
+ 'Dogecoin': 'DOGE'
+ },
+
+ nameToDisplayName: {
+ 'Bitcoin': 'Bitcoin',
+ 'Litecoin': 'Litecoin',
+ 'Monero': 'Monero',
+ 'Particl': 'Particl',
+ 'Particl Blind': 'Particl Blind',
+ 'Particl Anon': 'Particl Anon',
+ 'PIVX': 'PIVX',
+ 'Firo': 'Firo',
+ 'Zcoin': 'Firo',
+ 'Dash': 'Dash',
+ 'Decred': 'Decred',
+ 'Wownero': 'Wownero',
+ 'Bitcoin Cash': 'Bitcoin Cash',
+ 'Dogecoin': 'Dogecoin'
+ },
+
+ idToName: {
+ 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
+ 6: 'monero', 7: 'particl blind', 8: 'particl anon',
+ 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash',
+ 18: 'dogecoin'
+ },
+
+ nameToCoinGecko: {
+ 'bitcoin': 'bitcoin',
+ 'monero': 'monero',
+ 'particl': 'particl',
+ 'bitcoin cash': 'bitcoin-cash',
+ 'bitcoincash': 'bitcoin-cash',
+ 'pivx': 'pivx',
+ 'firo': 'firo',
+ 'zcoin': 'firo',
+ 'dash': 'dash',
+ 'litecoin': 'litecoin',
+ 'dogecoin': 'dogecoin',
+ 'decred': 'decred',
+ 'wownero': 'wownero'
+ }
+ },
+
+ chartConfig: {
+ colors: {
+ default: {
+ lineColor: 'rgba(77, 132, 240, 1)',
+ backgroundColor: 'rgba(77, 132, 240, 0.1)'
+ }
+ },
+ showVolume: false,
+ specialCoins: [''],
+ resolutions: {
+ year: { days: 365, interval: 'month' },
+ sixMonths: { days: 180, interval: 'daily' },
+ day: { days: 1, interval: 'hourly' }
+ },
+ currentResolution: 'year'
+ }
+ };
+
+ const publicAPI = {
+ ...defaultConfig,
+
+ initialize: function(options = {}) {
+ if (state.isInitialized) {
+ console.warn('[ConfigManager] Already initialized');
+ return this;
+ }
+
+ if (options) {
+ Object.assign(this, options);
+ }
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose());
+ }
+
+ this.utils = utils;
+
+ state.isInitialized = true;
+ console.log('ConfigManager initialized');
+ return this;
+ },
+
+ getAPIKeys: function() {
+ if (typeof window.getAPIKeys === 'function') {
+ const apiKeys = window.getAPIKeys();
+ return {
+ cryptoCompare: apiKeys.cryptoCompare || '',
+ coinGecko: apiKeys.coinGecko || ''
+ };
+ }
+
+ return {
+ cryptoCompare: '',
+ coinGecko: ''
+ };
+ },
+
+ getCoinBackendId: function(coinName) {
+ if (!coinName) return null;
+
+ 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;
+ },
+
+ coinMatches: function(offerCoin, filterCoin) {
+ if (!offerCoin || !filterCoin) return false;
+
+ offerCoin = offerCoin.toLowerCase();
+ filterCoin = filterCoin.toLowerCase();
+
+ 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')) {
+ return true;
+ }
+
+ const particlVariants = ['particl', 'particl anon', 'particl blind'];
+ if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
+ return true;
+ }
+
+ if (particlVariants.includes(filterCoin)) {
+ return offerCoin === filterCoin;
+ }
+
+ return false;
+ },
+
+ update: function(path, value) {
+ const parts = path.split('.');
+ let current = this;
+
+ for (let i = 0; i < parts.length - 1; i++) {
+ if (!current[parts[i]]) {
+ current[parts[i]] = {};
+ }
+ current = current[parts[i]];
+ }
+
+ current[parts[parts.length - 1]] = value;
+ return this;
+ },
+
+ get: function(path, defaultValue = null) {
+ const parts = path.split('.');
+ let current = this;
+
+ for (let i = 0; i < parts.length; i++) {
+ if (current === undefined || current === null) {
+ return defaultValue;
+ }
+ current = current[parts[i]];
+ }
+
+ return current !== undefined ? current : defaultValue;
+ },
+
+ dispose: function() {
+ state.isInitialized = false;
+ console.log('ConfigManager disposed');
+ }
+ };
+
+ const utils = {
+ formatNumber: function(number, decimals = 2) {
+ if (typeof number !== 'number' || isNaN(number)) {
+ console.warn('formatNumber received a non-number value:', number);
+ return '0';
+ }
+ try {
+ return new Intl.NumberFormat('en-US', {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals
+ }).format(number);
+ } catch (e) {
+ return '0';
+ }
+ },
+
+ formatDate: function(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: function(func, delay) {
+ let timeoutId;
+ return function(...args) {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => func(...args), delay);
+ };
+ },
+
+ formatTimeLeft: function(timestamp) {
+ const now = Math.floor(Date.now() / 1000);
+ if (timestamp <= now) return "Expired";
+ return this.formatTime(timestamp);
+ },
+
+ formatTime: function(timestamp, addAgoSuffix = false) {
+ const now = Math.floor(Date.now() / 1000);
+ const diff = Math.abs(now - timestamp);
+
+ let timeString;
+ if (diff < 60) {
+ timeString = `${diff} seconds`;
+ } else if (diff < 3600) {
+ timeString = `${Math.floor(diff / 60)} minutes`;
+ } else if (diff < 86400) {
+ timeString = `${Math.floor(diff / 3600)} hours`;
+ } else if (diff < 2592000) {
+ timeString = `${Math.floor(diff / 86400)} days`;
+ } else if (diff < 31536000) {
+ timeString = `${Math.floor(diff / 2592000)} months`;
+ } else {
+ timeString = `${Math.floor(diff / 31536000)} years`;
+ }
+
+ return addAgoSuffix ? `${timeString} ago` : timeString;
+ },
+
+ escapeHtml: function(unsafe) {
+ if (typeof unsafe !== 'string') {
+ console.warn('escapeHtml received a non-string value:', unsafe);
+ return '';
+ }
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ },
+
+ formatPrice: function(coin, price) {
+ if (typeof price !== 'number' || isNaN(price)) {
+ console.warn(`Invalid price for ${coin}:`, price);
+ return 'N/A';
+ }
+ if (price < 0.000001) return price.toExponential(2);
+ if (price < 0.001) return price.toFixed(8);
+ if (price < 1) return price.toFixed(4);
+ if (price < 10) return price.toFixed(3);
+ if (price < 1000) return price.toFixed(2);
+ if (price < 100000) return price.toFixed(1);
+ return price.toFixed(0);
+ },
+
+ getEmptyPriceData: function() {
+ return {
+ 'bitcoin': { usd: null, btc: null },
+ 'bitcoin-cash': { usd: null, btc: null },
+ 'dash': { usd: null, btc: null },
+ 'dogecoin': { usd: null, btc: null },
+ 'decred': { usd: null, btc: null },
+ 'litecoin': { usd: null, btc: null },
+ 'particl': { usd: null, btc: null },
+ 'pivx': { usd: null, btc: null },
+ 'monero': { usd: null, btc: null },
+ 'zano': { usd: null, btc: null },
+ 'wownero': { usd: null, btc: null },
+ 'firo': { usd: null, btc: null }
+ };
+ },
+
+ getCoinSymbol: function(fullName) {
+ return publicAPI.coinMappings?.nameToSymbol[fullName] || fullName;
+ }
+ };
+
+ return publicAPI;
+})();
+
+window.logger = {
+ log: function(message) {
+ console.log(`[AppLog] ${new Date().toISOString()}: ${message}`);
+ },
+ warn: function(message) {
+ console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`);
+ },
+ error: function(message) {
+ console.error(`[AppError] ${new Date().toISOString()}: ${message}`);
+ }
+};
+
+window.config = ConfigManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.configManagerInitialized) {
+ ConfigManager.initialize();
+ window.configManagerInitialized = true;
+ }
+});
+
+if (typeof module !== 'undefined') {
+ module.exports = ConfigManager;
+}
+
+//console.log('ConfigManager initialized with properties:', Object.keys(ConfigManager));
+console.log('ConfigManager initialized');
diff --git a/basicswap/static/js/modules/identity-manager.js b/basicswap/static/js/modules/identity-manager.js
new file mode 100644
index 0000000..9ac2859
--- /dev/null
+++ b/basicswap/static/js/modules/identity-manager.js
@@ -0,0 +1,192 @@
+const IdentityManager = (function() {
+ const state = {
+ cache: new Map(),
+ pendingRequests: new Map(),
+ config: {
+ retryDelay: 2000,
+ maxRetries: 3,
+ maxCacheSize: 100,
+ cacheTimeout: window.config?.cacheConfig?.ttlSettings?.identity || 15 * 60 * 1000,
+ debug: false
+ }
+ };
+
+ function log(message, ...args) {
+ if (state.config.debug) {
+ console.log(`[IdentityManager] ${message}`, ...args);
+ }
+ }
+
+ const publicAPI = {
+ getIdentityData: async function(address) {
+ if (!address) {
+ return null;
+ }
+
+ const cachedData = this.getCachedIdentity(address);
+ if (cachedData) {
+ log(`Cache hit for ${address}`);
+ return cachedData;
+ }
+
+ if (state.pendingRequests.has(address)) {
+ log(`Using pending request for ${address}`);
+ return state.pendingRequests.get(address);
+ }
+
+ log(`Fetching identity for ${address}`);
+ const request = fetchWithRetry(address);
+ state.pendingRequests.set(address, request);
+
+ try {
+ const data = await request;
+ this.setCachedIdentity(address, data);
+ return data;
+ } finally {
+ state.pendingRequests.delete(address);
+ }
+ },
+
+ getCachedIdentity: function(address) {
+ const cached = state.cache.get(address);
+ if (cached && (Date.now() - cached.timestamp) < state.config.cacheTimeout) {
+ return cached.data;
+ }
+ return null;
+ },
+
+ setCachedIdentity: function(address, data) {
+ if (state.cache.size >= state.config.maxCacheSize) {
+ const oldestEntries = [...state.cache.entries()]
+ .sort((a, b) => a[1].timestamp - b[1].timestamp)
+ .slice(0, Math.floor(state.config.maxCacheSize * 0.2));
+
+ oldestEntries.forEach(([key]) => {
+ state.cache.delete(key);
+ log(`Pruned cache entry for ${key}`);
+ });
+ }
+
+ state.cache.set(address, {
+ data,
+ timestamp: Date.now()
+ });
+ log(`Cached identity for ${address}`);
+ },
+
+ clearCache: function() {
+ log(`Clearing identity cache (${state.cache.size} entries)`);
+ state.cache.clear();
+ state.pendingRequests.clear();
+ },
+
+ limitCacheSize: function(maxSize = state.config.maxCacheSize) {
+ if (state.cache.size <= maxSize) {
+ return 0;
+ }
+
+ const entriesToRemove = [...state.cache.entries()]
+ .sort((a, b) => a[1].timestamp - b[1].timestamp)
+ .slice(0, state.cache.size - maxSize);
+
+ entriesToRemove.forEach(([key]) => state.cache.delete(key));
+ log(`Limited cache size, removed ${entriesToRemove.length} entries`);
+
+ return entriesToRemove.length;
+ },
+
+ getCacheSize: function() {
+ return state.cache.size;
+ },
+
+ configure: function(options = {}) {
+ Object.assign(state.config, options);
+ log(`Configuration updated:`, state.config);
+ return state.config;
+ },
+
+ getStats: function() {
+ const now = Date.now();
+ let expiredCount = 0;
+ let totalSize = 0;
+
+ state.cache.forEach((value, key) => {
+ if (now - value.timestamp > state.config.cacheTimeout) {
+ expiredCount++;
+ }
+ const keySize = key.length * 2;
+ const dataSize = JSON.stringify(value.data).length * 2;
+ totalSize += keySize + dataSize;
+ });
+
+ return {
+ cacheEntries: state.cache.size,
+ pendingRequests: state.pendingRequests.size,
+ expiredEntries: expiredCount,
+ estimatedSizeKB: Math.round(totalSize / 1024),
+ config: { ...state.config }
+ };
+ },
+
+ setDebugMode: function(enabled) {
+ state.config.debug = Boolean(enabled);
+ return `Debug mode ${state.config.debug ? 'enabled' : 'disabled'}`;
+ },
+
+ initialize: function(options = {}) {
+
+ if (options) {
+ this.configure(options);
+ }
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('identityManager', this, (mgr) => mgr.dispose());
+ }
+
+ log('IdentityManager initialized');
+ return this;
+ },
+
+ dispose: function() {
+ this.clearCache();
+ log('IdentityManager disposed');
+ }
+ };
+
+ async function fetchWithRetry(address, attempt = 1) {
+ try {
+ const response = await fetch(`/json/identities/${address}`, {
+ signal: AbortSignal.timeout(5000)
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ return await response.json();
+ } catch (error) {
+ if (attempt >= state.config.maxRetries) {
+ console.error(`[IdentityManager] Error:`, error.message);
+ console.warn(`[IdentityManager] Failed to fetch identity for ${address} after ${attempt} attempts`);
+ return null;
+ }
+
+ await new Promise(resolve => setTimeout(resolve, state.config.retryDelay * attempt));
+ return fetchWithRetry(address, attempt + 1);
+ }
+ }
+
+ return publicAPI;
+})();
+
+window.IdentityManager = IdentityManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.identityManagerInitialized) {
+ IdentityManager.initialize();
+ window.identityManagerInitialized = true;
+ }
+});
+
+//console.log('IdentityManager initialized with methods:', Object.keys(IdentityManager));
+console.log('IdentityManager initialized');
diff --git a/basicswap/static/js/modules/memory-manager.js b/basicswap/static/js/modules/memory-manager.js
new file mode 100644
index 0000000..4c3cffd
--- /dev/null
+++ b/basicswap/static/js/modules/memory-manager.js
@@ -0,0 +1,219 @@
+const MemoryManager = (function() {
+
+ const state = {
+ isMonitoringEnabled: false,
+ monitorInterval: null,
+ cleanupInterval: null
+ };
+
+ const config = {
+ monitorInterval: 30000,
+ cleanupInterval: 60000,
+ debug: false
+ };
+
+ function log(message, ...args) {
+ if (config.debug) {
+ console.log(`[MemoryManager] ${message}`, ...args);
+ }
+ }
+
+ const publicAPI = {
+ enableMonitoring: function(interval = config.monitorInterval) {
+ if (state.monitorInterval) {
+ clearInterval(state.monitorInterval);
+ }
+
+ state.isMonitoringEnabled = true;
+ config.monitorInterval = interval;
+
+ this.logMemoryUsage();
+
+ state.monitorInterval = setInterval(() => {
+ this.logMemoryUsage();
+ }, interval);
+
+ console.log(`Memory monitoring enabled - reporting every ${interval/1000} seconds`);
+ return true;
+ },
+
+ disableMonitoring: function() {
+ if (state.monitorInterval) {
+ clearInterval(state.monitorInterval);
+ state.monitorInterval = null;
+ }
+
+ state.isMonitoringEnabled = false;
+ console.log('Memory monitoring disabled');
+ return true;
+ },
+
+ logMemoryUsage: function() {
+ const timestamp = new Date().toLocaleTimeString();
+ console.log(`=== Memory Monitor [${timestamp}] ===`);
+
+ if (window.performance && window.performance.memory) {
+ console.log('Memory usage:', {
+ usedJSHeapSize: (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
+ totalJSHeapSize: (window.performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB'
+ });
+ }
+
+ if (navigator.deviceMemory) {
+ console.log('Device memory:', navigator.deviceMemory, 'GB');
+ }
+
+ const nodeCount = document.querySelectorAll('*').length;
+ console.log('DOM node count:', nodeCount);
+
+ if (window.CleanupManager) {
+ const counts = CleanupManager.getResourceCounts();
+ console.log('Managed resources:', counts);
+ }
+
+ if (window.TooltipManager) {
+ const tooltipInstances = document.querySelectorAll('[data-tippy-root]').length;
+ const tooltipTriggers = document.querySelectorAll('[data-tooltip-trigger-id]').length;
+ console.log('Tooltip instances:', tooltipInstances, '- Tooltip triggers:', tooltipTriggers);
+ }
+
+ if (window.CacheManager && window.CacheManager.getStats) {
+ const cacheStats = CacheManager.getStats();
+ console.log('Cache stats:', cacheStats);
+ }
+
+ if (window.IdentityManager && window.IdentityManager.getStats) {
+ const identityStats = window.IdentityManager.getStats();
+ console.log('Identity cache stats:', identityStats);
+ }
+
+ console.log('==============================');
+ },
+
+ enableAutoCleanup: function(interval = config.cleanupInterval) {
+ if (state.cleanupInterval) {
+ clearInterval(state.cleanupInterval);
+ }
+
+ config.cleanupInterval = interval;
+
+ this.forceCleanup();
+
+ state.cleanupInterval = setInterval(() => {
+ this.forceCleanup();
+ }, interval);
+
+ log('Auto-cleanup enabled every', interval/1000, 'seconds');
+ return true;
+ },
+
+ disableAutoCleanup: function() {
+ if (state.cleanupInterval) {
+ clearInterval(state.cleanupInterval);
+ state.cleanupInterval = null;
+ }
+
+ console.log('Memory auto-cleanup disabled');
+ return true;
+ },
+
+ forceCleanup: function() {
+ if (config.debug) {
+ console.log('Running memory cleanup...', new Date().toLocaleTimeString());
+ }
+
+ if (window.CacheManager && CacheManager.cleanup) {
+ CacheManager.cleanup(true);
+ }
+
+ if (window.TooltipManager && TooltipManager.cleanup) {
+ window.TooltipManager.cleanup();
+ }
+
+ document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
+ if (window.TooltipManager && TooltipManager.destroy) {
+ window.TooltipManager.destroy(element);
+ }
+ });
+
+ if (window.chartModule && chartModule.cleanup) {
+ chartModule.cleanup();
+ }
+
+ if (window.gc) {
+ window.gc();
+ } else {
+ const arr = new Array(1000);
+ for (let i = 0; i < 1000; i++) {
+ arr[i] = new Array(10000).join('x');
+ }
+ }
+
+ if (config.debug) {
+ console.log('Memory cleanup completed');
+ }
+
+ return true;
+ },
+
+ setDebugMode: function(enabled) {
+ config.debug = Boolean(enabled);
+ return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
+ },
+
+ getStatus: function() {
+ return {
+ monitoring: {
+ enabled: Boolean(state.monitorInterval),
+ interval: config.monitorInterval
+ },
+ autoCleanup: {
+ enabled: Boolean(state.cleanupInterval),
+ interval: config.cleanupInterval
+ },
+ debug: config.debug
+ };
+ },
+
+ initialize: function(options = {}) {
+ if (options.debug !== undefined) {
+ this.setDebugMode(options.debug);
+ }
+
+ if (options.enableMonitoring) {
+ this.enableMonitoring(options.monitorInterval || config.monitorInterval);
+ }
+
+ if (options.enableAutoCleanup) {
+ this.enableAutoCleanup(options.cleanupInterval || config.cleanupInterval);
+ }
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('memoryManager', this, (mgr) => mgr.dispose());
+ }
+
+ log('MemoryManager initialized');
+ return this;
+ },
+
+ dispose: function() {
+ this.disableMonitoring();
+ this.disableAutoCleanup();
+ log('MemoryManager disposed');
+ }
+ };
+
+ return publicAPI;
+})();
+
+window.MemoryManager = MemoryManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.memoryManagerInitialized) {
+ MemoryManager.initialize();
+ window.memoryManagerInitialized = true;
+ }
+});
+
+//console.log('MemoryManager initialized with methods:', Object.keys(MemoryManager));
+console.log('MemoryManager initialized');
diff --git a/basicswap/static/js/modules/network-manager.js b/basicswap/static/js/modules/network-manager.js
new file mode 100644
index 0000000..c5a1dc4
--- /dev/null
+++ b/basicswap/static/js/modules/network-manager.js
@@ -0,0 +1,280 @@
+const NetworkManager = (function() {
+ const state = {
+ isOnline: navigator.onLine,
+ reconnectAttempts: 0,
+ reconnectTimer: null,
+ lastNetworkError: null,
+ eventHandlers: {},
+ connectionTestInProgress: false
+ };
+
+ const config = {
+ maxReconnectAttempts: 5,
+ reconnectDelay: 5000,
+ reconnectBackoff: 1.5,
+ connectionTestEndpoint: '/json',
+ connectionTestTimeout: 3000,
+ debug: false
+ };
+
+ function log(message, ...args) {
+ if (config.debug) {
+ console.log(`[NetworkManager] ${message}`, ...args);
+ }
+ }
+
+ function generateHandlerId() {
+ return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
+ }
+
+ const publicAPI = {
+ initialize: function(options = {}) {
+ Object.assign(config, options);
+
+ window.addEventListener('online', this.handleOnlineStatus.bind(this));
+ window.addEventListener('offline', this.handleOfflineStatus.bind(this));
+
+ state.isOnline = navigator.onLine;
+ log(`Network status initialized: ${state.isOnline ? 'online' : 'offline'}`);
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('networkManager', this, (mgr) => mgr.dispose());
+ }
+
+ return this;
+ },
+
+ isOnline: function() {
+ return state.isOnline;
+ },
+
+ getReconnectAttempts: function() {
+ return state.reconnectAttempts;
+ },
+
+ resetReconnectAttempts: function() {
+ state.reconnectAttempts = 0;
+ return this;
+ },
+
+ handleOnlineStatus: function() {
+ log('Browser reports online status');
+ state.isOnline = true;
+ this.notifyHandlers('online');
+
+ if (state.reconnectTimer) {
+ this.scheduleReconnectRefresh();
+ }
+ },
+
+ handleOfflineStatus: function() {
+ log('Browser reports offline status');
+ state.isOnline = false;
+ this.notifyHandlers('offline');
+ },
+
+ handleNetworkError: function(error) {
+ if (error && (
+ (error.name === 'TypeError' && error.message.includes('NetworkError')) ||
+ (error.name === 'AbortError') ||
+ (error.message && error.message.includes('network')) ||
+ (error.message && error.message.includes('timeout'))
+ )) {
+ log('Network error detected:', error.message);
+
+ if (state.isOnline) {
+ state.isOnline = false;
+ state.lastNetworkError = error;
+ this.notifyHandlers('error', error);
+ }
+
+ if (!state.reconnectTimer) {
+ this.scheduleReconnectRefresh();
+ }
+
+ return true;
+ }
+ return false;
+ },
+
+ scheduleReconnectRefresh: function() {
+ if (state.reconnectTimer) {
+ clearTimeout(state.reconnectTimer);
+ state.reconnectTimer = null;
+ }
+
+ const delay = config.reconnectDelay * Math.pow(config.reconnectBackoff,
+ Math.min(state.reconnectAttempts, 5));
+
+ log(`Scheduling reconnection attempt in ${delay/1000} seconds`);
+
+ state.reconnectTimer = setTimeout(() => {
+ state.reconnectTimer = null;
+ this.attemptReconnect();
+ }, delay);
+
+ return this;
+ },
+
+ attemptReconnect: function() {
+ if (!navigator.onLine) {
+ log('Browser still reports offline, delaying reconnection attempt');
+ this.scheduleReconnectRefresh();
+ return;
+ }
+
+ if (state.connectionTestInProgress) {
+ log('Connection test already in progress');
+ return;
+ }
+
+ state.reconnectAttempts++;
+ state.connectionTestInProgress = true;
+
+ log(`Attempting reconnect #${state.reconnectAttempts}`);
+
+ this.testBackendConnection()
+ .then(isAvailable => {
+ state.connectionTestInProgress = false;
+
+ if (isAvailable) {
+ log('Backend connection confirmed');
+ state.isOnline = true;
+ state.reconnectAttempts = 0;
+ state.lastNetworkError = null;
+ this.notifyHandlers('reconnected');
+ } else {
+ log('Backend still unavailable');
+
+ if (state.reconnectAttempts < config.maxReconnectAttempts) {
+ this.scheduleReconnectRefresh();
+ } else {
+ log('Maximum reconnect attempts reached');
+ this.notifyHandlers('maxAttemptsReached');
+ }
+ }
+ })
+ .catch(error => {
+ state.connectionTestInProgress = false;
+ log('Error during connection test:', error);
+
+ if (state.reconnectAttempts < config.maxReconnectAttempts) {
+ this.scheduleReconnectRefresh();
+ } else {
+ log('Maximum reconnect attempts reached');
+ this.notifyHandlers('maxAttemptsReached');
+ }
+ });
+ },
+
+ testBackendConnection: function() {
+ return fetch(config.connectionTestEndpoint, {
+ method: 'HEAD',
+ headers: {
+ 'Cache-Control': 'no-cache',
+ 'Pragma': 'no-cache'
+ },
+ timeout: config.connectionTestTimeout,
+ signal: AbortSignal.timeout(config.connectionTestTimeout)
+ })
+ .then(response => {
+ return response.ok;
+ })
+ .catch(error => {
+ log('Backend connection test failed:', error.message);
+ return false;
+ });
+ },
+
+ manualReconnect: function() {
+ log('Manual reconnection requested');
+
+ state.isOnline = navigator.onLine;
+ state.reconnectAttempts = 0;
+
+ this.notifyHandlers('manualReconnect');
+
+ if (state.isOnline) {
+ return this.attemptReconnect();
+ } else {
+ log('Cannot attempt manual reconnect while browser reports offline');
+ this.notifyHandlers('offlineWarning');
+ return false;
+ }
+ },
+
+ addHandler: function(event, handler) {
+ if (!state.eventHandlers[event]) {
+ state.eventHandlers[event] = {};
+ }
+
+ const handlerId = generateHandlerId();
+ state.eventHandlers[event][handlerId] = handler;
+
+ return handlerId;
+ },
+
+ removeHandler: function(event, handlerId) {
+ if (state.eventHandlers[event] && state.eventHandlers[event][handlerId]) {
+ delete state.eventHandlers[event][handlerId];
+ return true;
+ }
+ return false;
+ },
+
+ notifyHandlers: function(event, data) {
+ if (state.eventHandlers[event]) {
+ Object.values(state.eventHandlers[event]).forEach(handler => {
+ try {
+ handler(data);
+ } catch (error) {
+ log(`Error in ${event} handler:`, error);
+ }
+ });
+ }
+ },
+
+ setDebugMode: function(enabled) {
+ config.debug = Boolean(enabled);
+ return `Debug mode ${config.debug ? 'enabled' : 'disabled'}`;
+ },
+
+ getState: function() {
+ return {
+ isOnline: state.isOnline,
+ reconnectAttempts: state.reconnectAttempts,
+ hasReconnectTimer: Boolean(state.reconnectTimer),
+ connectionTestInProgress: state.connectionTestInProgress
+ };
+ },
+
+ dispose: function() {
+ if (state.reconnectTimer) {
+ clearTimeout(state.reconnectTimer);
+ state.reconnectTimer = null;
+ }
+
+ window.removeEventListener('online', this.handleOnlineStatus);
+ window.removeEventListener('offline', this.handleOfflineStatus);
+
+ state.eventHandlers = {};
+
+ log('NetworkManager disposed');
+ }
+ };
+
+ return publicAPI;
+})();
+
+window.NetworkManager = NetworkManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.networkManagerInitialized) {
+ NetworkManager.initialize();
+ window.networkManagerInitialized = true;
+ }
+});
+
+//console.log('NetworkManager initialized with methods:', Object.keys(NetworkManager));
+console.log('NetworkManager initialized');
+
diff --git a/basicswap/static/js/modules/notification-manager.js b/basicswap/static/js/modules/notification-manager.js
new file mode 100644
index 0000000..7265b15
--- /dev/null
+++ b/basicswap/static/js/modules/notification-manager.js
@@ -0,0 +1,126 @@
+const NotificationManager = (function() {
+
+ const config = {
+ showNewOffers: false,
+ showNewBids: true,
+ showBidAccepted: true
+ };
+
+ function ensureToastContainer() {
+ let container = document.getElementById('ul_updates');
+ if (!container) {
+ const floating_div = document.createElement('div');
+ floating_div.classList.add('floatright');
+ container = document.createElement('ul');
+ container.setAttribute('id', 'ul_updates');
+ floating_div.appendChild(container);
+ document.body.appendChild(floating_div);
+ }
+ return container;
+ }
+
+ const publicAPI = {
+ initialize: function(options = {}) {
+ Object.assign(config, options);
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('notificationManager', this, (mgr) => {
+
+ console.log('NotificationManager disposed');
+ });
+ }
+
+ return this;
+ },
+
+ createToast: function(title, type = 'success') {
+ const messages = ensureToastContainer();
+ const message = document.createElement('li');
+ message.innerHTML = `
+
+
+
+ ${title}
+
+
+
+ `;
+ messages.appendChild(message);
+ },
+
+ handleWebSocketEvent: function(data) {
+ if (!data || !data.event) return;
+ let toastTitle;
+ let shouldShowToast = false;
+
+ switch (data.event) {
+ case 'new_offer':
+ toastTitle = `New network offer`;
+ shouldShowToast = config.showNewOffers;
+ break;
+ case 'new_bid':
+ toastTitle = ` New bid on
+ offer`;
+ shouldShowToast = config.showNewBids;
+ break;
+ case 'bid_accepted':
+ toastTitle = ` Bid accepted`;
+ shouldShowToast = config.showBidAccepted;
+ break;
+ }
+
+ if (toastTitle && shouldShowToast) {
+ this.createToast(toastTitle);
+ }
+ },
+
+ updateConfig: function(newConfig) {
+ Object.assign(config, newConfig);
+ return this;
+ }
+ };
+
+ window.closeAlert = function(event) {
+ let element = event.target;
+ while (element.nodeName !== "BUTTON") {
+ element = element.parentNode;
+ }
+ element.parentNode.parentNode.removeChild(element.parentNode);
+ };
+
+ return publicAPI;
+})();
+
+window.NotificationManager = NotificationManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+
+ if (!window.notificationManagerInitialized) {
+ window.NotificationManager.initialize(window.notificationConfig || {});
+ window.notificationManagerInitialized = true;
+ }
+});
+
+//console.log('NotificationManager initialized with methods:', Object.keys(NotificationManager));
+console.log('NotificationManager initialized');
diff --git a/basicswap/static/js/modules/summary-manager.js b/basicswap/static/js/modules/summary-manager.js
new file mode 100644
index 0000000..d4f42b4
--- /dev/null
+++ b/basicswap/static/js/modules/summary-manager.js
@@ -0,0 +1,338 @@
+const SummaryManager = (function() {
+ const config = {
+ refreshInterval: window.config?.cacheDuration || 30000,
+ summaryEndpoint: '/json',
+ retryDelay: 5000,
+ maxRetries: 3,
+ requestTimeout: 15000
+ };
+
+ let refreshTimer = null;
+ let webSocket = null;
+ let fetchRetryCount = 0;
+ let lastSuccessfulData = null;
+
+ function updateElement(elementId, value) {
+ const element = document.getElementById(elementId);
+ if (!element) return false;
+
+ const safeValue = (value !== undefined && value !== null)
+ ? value
+ : (element.dataset.lastValue || 0);
+
+ element.dataset.lastValue = safeValue;
+
+ if (elementId === 'sent-bids-counter' || elementId === 'recv-bids-counter') {
+ const svg = element.querySelector('svg');
+ element.textContent = safeValue;
+ if (svg) {
+ element.insertBefore(svg, element.firstChild);
+ }
+ } else {
+ element.textContent = safeValue;
+ }
+
+ if (['offers-counter', 'bid-requests-counter', 'sent-bids-counter',
+ 'recv-bids-counter', 'swaps-counter', 'network-offers-counter',
+ 'watched-outputs-counter'].includes(elementId)) {
+ element.classList.remove('bg-blue-500', 'bg-gray-400');
+ element.classList.add(safeValue > 0 ? 'bg-blue-500' : 'bg-gray-400');
+ }
+
+ if (elementId === 'swaps-counter') {
+ const swapContainer = document.getElementById('swapContainer');
+ if (swapContainer) {
+ const isSwapping = safeValue > 0;
+ if (isSwapping) {
+ swapContainer.innerHTML = document.querySelector('#swap-in-progress-green-template').innerHTML || '';
+ swapContainer.style.animation = 'spin 2s linear infinite';
+ } else {
+ swapContainer.innerHTML = document.querySelector('#swap-in-progress-template').innerHTML || '';
+ swapContainer.style.animation = 'none';
+ }
+ }
+ }
+ return true;
+ }
+
+ function updateUIFromData(data) {
+ if (!data) return;
+
+ updateElement('network-offers-counter', data.num_network_offers);
+ updateElement('offers-counter', data.num_sent_active_offers);
+ updateElement('sent-bids-counter', data.num_sent_active_bids);
+ updateElement('recv-bids-counter', data.num_recv_active_bids);
+ updateElement('bid-requests-counter', data.num_available_bids);
+ updateElement('swaps-counter', data.num_swapping);
+ updateElement('watched-outputs-counter', data.num_watched_outputs);
+
+ const shutdownButtons = document.querySelectorAll('.shutdown-button');
+ shutdownButtons.forEach(button => {
+ button.setAttribute('data-active-swaps', data.num_swapping);
+ if (data.num_swapping > 0) {
+ button.classList.add('shutdown-disabled');
+ button.setAttribute('data-disabled', 'true');
+ button.setAttribute('title', 'Caution: Swaps in progress');
+ } else {
+ button.classList.remove('shutdown-disabled');
+ button.removeAttribute('data-disabled');
+ button.removeAttribute('title');
+ }
+ });
+ }
+
+ function cacheSummaryData(data) {
+ if (!data) return;
+
+ localStorage.setItem('summary_data_cache', JSON.stringify({
+ timestamp: Date.now(),
+ data: data
+ }));
+ }
+
+ function getCachedSummaryData() {
+ let cachedData = null;
+
+ cachedData = localStorage.getItem('summary_data_cache');
+ if (!cachedData) return null;
+
+ const parsedCache = JSON.parse(cachedData);
+ const maxAge = 24 * 60 * 60 * 1000;
+
+ if (Date.now() - parsedCache.timestamp < maxAge) {
+ return parsedCache.data;
+ }
+
+ return null;
+ }
+
+ function fetchSummaryDataWithTimeout() {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), config.requestTimeout);
+
+ return fetch(config.summaryEndpoint, {
+ signal: controller.signal,
+ headers: {
+ 'Accept': 'application/json',
+ 'Cache-Control': 'no-cache',
+ 'Pragma': 'no-cache'
+ }
+ })
+ .then(response => {
+ clearTimeout(timeoutId);
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`);
+ }
+
+ return response.json();
+ })
+ .catch(error => {
+ clearTimeout(timeoutId);
+ throw error;
+ });
+ }
+
+ function setupWebSocket() {
+ if (webSocket) {
+ webSocket.close();
+ }
+
+ const wsPort = window.config?.wsPort ||
+ (typeof determineWebSocketPort === 'function' ? determineWebSocketPort() : '11700');
+
+ const wsUrl = "ws://" + window.location.hostname + ":" + wsPort;
+ webSocket = new WebSocket(wsUrl);
+
+ webSocket.onopen = () => {
+ publicAPI.fetchSummaryData()
+ .then(() => {})
+ .catch(() => {});
+ };
+
+ webSocket.onmessage = (event) => {
+ let data;
+
+ try {
+ data = JSON.parse(event.data);
+ } catch (error) {
+ if (window.logger && window.logger.error) {
+ window.logger.error('WebSocket message processing error: ' + error.message);
+ }
+ return;
+ }
+
+ if (data.event) {
+ publicAPI.fetchSummaryData()
+ .then(() => {})
+ .catch(() => {});
+
+ if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
+ window.NotificationManager.handleWebSocketEvent(data);
+ }
+ }
+ };
+
+ webSocket.onclose = () => {
+ setTimeout(setupWebSocket, 5000);
+ };
+ }
+
+ function ensureSwapTemplates() {
+ if (!document.getElementById('swap-in-progress-template')) {
+ const template = document.createElement('template');
+ template.id = 'swap-in-progress-template';
+ template.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
+ document.body.appendChild(template);
+ }
+
+ if (!document.getElementById('swap-in-progress-green-template') &&
+ document.querySelector('[id^="swapContainer"]')?.innerHTML) {
+ const greenTemplate = document.createElement('template');
+ greenTemplate.id = 'swap-in-progress-green-template';
+ greenTemplate.innerHTML = document.querySelector('[id^="swapContainer"]')?.innerHTML || '';
+ document.body.appendChild(greenTemplate);
+ }
+ }
+
+ function startRefreshTimer() {
+ stopRefreshTimer();
+
+ publicAPI.fetchSummaryData()
+ .then(() => {})
+ .catch(() => {});
+
+ refreshTimer = setInterval(() => {
+ publicAPI.fetchSummaryData()
+ .then(() => {})
+ .catch(() => {});
+ }, config.refreshInterval);
+ }
+
+ function stopRefreshTimer() {
+ if (refreshTimer) {
+ clearInterval(refreshTimer);
+ refreshTimer = null;
+ }
+ }
+
+ const publicAPI = {
+ initialize: function(options = {}) {
+ Object.assign(config, options);
+
+ ensureSwapTemplates();
+
+ const cachedData = getCachedSummaryData();
+ if (cachedData) {
+ updateUIFromData(cachedData);
+ }
+
+ if (window.WebSocketManager && typeof window.WebSocketManager.initialize === 'function') {
+ const wsManager = window.WebSocketManager;
+
+ if (!wsManager.isConnected()) {
+ wsManager.connect();
+ }
+
+ wsManager.addMessageHandler('message', (data) => {
+ if (data.event) {
+ this.fetchSummaryData()
+ .then(() => {})
+ .catch(() => {});
+
+ if (window.NotificationManager && typeof window.NotificationManager.handleWebSocketEvent === 'function') {
+ window.NotificationManager.handleWebSocketEvent(data);
+ }
+ }
+ });
+ } else {
+ setupWebSocket();
+ }
+
+ startRefreshTimer();
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('summaryManager', this, (mgr) => mgr.dispose());
+ }
+
+ return this;
+ },
+
+ fetchSummaryData: function() {
+ return fetchSummaryDataWithTimeout()
+ .then(data => {
+ lastSuccessfulData = data;
+ cacheSummaryData(data);
+ fetchRetryCount = 0;
+
+ updateUIFromData(data);
+
+ return data;
+ })
+ .catch(error => {
+ if (window.logger && window.logger.error) {
+ window.logger.error('Summary data fetch error: ' + error.message);
+ }
+
+ if (fetchRetryCount < config.maxRetries) {
+ fetchRetryCount++;
+
+ if (window.logger && window.logger.warn) {
+ window.logger.warn(`Retrying summary data fetch (${fetchRetryCount}/${config.maxRetries}) in ${config.retryDelay/1000}s`);
+ }
+
+ return new Promise(resolve => {
+ setTimeout(() => {
+ resolve(this.fetchSummaryData());
+ }, config.retryDelay);
+ });
+ } else {
+ const cachedData = lastSuccessfulData || getCachedSummaryData();
+
+ if (cachedData) {
+ if (window.logger && window.logger.warn) {
+ window.logger.warn('Using cached summary data after fetch failures');
+ }
+ updateUIFromData(cachedData);
+ }
+
+ fetchRetryCount = 0;
+
+ throw error;
+ }
+ });
+ },
+
+ startRefreshTimer: function() {
+ startRefreshTimer();
+ },
+
+ stopRefreshTimer: function() {
+ stopRefreshTimer();
+ },
+
+ dispose: function() {
+ stopRefreshTimer();
+
+ if (webSocket && webSocket.readyState === WebSocket.OPEN) {
+ webSocket.close();
+ }
+
+ webSocket = null;
+ }
+ };
+
+ return publicAPI;
+})();
+
+window.SummaryManager = SummaryManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.summaryManagerInitialized) {
+ window.SummaryManager = SummaryManager.initialize();
+ window.summaryManagerInitialized = true;
+ }
+});
+
+//console.log('SummaryManager initialized with methods:', Object.keys(SummaryManager));
+console.log('SummaryManager initialized');
diff --git a/basicswap/static/js/modules/tooltips-manager.js b/basicswap/static/js/modules/tooltips-manager.js
new file mode 100644
index 0000000..cb8018f
--- /dev/null
+++ b/basicswap/static/js/modules/tooltips-manager.js
@@ -0,0 +1,588 @@
+const TooltipManager = (function() {
+ let instance = null;
+
+ class TooltipManagerImpl {
+ constructor() {
+
+ if (instance) {
+ return instance;
+ }
+
+ this.activeTooltips = new WeakMap();
+ this.tooltipIdCounter = 0;
+ this.pendingAnimationFrames = new Set();
+ this.tooltipElementsMap = new Map();
+ this.maxTooltips = 300;
+ this.cleanupThreshold = 1.3;
+ this.disconnectedCheckInterval = null;
+
+ this.setupStyles();
+ this.setupCleanupEvents();
+ this.initializeMutationObserver();
+ this.startDisconnectedElementsCheck();
+
+ instance = this;
+ }
+
+ create(element, content, options = {}) {
+ if (!element) return null;
+
+ this.destroy(element);
+
+ if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
+ const oldestEntries = Array.from(this.tooltipElementsMap.entries())
+ .sort((a, b) => a[1].timestamp - b[1].timestamp)
+ .slice(0, 20);
+
+ oldestEntries.forEach(([el]) => {
+ this.destroy(el);
+ });
+ }
+
+ const originalContent = content;
+
+ const rafId = requestAnimationFrame(() => {
+ this.pendingAnimationFrames.delete(rafId);
+
+ if (!document.body.contains(element)) return;
+
+ const rect = element.getBoundingClientRect();
+ if (rect.width > 0 && rect.height > 0) {
+ this.createTooltip(element, originalContent, options, rect);
+ } else {
+ let retryCount = 0;
+ const retryCreate = () => {
+ const newRect = element.getBoundingClientRect();
+ if ((newRect.width > 0 && newRect.height > 0) || retryCount >= 3) {
+ if (newRect.width > 0 && newRect.height > 0) {
+ this.createTooltip(element, originalContent, options, newRect);
+ }
+ } else {
+ retryCount++;
+ const newRafId = requestAnimationFrame(retryCreate);
+ this.pendingAnimationFrames.add(newRafId);
+ }
+ };
+ const initialRetryId = requestAnimationFrame(retryCreate);
+ this.pendingAnimationFrames.add(initialRetryId);
+ }
+ });
+
+ this.pendingAnimationFrames.add(rafId);
+ return null;
+ }
+
+ createTooltip(element, content, options, rect) {
+ const targetId = element.getAttribute('data-tooltip-target');
+ let bgClass = 'bg-gray-400';
+ let arrowColor = 'rgb(156 163 175)';
+
+ if (targetId?.includes('tooltip-offer-') && window.jsonData) {
+ try {
+ const offerId = targetId.split('tooltip-offer-')[1];
+ let actualOfferId = offerId;
+
+ if (offerId.includes('_')) {
+ [actualOfferId] = offerId.split('_');
+ }
+
+ let offer = null;
+ if (Array.isArray(window.jsonData)) {
+ for (let i = 0; i < window.jsonData.length; i++) {
+ const o = window.jsonData[i];
+ if (o && (o.unique_id === offerId || o.offer_id === actualOfferId)) {
+ offer = o;
+ break;
+ }
+ }
+ }
+
+ if (offer) {
+ if (offer.is_revoked) {
+ bgClass = 'bg-red-500';
+ arrowColor = 'rgb(239 68 68)';
+ } else if (offer.is_own_offer) {
+ bgClass = 'bg-gray-300';
+ arrowColor = 'rgb(209 213 219)';
+ } else {
+ bgClass = 'bg-green-700';
+ arrowColor = 'rgb(21 128 61)';
+ }
+ }
+ } catch (e) {
+ console.warn('Error finding offer for tooltip:', e);
+ }
+ }
+
+ const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
+
+ try {
+ if (typeof tippy !== 'function') {
+ console.error('Tippy.js is not loaded. Cannot create tooltip.');
+ return null;
+ }
+
+ const instance = tippy(element, {
+ content: content,
+ allowHTML: true,
+ placement: options.placement || 'top',
+ appendTo: document.body,
+ animation: false,
+ duration: 0,
+ delay: 0,
+ interactive: true,
+ arrow: false,
+ theme: '',
+ moveTransition: 'none',
+ offset: [0, 10],
+ onShow(instance) {
+ if (!document.body.contains(element)) {
+ return false;
+ }
+ return true;
+ },
+ onMount(instance) {
+ if (instance.popper && instance.popper.firstElementChild) {
+ instance.popper.firstElementChild.classList.add(bgClass);
+ instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
+ }
+ const arrow = instance.popper.querySelector('.tippy-arrow');
+ if (arrow) {
+ arrow.style.setProperty('color', arrowColor, 'important');
+ }
+ },
+ popperOptions: {
+ strategy: 'fixed',
+ modifiers: [
+ {
+ name: 'preventOverflow',
+ options: {
+ boundary: 'viewport',
+ padding: 10
+ }
+ },
+ {
+ name: 'flip',
+ options: {
+ padding: 10,
+ fallbackPlacements: ['top', 'bottom', 'right', 'left']
+ }
+ }
+ ]
+ }
+ });
+
+ element.setAttribute('data-tooltip-trigger-id', tooltipId);
+ this.activeTooltips.set(element, instance);
+
+ this.tooltipElementsMap.set(element, {
+ timestamp: Date.now(),
+ id: tooltipId
+ });
+
+ return instance;
+ } catch (e) {
+ console.error('Error creating tooltip:', e);
+ return null;
+ }
+ }
+
+ destroy(element) {
+ if (!element) return;
+
+ const id = element.getAttribute('data-tooltip-trigger-id');
+ if (!id) return;
+
+ const instance = this.activeTooltips.get(element);
+ if (instance?.[0]) {
+ try {
+ instance[0].destroy();
+ } catch (e) {
+ console.warn('Error destroying tooltip:', e);
+
+ const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
+ if (tippyRoot && tippyRoot.parentNode) {
+ tippyRoot.parentNode.removeChild(tippyRoot);
+ }
+ }
+ }
+
+ this.activeTooltips.delete(element);
+ this.tooltipElementsMap.delete(element);
+
+ element.removeAttribute('data-tooltip-trigger-id');
+ }
+
+ cleanup() {
+ this.pendingAnimationFrames.forEach(id => {
+ cancelAnimationFrame(id);
+ });
+ this.pendingAnimationFrames.clear();
+
+ const elements = document.querySelectorAll('[data-tooltip-trigger-id]');
+ const batchSize = 20;
+
+ const processElementsBatch = (startIdx) => {
+ const endIdx = Math.min(startIdx + batchSize, elements.length);
+
+ for (let i = startIdx; i < endIdx; i++) {
+ this.destroy(elements[i]);
+ }
+
+ if (endIdx < elements.length) {
+ const rafId = requestAnimationFrame(() => {
+ this.pendingAnimationFrames.delete(rafId);
+ processElementsBatch(endIdx);
+ });
+ this.pendingAnimationFrames.add(rafId);
+ } else {
+ this.cleanupOrphanedTippyElements();
+ }
+ };
+
+ if (elements.length > 0) {
+ processElementsBatch(0);
+ } else {
+ this.cleanupOrphanedTippyElements();
+ }
+
+ this.tooltipElementsMap.clear();
+ }
+
+ cleanupOrphanedTippyElements() {
+ const tippyElements = document.querySelectorAll('[data-tippy-root]');
+ tippyElements.forEach(element => {
+ if (element.parentNode) {
+ element.parentNode.removeChild(element);
+ }
+ });
+ }
+
+ setupStyles() {
+ if (document.getElementById('tooltip-styles')) return;
+
+ document.head.insertAdjacentHTML('beforeend', `
+
+ `);
+ }
+
+ setupCleanupEvents() {
+ this.boundCleanup = this.cleanup.bind(this);
+ this.handleVisibilityChange = () => {
+ if (document.hidden) {
+ this.cleanup();
+
+ if (window.MemoryManager) {
+ window.MemoryManager.forceCleanup();
+ }
+ }
+ };
+
+ window.addEventListener('beforeunload', this.boundCleanup);
+ window.addEventListener('unload', this.boundCleanup);
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('tooltipManager', this, (tm) => tm.dispose());
+ }
+
+ this.cleanupInterval = setInterval(() => {
+ this.performPeriodicCleanup();
+ }, 120000);
+ }
+
+ startDisconnectedElementsCheck() {
+
+ if (this.disconnectedCheckInterval) {
+ clearInterval(this.disconnectedCheckInterval);
+ }
+
+ this.disconnectedCheckInterval = setInterval(() => {
+ this.checkForDisconnectedElements();
+ }, 60000);
+ }
+
+ checkForDisconnectedElements() {
+ if (this.tooltipElementsMap.size === 0) return;
+
+ const elementsToCheck = Array.from(this.tooltipElementsMap.keys());
+ let removedCount = 0;
+
+ elementsToCheck.forEach(element => {
+
+ if (!document.body.contains(element)) {
+ this.destroy(element);
+ removedCount++;
+ }
+ });
+
+ if (removedCount > 0) {
+ this.cleanupOrphanedTippyElements();
+ }
+ }
+
+ performPeriodicCleanup() {
+ this.cleanupOrphanedTippyElements();
+ this.checkForDisconnectedElements();
+
+ if (this.tooltipElementsMap.size > this.maxTooltips * this.cleanupThreshold) {
+ const sortedTooltips = Array.from(this.tooltipElementsMap.entries())
+ .sort((a, b) => a[1].timestamp - b[1].timestamp);
+
+ const tooltipsToRemove = sortedTooltips.slice(0, sortedTooltips.length - this.maxTooltips);
+ tooltipsToRemove.forEach(([element]) => {
+ this.destroy(element);
+ });
+ }
+ }
+
+ removeCleanupEvents() {
+ window.removeEventListener('beforeunload', this.boundCleanup);
+ window.removeEventListener('unload', this.boundCleanup);
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
+
+ if (this.cleanupInterval) {
+ clearInterval(this.cleanupInterval);
+ this.cleanupInterval = null;
+ }
+
+ if (this.disconnectedCheckInterval) {
+ clearInterval(this.disconnectedCheckInterval);
+ this.disconnectedCheckInterval = null;
+ }
+ }
+
+ initializeMutationObserver() {
+ if (this.mutationObserver) return;
+
+ this.mutationObserver = new MutationObserver(mutations => {
+ let needsCleanup = false;
+
+ mutations.forEach(mutation => {
+ if (mutation.removedNodes.length) {
+ Array.from(mutation.removedNodes).forEach(node => {
+ if (node.nodeType === 1) {
+
+ if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
+ this.destroy(node);
+ needsCleanup = true;
+ }
+
+ if (node.querySelectorAll) {
+ const tooltipTriggers = node.querySelectorAll('[data-tooltip-trigger-id]');
+ if (tooltipTriggers.length > 0) {
+ tooltipTriggers.forEach(el => {
+ this.destroy(el);
+ });
+ needsCleanup = true;
+ }
+ }
+ }
+ });
+ }
+ });
+
+ if (needsCleanup) {
+ this.cleanupOrphanedTippyElements();
+ }
+ });
+
+ this.mutationObserver.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+ }
+
+ initializeTooltips(selector = '[data-tooltip-target]') {
+ document.querySelectorAll(selector).forEach(element => {
+ const targetId = element.getAttribute('data-tooltip-target');
+ const tooltipContent = document.getElementById(targetId);
+
+ if (tooltipContent) {
+ this.create(element, tooltipContent.innerHTML, {
+ placement: element.getAttribute('data-tooltip-placement') || 'top'
+ });
+ }
+ });
+ }
+
+ dispose() {
+ this.cleanup();
+
+ this.pendingAnimationFrames.forEach(id => {
+ cancelAnimationFrame(id);
+ });
+ this.pendingAnimationFrames.clear();
+
+ if (this.mutationObserver) {
+ this.mutationObserver.disconnect();
+ this.mutationObserver = null;
+ }
+
+ this.removeCleanupEvents();
+
+ const styleElement = document.getElementById('tooltip-styles');
+ if (styleElement && styleElement.parentNode) {
+ styleElement.parentNode.removeChild(styleElement);
+ }
+
+ this.activeTooltips = new WeakMap();
+ this.tooltipElementsMap.clear();
+
+ instance = null;
+ }
+
+ initialize(options = {}) {
+
+ if (options.maxTooltips) {
+ this.maxTooltips = options.maxTooltips;
+ }
+
+ console.log('TooltipManager initialized');
+ return this;
+ }
+ }
+
+ return {
+ initialize: function(options = {}) {
+ if (!instance) {
+ const manager = new TooltipManagerImpl();
+ manager.initialize(options);
+ }
+ return instance;
+ },
+
+ getInstance: function() {
+ if (!instance) {
+ const manager = new TooltipManagerImpl();
+ }
+ return instance;
+ },
+
+ create: function(...args) {
+ const manager = this.getInstance();
+ return manager.create(...args);
+ },
+
+ destroy: function(...args) {
+ const manager = this.getInstance();
+ return manager.destroy(...args);
+ },
+
+ cleanup: function(...args) {
+ const manager = this.getInstance();
+ return manager.cleanup(...args);
+ },
+
+ initializeTooltips: function(...args) {
+ const manager = this.getInstance();
+ return manager.initializeTooltips(...args);
+ },
+
+ dispose: function(...args) {
+ const manager = this.getInstance();
+ return manager.dispose(...args);
+ }
+ };
+})();
+
+window.TooltipManager = TooltipManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.tooltipManagerInitialized) {
+ TooltipManager.initialize();
+ TooltipManager.initializeTooltips();
+ window.tooltipManagerInitialized = true;
+ }
+});
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = TooltipManager;
+}
+
+//console.log('TooltipManager initialized with methods:', Object.keys(TooltipManager));
+console.log('TooltipManager initialized');
diff --git a/basicswap/static/js/modules/wallet-manager.js b/basicswap/static/js/modules/wallet-manager.js
new file mode 100644
index 0000000..ac81e0d
--- /dev/null
+++ b/basicswap/static/js/modules/wallet-manager.js
@@ -0,0 +1,655 @@
+const WalletManager = (function() {
+
+ const config = {
+ maxRetries: 5,
+ baseDelay: 500,
+ cacheExpiration: 5 * 60 * 1000,
+ priceUpdateInterval: 5 * 60 * 1000,
+ apiTimeout: 30000,
+ debounceDelay: 300,
+ cacheMinInterval: 60 * 1000,
+ defaultTTL: 300,
+ priceSource: {
+ primary: 'coingecko.com',
+ fallback: 'cryptocompare.com',
+ enabledSources: ['coingecko.com', 'cryptocompare.com']
+ }
+ };
+
+ const stateKeys = {
+ lastUpdate: 'last-update-time',
+ previousTotal: 'previous-total-usd',
+ currentTotal: 'current-total-usd',
+ balancesVisible: 'balancesVisible'
+ };
+
+ const coinData = {
+ symbols: {
+ 'Bitcoin': 'BTC',
+ 'Particl': 'PART',
+ 'Monero': 'XMR',
+ 'Wownero': 'WOW',
+ 'Litecoin': 'LTC',
+ 'Dogecoin': 'DOGE',
+ 'Firo': 'FIRO',
+ 'Dash': 'DASH',
+ 'PIVX': 'PIVX',
+ 'Decred': 'DCR',
+ 'Bitcoin Cash': 'BCH'
+ },
+
+ coingeckoIds: {
+ 'BTC': 'btc',
+ 'PART': 'part',
+ 'XMR': 'xmr',
+ 'WOW': 'wownero',
+ 'LTC': 'ltc',
+ 'DOGE': 'doge',
+ 'FIRO': 'firo',
+ 'DASH': 'dash',
+ 'PIVX': 'pivx',
+ 'DCR': 'dcr',
+ 'BCH': 'bch'
+ },
+
+ shortNames: {
+ 'Bitcoin': 'BTC',
+ 'Particl': 'PART',
+ 'Monero': 'XMR',
+ 'Wownero': 'WOW',
+ 'Litecoin': 'LTC',
+ 'Litecoin MWEB': 'LTC MWEB',
+ 'Firo': 'FIRO',
+ 'Dash': 'DASH',
+ 'PIVX': 'PIVX',
+ 'Decred': 'DCR',
+ 'Bitcoin Cash': 'BCH',
+ 'Dogecoin': 'DOGE'
+ }
+ };
+
+ const state = {
+ lastFetchTime: 0,
+ toggleInProgress: false,
+ toggleDebounceTimer: null,
+ priceUpdateInterval: null,
+ lastUpdateTime: 0,
+ isWalletsPage: false,
+ initialized: false,
+ cacheKey: 'rates_crypto_prices'
+ };
+
+ function getShortName(fullName) {
+ return coinData.shortNames[fullName] || fullName;
+ }
+
+ async function fetchPrices(forceUpdate = false) {
+ const now = Date.now();
+ const timeSinceLastFetch = now - state.lastFetchTime;
+
+ if (!forceUpdate && timeSinceLastFetch < config.cacheMinInterval) {
+ const cachedData = CacheManager.get(state.cacheKey);
+ if (cachedData) {
+ return cachedData.value;
+ }
+ }
+
+ let lastError = null;
+ for (let attempt = 0; attempt < config.maxRetries; attempt++) {
+ try {
+ const processedData = {};
+ const currentSource = config.priceSource.primary;
+
+ const shouldIncludeWow = currentSource === 'coingecko.com';
+
+ const coinsToFetch = Object.values(coinData.symbols)
+ .filter(symbol => shouldIncludeWow || symbol !== 'WOW')
+ .map(symbol => coinData.coingeckoIds[symbol] || symbol.toLowerCase())
+ .join(',');
+
+ const mainResponse = await fetch("/json/coinprices", {
+ method: "POST",
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ coins: coinsToFetch,
+ source: currentSource,
+ ttl: config.defaultTTL
+ })
+ });
+
+ if (!mainResponse.ok) {
+ throw new Error(`HTTP error: ${mainResponse.status}`);
+ }
+
+ const mainData = await mainResponse.json();
+
+ if (mainData && mainData.rates) {
+ Object.entries(mainData.rates).forEach(([coinId, price]) => {
+ const symbol = Object.entries(coinData.coingeckoIds).find(([sym, id]) => id.toLowerCase() === coinId.toLowerCase())?.[0];
+ if (symbol) {
+ const coinKey = Object.keys(coinData.symbols).find(key => coinData.symbols[key] === symbol);
+ if (coinKey) {
+ processedData[coinKey.toLowerCase().replace(' ', '-')] = {
+ usd: price,
+ btc: symbol === 'BTC' ? 1 : price / (mainData.rates.btc || 1)
+ };
+ }
+ }
+ });
+ }
+
+ if (!shouldIncludeWow && !processedData['wownero']) {
+ try {
+ const wowResponse = await fetch("/json/coinprices", {
+ method: "POST",
+ headers: {'Content-Type': 'application/json'},
+ body: JSON.stringify({
+ coins: "wownero",
+ source: "coingecko.com",
+ ttl: config.defaultTTL
+ })
+ });
+
+ if (wowResponse.ok) {
+ const wowData = await wowResponse.json();
+ if (wowData && wowData.rates && wowData.rates.wownero) {
+ processedData['wownero'] = {
+ usd: wowData.rates.wownero,
+ btc: processedData.bitcoin ? wowData.rates.wownero / processedData.bitcoin.usd : 0
+ };
+ }
+ }
+ } catch (wowError) {
+ console.error('Error fetching WOW price:', wowError);
+ }
+ }
+
+ CacheManager.set(state.cacheKey, processedData, config.cacheExpiration);
+ state.lastFetchTime = now;
+ return processedData;
+ } catch (error) {
+ lastError = error;
+ console.error(`Price fetch attempt ${attempt + 1} failed:`, error);
+
+ if (attempt === config.maxRetries - 1 &&
+ config.priceSource.fallback &&
+ config.priceSource.fallback !== config.priceSource.primary) {
+ const temp = config.priceSource.primary;
+ config.priceSource.primary = config.priceSource.fallback;
+ config.priceSource.fallback = temp;
+
+ console.warn(`Switching to fallback source: ${config.priceSource.primary}`);
+ attempt = -1;
+ continue;
+ }
+
+ if (attempt < config.maxRetries - 1) {
+ const delay = Math.min(config.baseDelay * Math.pow(2, attempt), 10000);
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+ }
+
+ const cachedData = CacheManager.get(state.cacheKey);
+ if (cachedData) {
+ console.warn('Using cached data after fetch failures');
+ return cachedData.value;
+ }
+
+ throw lastError || new Error('Failed to fetch prices');
+ }
+
+ // UI Management functions
+ function storeOriginalValues() {
+ document.querySelectorAll('.coinname-value').forEach(el => {
+ const coinName = el.getAttribute('data-coinname');
+ const value = el.textContent?.trim() || '';
+
+ if (coinName) {
+ const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
+ const coinId = coinData.symbols[coinName];
+ const shortName = getShortName(coinName);
+
+ if (coinId) {
+ if (coinName === 'Particl') {
+ const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
+ const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
+ const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
+ localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
+ } else if (coinName === 'Litecoin') {
+ const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
+ const balanceType = isMWEB ? 'mweb' : 'public';
+ localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
+ } else {
+ localStorage.setItem(`${coinId.toLowerCase()}-amount`, amount.toString());
+ }
+
+ el.setAttribute('data-original-value', `${amount} ${shortName}`);
+ }
+ }
+ });
+
+ document.querySelectorAll('.usd-value').forEach(el => {
+ const text = el.textContent?.trim() || '';
+ if (text === 'Loading...') {
+ el.textContent = '';
+ }
+ });
+ }
+
+ async function updatePrices(forceUpdate = false) {
+ try {
+ const prices = await fetchPrices(forceUpdate);
+ let newTotal = 0;
+
+ const currentTime = Date.now();
+ localStorage.setItem(stateKeys.lastUpdate, currentTime.toString());
+ state.lastUpdateTime = currentTime;
+
+ if (prices) {
+ Object.entries(prices).forEach(([coinId, priceData]) => {
+ if (priceData?.usd) {
+ localStorage.setItem(`${coinId}-price`, priceData.usd.toString());
+ }
+ });
+ }
+
+ document.querySelectorAll('.coinname-value').forEach(el => {
+ const coinName = el.getAttribute('data-coinname');
+ const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
+
+ if (!coinName) return;
+
+ let amount = 0;
+ if (amountStr) {
+ const matches = amountStr.match(/([0-9]*[.])?[0-9]+/);
+ if (matches && matches.length > 0) {
+ amount = parseFloat(matches[0]);
+ }
+ }
+
+ const coinId = coinName.toLowerCase().replace(' ', '-');
+
+ if (!prices[coinId]) {
+ return;
+ }
+
+ const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
+ if (!price) return;
+
+ const usdValue = (amount * price).toFixed(2);
+
+ if (coinName === 'Particl') {
+ const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
+ const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
+ const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
+ localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
+ localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
+ } else if (coinName === 'Litecoin') {
+ const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
+ const balanceType = isMWEB ? 'mweb' : 'public';
+ localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
+ localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
+ } else {
+ localStorage.setItem(`${coinId}-last-value`, usdValue);
+ localStorage.setItem(`${coinId}-amount`, amount.toString());
+ }
+
+ if (amount > 0) {
+ newTotal += parseFloat(usdValue);
+ }
+
+ let usdEl = null;
+
+ const flexContainer = el.closest('.flex');
+ if (flexContainer) {
+ const nextFlex = flexContainer.nextElementSibling;
+ if (nextFlex) {
+ const usdInNextFlex = nextFlex.querySelector('.usd-value');
+ if (usdInNextFlex) {
+ usdEl = usdInNextFlex;
+ }
+ }
+ }
+
+ if (!usdEl) {
+ const parentCell = el.closest('td');
+ if (parentCell) {
+ const usdInSameCell = parentCell.querySelector('.usd-value');
+ if (usdInSameCell) {
+ usdEl = usdInSameCell;
+ }
+ }
+ }
+
+ if (!usdEl) {
+ const sibling = el.nextElementSibling;
+ if (sibling && sibling.classList.contains('usd-value')) {
+ usdEl = sibling;
+ }
+ }
+
+ if (!usdEl) {
+ const parentElement = el.parentElement;
+ if (parentElement) {
+ const usdElNearby = parentElement.querySelector('.usd-value');
+ if (usdElNearby) {
+ usdEl = usdElNearby;
+ }
+ }
+ }
+
+ if (usdEl) {
+ usdEl.textContent = `$${usdValue}`;
+ usdEl.setAttribute('data-original-value', usdValue);
+ }
+ });
+
+ document.querySelectorAll('.usd-value').forEach(el => {
+ if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
+ const parentCell = el.closest('td');
+ if (!parentCell) return;
+
+ const coinValueEl = parentCell.querySelector('.coinname-value');
+ if (!coinValueEl) return;
+
+ const coinName = coinValueEl.getAttribute('data-coinname');
+ if (!coinName) return;
+
+ const amountStr = coinValueEl.textContent?.trim() || '0';
+ const amount = parseFloat(amountStr) || 0;
+
+ const coinId = coinName.toLowerCase().replace(' ', '-');
+ if (!prices[coinId]) return;
+
+ const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
+ if (!price) return;
+
+ const usdValue = (amount * price).toFixed(8);
+ el.textContent = `$${usdValue}`;
+ el.setAttribute('data-original-value', usdValue);
+ }
+ });
+
+ if (state.isWalletsPage) {
+ updateTotalValues(newTotal, prices?.bitcoin?.usd);
+ }
+
+ localStorage.setItem(stateKeys.previousTotal, localStorage.getItem(stateKeys.currentTotal) || '0');
+ localStorage.setItem(stateKeys.currentTotal, newTotal.toString());
+
+ return true;
+ } catch (error) {
+ console.error('Price update failed:', error);
+ return false;
+ }
+ }
+
+ function updateTotalValues(totalUsd, btcPrice) {
+ const totalUsdEl = document.getElementById('total-usd-value');
+ if (totalUsdEl) {
+ totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
+ totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
+ localStorage.setItem('total-usd', totalUsd.toString());
+ }
+
+ if (btcPrice) {
+ const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
+ const totalBtcEl = document.getElementById('total-btc-value');
+ if (totalBtcEl) {
+ totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
+ totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
+ }
+ }
+ }
+
+ async function toggleBalances() {
+ if (state.toggleInProgress) return;
+
+ try {
+ state.toggleInProgress = true;
+ const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
+ const newVisibility = !balancesVisible;
+
+ localStorage.setItem('balancesVisible', newVisibility.toString());
+ updateVisibility(newVisibility);
+
+ if (state.toggleDebounceTimer) {
+ clearTimeout(state.toggleDebounceTimer);
+ }
+
+ state.toggleDebounceTimer = window.setTimeout(async () => {
+ state.toggleInProgress = false;
+ if (newVisibility) {
+ await updatePrices(true);
+ }
+ }, config.debounceDelay);
+ } catch (error) {
+ console.error('Failed to toggle balances:', error);
+ state.toggleInProgress = false;
+ }
+ }
+
+ function updateVisibility(isVisible) {
+ if (isVisible) {
+ showBalances();
+ } else {
+ hideBalances();
+ }
+
+ const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
+ if (eyeIcon) {
+ eyeIcon.innerHTML = isVisible ?
+ ' ' :
+ ' ';
+ }
+ }
+
+ function showBalances() {
+ const usdText = document.getElementById('usd-text');
+ if (usdText) {
+ usdText.style.display = 'inline';
+ }
+
+ document.querySelectorAll('.coinname-value').forEach(el => {
+ const originalValue = el.getAttribute('data-original-value');
+ if (originalValue) {
+ el.textContent = originalValue;
+ }
+ });
+
+ document.querySelectorAll('.usd-value').forEach(el => {
+ const storedValue = el.getAttribute('data-original-value');
+ if (storedValue !== null && storedValue !== undefined) {
+ if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
+ el.textContent = `$${parseFloat(storedValue).toFixed(8)}`;
+ } else {
+ el.textContent = `$${parseFloat(storedValue).toFixed(2)}`;
+ }
+ } else {
+ if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
+ el.textContent = '$0.00000000';
+ } else {
+ el.textContent = '$0.00';
+ }
+ }
+ });
+
+ if (state.isWalletsPage) {
+ ['total-usd-value', 'total-btc-value'].forEach(id => {
+ const el = document.getElementById(id);
+ const originalValue = el?.getAttribute('data-original-value');
+ if (el && originalValue) {
+ if (id === 'total-usd-value') {
+ el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
+ el.classList.add('font-extrabold');
+ } else {
+ el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
+ }
+ }
+ });
+ }
+ }
+
+ function hideBalances() {
+ const usdText = document.getElementById('usd-text');
+ if (usdText) {
+ usdText.style.display = 'none';
+ }
+
+ document.querySelectorAll('.coinname-value').forEach(el => {
+ el.textContent = '****';
+ });
+
+ document.querySelectorAll('.usd-value').forEach(el => {
+ el.textContent = '****';
+ });
+
+ if (state.isWalletsPage) {
+ ['total-usd-value', 'total-btc-value'].forEach(id => {
+ const el = document.getElementById(id);
+ if (el) {
+ el.textContent = '****';
+ }
+ });
+
+ const totalUsdEl = document.getElementById('total-usd-value');
+ if (totalUsdEl) {
+ totalUsdEl.classList.remove('font-extrabold');
+ }
+ }
+ }
+
+ async function loadBalanceVisibility() {
+ const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
+ updateVisibility(balancesVisible);
+
+ if (balancesVisible) {
+ await updatePrices(true);
+ }
+ }
+
+ // Public API
+ const publicAPI = {
+ initialize: async function(options) {
+ if (state.initialized) {
+ console.warn('[WalletManager] Already initialized');
+ return this;
+ }
+
+ if (options) {
+ Object.assign(config, options);
+ }
+
+ state.lastUpdateTime = parseInt(localStorage.getItem(stateKeys.lastUpdate) || '0');
+ state.isWalletsPage = document.querySelector('.wallet-list') !== null ||
+ window.location.pathname.includes('/wallets');
+
+ document.querySelectorAll('.usd-value').forEach(el => {
+ const text = el.textContent?.trim() || '';
+ if (text === 'Loading...') {
+ el.textContent = '';
+ }
+ });
+
+ storeOriginalValues();
+
+ if (localStorage.getItem('balancesVisible') === null) {
+ localStorage.setItem('balancesVisible', 'true');
+ }
+
+ const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
+ if (hideBalancesToggle) {
+ hideBalancesToggle.addEventListener('click', toggleBalances);
+ }
+
+ await loadBalanceVisibility();
+
+ if (state.priceUpdateInterval) {
+ clearInterval(state.priceUpdateInterval);
+ }
+
+ state.priceUpdateInterval = setInterval(() => {
+ if (localStorage.getItem('balancesVisible') === 'true' && !state.toggleInProgress) {
+ updatePrices(false);
+ }
+ }, config.priceUpdateInterval);
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('walletManager', this, (mgr) => mgr.dispose());
+ }
+
+ state.initialized = true;
+ console.log('WalletManager initialized');
+
+ return this;
+ },
+
+ updatePrices: function(forceUpdate = false) {
+ return updatePrices(forceUpdate);
+ },
+
+ toggleBalances: function() {
+ return toggleBalances();
+ },
+
+ setPriceSource: function(primarySource, fallbackSource = null) {
+ if (!config.priceSource.enabledSources.includes(primarySource)) {
+ throw new Error(`Invalid primary source: ${primarySource}`);
+ }
+
+ if (fallbackSource && !config.priceSource.enabledSources.includes(fallbackSource)) {
+ throw new Error(`Invalid fallback source: ${fallbackSource}`);
+ }
+
+ config.priceSource.primary = primarySource;
+ if (fallbackSource) {
+ config.priceSource.fallback = fallbackSource;
+ }
+
+ return this;
+ },
+
+ getConfig: function() {
+ return { ...config };
+ },
+
+ getState: function() {
+ return {
+ initialized: state.initialized,
+ lastUpdateTime: state.lastUpdateTime,
+ isWalletsPage: state.isWalletsPage,
+ balancesVisible: localStorage.getItem('balancesVisible') === 'true'
+ };
+ },
+
+ dispose: function() {
+ if (state.priceUpdateInterval) {
+ clearInterval(state.priceUpdateInterval);
+ state.priceUpdateInterval = null;
+ }
+
+ if (state.toggleDebounceTimer) {
+ clearTimeout(state.toggleDebounceTimer);
+ state.toggleDebounceTimer = null;
+ }
+
+ state.initialized = false;
+ console.log('WalletManager disposed');
+ }
+ };
+
+ return publicAPI;
+})();
+
+window.WalletManager = WalletManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+ if (!window.walletManagerInitialized) {
+ WalletManager.initialize();
+ window.walletManagerInitialized = true;
+ }
+});
+
+//console.log('WalletManager initialized with methods:', Object.keys(WalletManager));
+console.log('WalletManager initialized');
diff --git a/basicswap/static/js/modules/websocket-manager.js b/basicswap/static/js/modules/websocket-manager.js
new file mode 100644
index 0000000..1aae881
--- /dev/null
+++ b/basicswap/static/js/modules/websocket-manager.js
@@ -0,0 +1,444 @@
+const WebSocketManager = (function() {
+ let ws = null;
+
+ const config = {
+ reconnectAttempts: 0,
+ maxReconnectAttempts: 5,
+ reconnectDelay: 5000,
+ debug: false
+ };
+
+ const state = {
+ isConnecting: false,
+ isIntentionallyClosed: false,
+ lastConnectAttempt: null,
+ connectTimeout: null,
+ lastHealthCheck: null,
+ healthCheckInterval: null,
+ isPageHidden: document.hidden,
+ messageHandlers: {},
+ listeners: {},
+ reconnectTimeout: null
+ };
+
+ function log(message, ...args) {
+ if (config.debug) {
+ console.log(`[WebSocketManager] ${message}`, ...args);
+ }
+ }
+
+ function generateHandlerId() {
+ return `handler_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
+ }
+
+ function determineWebSocketPort() {
+ let wsPort;
+
+ if (window.config && window.config.wsPort) {
+ wsPort = window.config.wsPort;
+ return wsPort;
+ }
+
+ if (window.ws_port) {
+ wsPort = window.ws_port.toString();
+ return wsPort;
+ }
+
+ if (typeof getWebSocketConfig === 'function') {
+ const wsConfig = getWebSocketConfig();
+ wsPort = (wsConfig.port || wsConfig.fallbackPort || '11700').toString();
+ return wsPort;
+ }
+
+ wsPort = '11700';
+ return wsPort;
+ }
+
+ const publicAPI = {
+ initialize: function(options = {}) {
+ Object.assign(config, options);
+ setupPageVisibilityHandler();
+ this.connect();
+ startHealthCheck();
+
+ log('WebSocketManager initialized with options:', options);
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('webSocketManager', this, (mgr) => mgr.dispose());
+ }
+
+ return this;
+ },
+
+ connect: function() {
+ if (state.isConnecting || state.isIntentionallyClosed) {
+ log('Connection attempt blocked - already connecting or intentionally closed');
+ return false;
+ }
+
+ if (state.reconnectTimeout) {
+ clearTimeout(state.reconnectTimeout);
+ state.reconnectTimeout = null;
+ }
+
+ cleanup();
+ state.isConnecting = true;
+ state.lastConnectAttempt = Date.now();
+
+ try {
+ const wsPort = determineWebSocketPort();
+
+ if (!wsPort) {
+ state.isConnecting = false;
+ return false;
+ }
+
+ ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
+ setupEventHandlers();
+
+ state.connectTimeout = setTimeout(() => {
+ if (state.isConnecting) {
+ log('Connection timeout, cleaning up');
+ cleanup();
+ handleReconnect();
+ }
+ }, 5000);
+
+ return true;
+ } catch (error) {
+ log('Error during connection attempt:', error);
+ state.isConnecting = false;
+ handleReconnect();
+ return false;
+ }
+ },
+
+ disconnect: function() {
+ log('Disconnecting WebSocket');
+ state.isIntentionallyClosed = true;
+ cleanup();
+ stopHealthCheck();
+ },
+
+ isConnected: function() {
+ return ws && ws.readyState === WebSocket.OPEN;
+ },
+
+ sendMessage: function(message) {
+ if (!this.isConnected()) {
+ log('Cannot send message - not connected');
+ return false;
+ }
+
+ try {
+ ws.send(JSON.stringify(message));
+ return true;
+ } catch (error) {
+ log('Error sending message:', error);
+ return false;
+ }
+ },
+
+ addMessageHandler: function(type, handler) {
+ if (!state.messageHandlers[type]) {
+ state.messageHandlers[type] = {};
+ }
+
+ const handlerId = generateHandlerId();
+ state.messageHandlers[type][handlerId] = handler;
+
+ return handlerId;
+ },
+
+ removeMessageHandler: function(type, handlerId) {
+ if (state.messageHandlers[type] && state.messageHandlers[type][handlerId]) {
+ delete state.messageHandlers[type][handlerId];
+ }
+ },
+
+ cleanup: function() {
+ log('Cleaning up WebSocket resources');
+
+ clearTimeout(state.connectTimeout);
+ stopHealthCheck();
+
+ if (state.reconnectTimeout) {
+ clearTimeout(state.reconnectTimeout);
+ state.reconnectTimeout = null;
+ }
+
+ state.isConnecting = false;
+
+ if (ws) {
+ ws.onopen = null;
+ ws.onmessage = null;
+ ws.onerror = null;
+ ws.onclose = null;
+
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.close(1000, 'Cleanup');
+ }
+
+ ws = null;
+ window.ws = null;
+ }
+ },
+
+ dispose: function() {
+ log('Disposing WebSocketManager');
+
+ this.disconnect();
+
+ if (state.listeners.visibilityChange) {
+ document.removeEventListener('visibilitychange', state.listeners.visibilityChange);
+ }
+
+ state.messageHandlers = {};
+ state.listeners = {};
+ },
+
+ pause: function() {
+ log('WebSocketManager paused');
+ state.isIntentionallyClosed = true;
+
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ ws.close(1000, 'WebSocketManager paused');
+ }
+
+ stopHealthCheck();
+ },
+
+ resume: function() {
+ log('WebSocketManager resumed');
+ state.isIntentionallyClosed = false;
+
+ if (!this.isConnected()) {
+ this.connect();
+ }
+
+ startHealthCheck();
+ }
+ };
+
+ function setupEventHandlers() {
+ if (!ws) return;
+
+ ws.onopen = () => {
+ state.isConnecting = false;
+ config.reconnectAttempts = 0;
+ clearTimeout(state.connectTimeout);
+ state.lastHealthCheck = Date.now();
+ window.ws = ws;
+
+ log('WebSocket connection established');
+
+ notifyHandlers('connect', { isConnected: true });
+
+ if (typeof updateConnectionStatus === 'function') {
+ updateConnectionStatus('connected');
+ }
+ };
+
+ ws.onmessage = (event) => {
+ try {
+ const message = JSON.parse(event.data);
+ log('WebSocket message received:', message);
+ notifyHandlers('message', message);
+ } catch (error) {
+ log('Error processing message:', error);
+ if (typeof updateConnectionStatus === 'function') {
+ updateConnectionStatus('error');
+ }
+ }
+ };
+
+ ws.onerror = (error) => {
+ log('WebSocket error:', error);
+ if (typeof updateConnectionStatus === 'function') {
+ updateConnectionStatus('error');
+ }
+ notifyHandlers('error', error);
+ };
+
+ ws.onclose = (event) => {
+ log('WebSocket closed:', event);
+ state.isConnecting = false;
+ window.ws = null;
+
+ if (typeof updateConnectionStatus === 'function') {
+ updateConnectionStatus('disconnected');
+ }
+
+ notifyHandlers('disconnect', {
+ code: event.code,
+ reason: event.reason
+ });
+
+ if (!state.isIntentionallyClosed) {
+ handleReconnect();
+ }
+ };
+ }
+
+ function setupPageVisibilityHandler() {
+ const visibilityChangeHandler = () => {
+ if (document.hidden) {
+ handlePageHidden();
+ } else {
+ handlePageVisible();
+ }
+ };
+
+ document.addEventListener('visibilitychange', visibilityChangeHandler);
+ state.listeners.visibilityChange = visibilityChangeHandler;
+ }
+
+ function handlePageHidden() {
+ log('Page hidden');
+ state.isPageHidden = true;
+ stopHealthCheck();
+
+ if (ws && ws.readyState === WebSocket.OPEN) {
+ state.isIntentionallyClosed = true;
+ ws.close(1000, 'Page hidden');
+ }
+ }
+
+ function handlePageVisible() {
+ log('Page visible');
+ state.isPageHidden = false;
+ state.isIntentionallyClosed = false;
+
+ setTimeout(() => {
+ if (!publicAPI.isConnected()) {
+ publicAPI.connect();
+ }
+ startHealthCheck();
+ }, 0);
+ }
+
+ function startHealthCheck() {
+ stopHealthCheck();
+ state.healthCheckInterval = setInterval(() => {
+ performHealthCheck();
+ }, 30000);
+ }
+
+ function stopHealthCheck() {
+ if (state.healthCheckInterval) {
+ clearInterval(state.healthCheckInterval);
+ state.healthCheckInterval = null;
+ }
+ }
+
+ function performHealthCheck() {
+ if (!publicAPI.isConnected()) {
+ log('Health check failed - not connected');
+ handleReconnect();
+ return;
+ }
+
+ const now = Date.now();
+ const lastCheck = state.lastHealthCheck;
+
+ if (lastCheck && (now - lastCheck) > 60000) {
+ log('Health check failed - too long since last check');
+ handleReconnect();
+ return;
+ }
+
+ state.lastHealthCheck = now;
+ log('Health check passed');
+ }
+
+ function handleReconnect() {
+
+ if (state.reconnectTimeout) {
+ clearTimeout(state.reconnectTimeout);
+ state.reconnectTimeout = null;
+ }
+
+ config.reconnectAttempts++;
+ if (config.reconnectAttempts <= config.maxReconnectAttempts) {
+ const delay = Math.min(
+ config.reconnectDelay * Math.pow(1.5, config.reconnectAttempts - 1),
+ 30000
+ );
+
+ log(`Scheduling reconnect in ${delay}ms (attempt ${config.reconnectAttempts})`);
+
+ state.reconnectTimeout = setTimeout(() => {
+ state.reconnectTimeout = null;
+ if (!state.isIntentionallyClosed) {
+ publicAPI.connect();
+ }
+ }, delay);
+ } else {
+ log('Max reconnect attempts reached');
+ if (typeof updateConnectionStatus === 'function') {
+ updateConnectionStatus('error');
+ }
+
+ state.reconnectTimeout = setTimeout(() => {
+ state.reconnectTimeout = null;
+ config.reconnectAttempts = 0;
+ publicAPI.connect();
+ }, 60000);
+ }
+ }
+
+ function notifyHandlers(type, data) {
+ if (state.messageHandlers[type]) {
+ Object.values(state.messageHandlers[type]).forEach(handler => {
+ try {
+ handler(data);
+ } catch (error) {
+ log(`Error in ${type} handler:`, error);
+ }
+ });
+ }
+ }
+
+ function cleanup() {
+ log('Cleaning up WebSocket resources');
+
+ clearTimeout(state.connectTimeout);
+ stopHealthCheck();
+
+ if (state.reconnectTimeout) {
+ clearTimeout(state.reconnectTimeout);
+ state.reconnectTimeout = null;
+ }
+
+ state.isConnecting = false;
+
+ if (ws) {
+ ws.onopen = null;
+ ws.onmessage = null;
+ ws.onerror = null;
+ ws.onclose = null;
+
+ if (ws.readyState === WebSocket.OPEN) {
+ ws.close(1000, 'Cleanup');
+ }
+
+ ws = null;
+ window.ws = null;
+ }
+ }
+
+ return publicAPI;
+})();
+
+window.WebSocketManager = WebSocketManager;
+
+document.addEventListener('DOMContentLoaded', function() {
+
+ if (!window.webSocketManagerInitialized) {
+ window.WebSocketManager.initialize();
+ window.webSocketManagerInitialized = true;
+ }
+});
+
+//console.log('WebSocketManager initialized with methods:', Object.keys(WebSocketManager));
+console.log('WebSocketManager initialized');
diff --git a/basicswap/static/js/new_offer.js b/basicswap/static/js/new_offer.js
index 058e8dc..c88df43 100644
--- a/basicswap/static/js/new_offer.js
+++ b/basicswap/static/js/new_offer.js
@@ -1,59 +1,548 @@
-window.addEventListener('DOMContentLoaded', () => {
- const err_msgs = document.querySelectorAll('p.error_msg');
- for (let i = 0; i < err_msgs.length; i++) {
- err_msg = err_msgs[i].innerText;
- if (err_msg.indexOf('coin_to') >= 0 || err_msg.indexOf('Coin To') >= 0) {
- e = document.getElementById('coin_to');
- e.classList.add('error');
- }
- if (err_msg.indexOf('Coin From') >= 0) {
- e = document.getElementById('coin_from');
- e.classList.add('error');
- }
- if (err_msg.indexOf('Amount From') >= 0) {
- e = document.getElementById('amt_from');
- e.classList.add('error');
- }
- if (err_msg.indexOf('Amount To') >= 0) {
- e = document.getElementById('amt_to');
- e.classList.add('error');
- }
- if (err_msg.indexOf('Minimum Bid Amount') >= 0) {
- e = document.getElementById('amt_bid_min');
- e.classList.add('error');
- }
- if (err_msg.indexOf('Select coin you send') >= 0) {
- e = document.getElementById('coin_from').parentNode;
- e.classList.add('error');
- }
- }
+const DOM = {
+ get: (id) => document.getElementById(id),
+ getValue: (id) => {
+ const el = document.getElementById(id);
+ return el ? el.value : '';
+ },
+ setValue: (id, value) => {
+ const el = document.getElementById(id);
+ if (el) el.value = value;
+ },
+ addEvent: (id, event, handler) => {
+ const el = document.getElementById(id);
+ if (el) el.addEventListener(event, handler);
+ },
+ query: (selector) => document.querySelector(selector),
+ queryAll: (selector) => document.querySelectorAll(selector)
+};
- // remove error class on input or select focus
- const inputs = document.querySelectorAll('input.error');
- const selects = document.querySelectorAll('select.error');
- const elements = [...inputs, ...selects];
- elements.forEach((element) => {
- element.addEventListener('focus', (event) => {
- event.target.classList.remove('error');
+const Storage = {
+ get: (key) => {
+ try {
+ return JSON.parse(localStorage.getItem(key));
+ } catch(e) {
+ console.warn(`Failed to retrieve item from storage: ${key}`, e);
+ return null;
+ }
+ },
+ set: (key, value) => {
+ try {
+ localStorage.setItem(key, JSON.stringify(value));
+ return true;
+ } catch(e) {
+ console.error(`Failed to save item to storage: ${key}`, e);
+ return false;
+ }
+ },
+ setRaw: (key, value) => {
+ try {
+ localStorage.setItem(key, value);
+ return true;
+ } catch(e) {
+ console.error(`Failed to save raw item to storage: ${key}`, e);
+ return false;
+ }
+ },
+ getRaw: (key) => {
+ try {
+ return localStorage.getItem(key);
+ } catch(e) {
+ console.warn(`Failed to retrieve raw item from storage: ${key}`, e);
+ return null;
+ }
+ }
+};
+
+const Ajax = {
+ post: (url, data, onSuccess, onError) => {
+ const xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState !== XMLHttpRequest.DONE) return;
+ if (xhr.status === 200) {
+ if (onSuccess) {
+ try {
+ const response = xhr.responseText.startsWith('{') ?
+ JSON.parse(xhr.responseText) : xhr.responseText;
+ onSuccess(response);
+ } catch (e) {
+ console.error('Failed to parse response:', e);
+ if (onError) onError('Invalid response format');
+ }
+ }
+ } else {
+ console.error('Request failed:', xhr.statusText);
+ if (onError) onError(xhr.statusText);
+ }
+ };
+ xhr.open('POST', url);
+ xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
+ xhr.send(data);
+ return xhr;
+ }
+};
+
+function handleNewOfferAddress() {
+ const STORAGE_KEY = 'lastUsedAddressNewOffer';
+ const selectElement = DOM.query('select[name="addr_from"]');
+ const form = selectElement?.closest('form');
+
+ if (!selectElement || !form) return;
+
+ function loadInitialAddress() {
+ const savedAddress = Storage.get(STORAGE_KEY);
+ if (savedAddress) {
+ try {
+ selectElement.value = savedAddress.value;
+ } catch (e) {
+ selectFirstAddress();
+ }
+ } else {
+ selectFirstAddress();
+ }
+ }
+
+ function selectFirstAddress() {
+ if (selectElement.options.length > 1) {
+ const firstOption = selectElement.options[1];
+ if (firstOption) {
+ selectElement.value = firstOption.value;
+ saveAddress(firstOption.value, firstOption.text);
+ }
+ }
+ }
+
+ function saveAddress(value, text) {
+ Storage.set(STORAGE_KEY, { value, text });
+ }
+
+ form.addEventListener('submit', () => {
+ saveAddress(selectElement.value, selectElement.selectedOptions[0].text);
});
- });
-});
-const selects = document.querySelectorAll('select.disabled-select');
-for (const select of selects) {
- if (select.disabled) {
- select.classList.add('disabled-select-enabled');
- } else {
- select.classList.remove('disabled-select-enabled');
- }
+ selectElement.addEventListener('change', (event) => {
+ saveAddress(event.target.value, event.target.selectedOptions[0].text);
+ });
+
+ loadInitialAddress();
}
+const RateManager = {
+ lookupRates: () => {
+ const coinFrom = DOM.getValue('coin_from');
+ const coinTo = DOM.getValue('coin_to');
+ const ratesDisplay = DOM.get('rates_display');
-const inputs = document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input');
-for (const input of inputs) {
- if (input.readOnly) {
- input.classList.add('disabled-input-enabled');
- } else {
- input.classList.remove('disabled-input-enabled');
+ if (!coinFrom || !coinTo || !ratesDisplay) {
+ console.log('Required elements for lookup_rates not found');
+ return;
+ }
+
+ if (coinFrom === '-1' || coinTo === '-1') {
+ alert('Coins from and to must be set first.');
+ return;
+ }
+
+ const selectedCoin = (coinFrom === '15') ? '3' : coinFrom;
+
+ ratesDisplay.innerHTML = ' Updating... ';
+
+ const priceJsonElement = DOM.query(".pricejsonhidden");
+ if (priceJsonElement) {
+ priceJsonElement.classList.remove("hidden");
+ }
+
+ const params = 'coin_from=' + selectedCoin + '&coin_to=' + coinTo;
+
+ Ajax.post('/json/rates', params,
+ (response) => {
+ if (ratesDisplay) {
+ ratesDisplay.innerHTML = typeof response === 'string' ?
+ response : ' ' + JSON.stringify(response, null, ' ') + '
';
+ }
+ },
+ (error) => {
+ if (ratesDisplay) {
+ ratesDisplay.innerHTML = ' Error loading rates: ' + error + ' ';
+ }
+ }
+ );
+ },
+
+ getRateInferred: (event) => {
+ if (event) event.preventDefault();
+
+ const coinFrom = DOM.getValue('coin_from');
+ const coinTo = DOM.getValue('coin_to');
+ const rateElement = DOM.get('rate');
+
+ if (!coinFrom || !coinTo || !rateElement) {
+ console.log('Required elements for getRateInferred not found');
+ return;
+ }
+
+ const params = 'coin_from=' + encodeURIComponent(coinFrom) +
+ '&coin_to=' + encodeURIComponent(coinTo);
+
+ DOM.setValue('rate', 'Loading...');
+
+ Ajax.post('/json/rates', params,
+ (response) => {
+ if (response.coingecko && response.coingecko.rate_inferred) {
+ DOM.setValue('rate', response.coingecko.rate_inferred);
+ RateManager.setRate('rate');
+ } else {
+ DOM.setValue('rate', 'Error: No rate available');
+ console.error('Rate not available in response');
+ }
+ },
+ (error) => {
+ DOM.setValue('rate', 'Error: Rate lookup failed');
+ console.error('Error fetching rate data:', error);
+ }
+ );
+ },
+
+ setRate: (valueChanged) => {
+ const elements = {
+ coinFrom: DOM.get('coin_from'),
+ coinTo: DOM.get('coin_to'),
+ amtFrom: DOM.get('amt_from'),
+ amtTo: DOM.get('amt_to'),
+ rate: DOM.get('rate'),
+ rateLock: DOM.get('rate_lock'),
+ swapType: DOM.get('swap_type')
+ };
+
+ if (!elements.coinFrom || !elements.coinTo ||
+ !elements.amtFrom || !elements.amtTo || !elements.rate) {
+ console.log('Required elements for setRate not found');
+ return;
+ }
+
+ const values = {
+ coinFrom: elements.coinFrom.value,
+ coinTo: elements.coinTo.value,
+ amtFrom: elements.amtFrom.value,
+ amtTo: elements.amtTo.value,
+ rate: elements.rate.value,
+ lockRate: elements.rate.value == '' ? false :
+ (elements.rateLock ? elements.rateLock.checked : false)
+ };
+
+ if (valueChanged === 'coin_from' || valueChanged === 'coin_to') {
+ DOM.setValue('rate', '');
+ return;
+ }
+
+ if (elements.swapType) {
+ SwapTypeManager.setSwapTypeEnabled(
+ values.coinFrom,
+ values.coinTo,
+ elements.swapType
+ );
+ }
+
+ if (values.coinFrom == '-1' || values.coinTo == '-1') {
+ return;
+ }
+
+ let params = 'coin_from=' + values.coinFrom + '&coin_to=' + values.coinTo;
+
+ if (valueChanged == 'rate' ||
+ (values.lockRate && valueChanged == 'amt_from') ||
+ (values.amtTo == '' && valueChanged == 'amt_from')) {
+
+ if (values.rate == '' || (values.amtFrom == '' && values.amtTo == '')) {
+ return;
+ } else if (values.amtFrom == '' && values.amtTo != '') {
+ if (valueChanged == 'amt_from') {
+ return;
+ }
+ params += '&rate=' + values.rate + '&amt_to=' + values.amtTo;
+ } else {
+ params += '&rate=' + values.rate + '&amt_from=' + values.amtFrom;
+ }
+ } else if (values.lockRate && valueChanged == 'amt_to') {
+ if (values.amtTo == '' || values.rate == '') {
+ return;
+ }
+ params += '&amt_to=' + values.amtTo + '&rate=' + values.rate;
+ } else {
+ if (values.amtFrom == '' || values.amtTo == '') {
+ return;
+ }
+ params += '&amt_from=' + values.amtFrom + '&amt_to=' + values.amtTo;
+ }
+
+ Ajax.post('/json/rate', params,
+ (response) => {
+ if (response.hasOwnProperty('rate')) {
+ DOM.setValue('rate', response.rate);
+ } else if (response.hasOwnProperty('amount_to')) {
+ DOM.setValue('amt_to', response.amount_to);
+ } else if (response.hasOwnProperty('amount_from')) {
+ DOM.setValue('amt_from', response.amount_from);
+ }
+ },
+ (error) => {
+ console.error('Rate calculation failed:', error);
+ }
+ );
}
+};
+
+function set_rate(valueChanged) {
+ RateManager.setRate(valueChanged);
+}
+
+function lookup_rates() {
+ RateManager.lookupRates();
+}
+
+function getRateInferred(event) {
+ RateManager.getRateInferred(event);
+}
+
+const SwapTypeManager = {
+ adaptor_sig_only_coins: ['6', '9', '8', '7', '13', '18', '17'],
+ secret_hash_only_coins: ['11', '12'],
+
+ setSwapTypeEnabled: (coinFrom, coinTo, swapTypeElement) => {
+ if (!swapTypeElement) return;
+
+ let makeHidden = false;
+ coinFrom = String(coinFrom);
+ coinTo = String(coinTo);
+
+ if (SwapTypeManager.adaptor_sig_only_coins.includes(coinFrom) ||
+ SwapTypeManager.adaptor_sig_only_coins.includes(coinTo)) {
+ swapTypeElement.disabled = true;
+ swapTypeElement.value = 'xmr_swap';
+ makeHidden = true;
+ swapTypeElement.classList.add('select-disabled');
+ } else if (SwapTypeManager.secret_hash_only_coins.includes(coinFrom) ||
+ SwapTypeManager.secret_hash_only_coins.includes(coinTo)) {
+ swapTypeElement.disabled = true;
+ swapTypeElement.value = 'seller_first';
+ makeHidden = true;
+ swapTypeElement.classList.add('select-disabled');
+ } else {
+ swapTypeElement.disabled = false;
+ swapTypeElement.classList.remove('select-disabled');
+ swapTypeElement.value = 'xmr_swap';
+ }
+
+ let swapTypeHidden = DOM.get('swap_type_hidden');
+ if (makeHidden) {
+ if (!swapTypeHidden) {
+ const form = DOM.get('form');
+ if (form) {
+ swapTypeHidden = document.createElement('input');
+ swapTypeHidden.setAttribute('id', 'swap_type_hidden');
+ swapTypeHidden.setAttribute('type', 'hidden');
+ swapTypeHidden.setAttribute('name', 'swap_type');
+ form.appendChild(swapTypeHidden);
+ }
+ }
+ if (swapTypeHidden) {
+ swapTypeHidden.setAttribute('value', swapTypeElement.value);
+ }
+ } else if (swapTypeHidden) {
+ swapTypeHidden.parentNode.removeChild(swapTypeHidden);
+ }
+ }
+};
+
+function set_swap_type_enabled(coinFrom, coinTo, swapTypeElement) {
+ SwapTypeManager.setSwapTypeEnabled(coinFrom, coinTo, swapTypeElement);
+}
+
+const UIEnhancer = {
+ handleErrorHighlighting: () => {
+ const errMsgs = document.querySelectorAll('p.error_msg');
+
+ const errorFieldMap = {
+ 'coin_to': ['coin_to', 'Coin To'],
+ 'coin_from': ['Coin From'],
+ 'amt_from': ['Amount From'],
+ 'amt_to': ['Amount To'],
+ 'amt_bid_min': ['Minimum Bid Amount'],
+ 'Select coin you send': ['coin_from', 'parentNode']
+ };
+
+ errMsgs.forEach(errMsg => {
+ const text = errMsg.innerText;
+
+ Object.entries(errorFieldMap).forEach(([field, keywords]) => {
+ if (keywords.some(keyword => text.includes(keyword))) {
+ let element = DOM.get(field);
+
+ if (field === 'Select coin you send' && element) {
+ element = element.parentNode;
+ }
+
+ if (element) {
+ element.classList.add('error');
+ }
+ }
+ });
+ });
+
+ document.querySelectorAll('input.error, select.error').forEach(element => {
+ element.addEventListener('focus', event => {
+ event.target.classList.remove('error');
+ });
+ });
+ },
+
+ updateDisabledStyles: () => {
+ document.querySelectorAll('select.disabled-select').forEach(select => {
+ if (select.disabled) {
+ select.classList.add('disabled-select-enabled');
+ } else {
+ select.classList.remove('disabled-select-enabled');
+ }
+ });
+
+ document.querySelectorAll('input.disabled-input, input[type="checkbox"].disabled-input').forEach(input => {
+ if (input.readOnly) {
+ input.classList.add('disabled-input-enabled');
+ } else {
+ input.classList.remove('disabled-input-enabled');
+ }
+ });
+ },
+
+ setupCustomSelects: () => {
+ const selectCache = {};
+
+ function updateSelectCache(select) {
+ if (!select || !select.options || select.selectedIndex === undefined) return;
+
+ const selectedOption = select.options[select.selectedIndex];
+ if (!selectedOption) return;
+
+ const image = selectedOption.getAttribute('data-image');
+ const name = selectedOption.textContent.trim();
+ selectCache[select.id] = { image, name };
+ }
+
+ function setSelectData(select) {
+ if (!select || !select.options || select.selectedIndex === undefined) return;
+
+ const selectedOption = select.options[select.selectedIndex];
+ if (!selectedOption) return;
+
+ const image = selectedOption.getAttribute('data-image') || '';
+ const name = selectedOption.textContent.trim();
+
+ select.style.backgroundImage = image ? `url(${image}?${new Date().getTime()})` : '';
+
+ const selectImage = select.nextElementSibling?.querySelector('.select-image');
+ if (selectImage) {
+ selectImage.src = image;
+ }
+
+ const selectNameElement = select.nextElementSibling?.querySelector('.select-name');
+ if (selectNameElement) {
+ selectNameElement.textContent = name;
+ }
+
+ updateSelectCache(select);
+ }
+
+ function setupCustomSelect(select) {
+ if (!select) return;
+
+ const options = select.querySelectorAll('option');
+ const selectIcon = select.parentElement?.querySelector('.select-icon');
+ const selectImage = select.parentElement?.querySelector('.select-image');
+
+ if (!options || !selectIcon || !selectImage) return;
+
+ options.forEach(option => {
+ const image = option.getAttribute('data-image');
+ if (image) {
+ option.style.backgroundImage = `url(${image})`;
+ }
+ });
+
+ const storedValue = Storage.getRaw(select.name);
+ if (storedValue && select.value == '-1') {
+ select.value = storedValue;
+ }
+
+ select.addEventListener('change', () => {
+ setSelectData(select);
+ Storage.setRaw(select.name, select.value);
+ });
+
+ setSelectData(select);
+ selectIcon.style.display = 'none';
+ selectImage.style.display = 'none';
+ }
+
+ const selectIcons = document.querySelectorAll('.custom-select .select-icon');
+ const selectImages = document.querySelectorAll('.custom-select .select-image');
+ const selectNames = document.querySelectorAll('.custom-select .select-name');
+
+ selectIcons.forEach(icon => icon.style.display = 'none');
+ selectImages.forEach(image => image.style.display = 'none');
+ selectNames.forEach(name => name.style.display = 'none');
+
+ const customSelects = document.querySelectorAll('.custom-select select');
+ customSelects.forEach(setupCustomSelect);
+ }
+};
+
+function initializeApp() {
+ handleNewOfferAddress();
+
+ DOM.addEvent('get_rate_inferred_button', 'click', RateManager.getRateInferred);
+
+ const coinFrom = DOM.get('coin_from');
+ const coinTo = DOM.get('coin_to');
+ const swapType = DOM.get('swap_type');
+
+ if (coinFrom && coinTo && swapType) {
+ SwapTypeManager.setSwapTypeEnabled(coinFrom.value, coinTo.value, swapType);
+
+ coinFrom.addEventListener('change', function() {
+ SwapTypeManager.setSwapTypeEnabled(this.value, coinTo.value, swapType);
+ RateManager.setRate('coin_from');
+ });
+
+ coinTo.addEventListener('change', function() {
+ SwapTypeManager.setSwapTypeEnabled(coinFrom.value, this.value, swapType);
+ RateManager.setRate('coin_to');
+ });
+ }
+
+ ['amt_from', 'amt_to', 'rate'].forEach(id => {
+ DOM.addEvent(id, 'change', function() {
+ RateManager.setRate(id);
+ });
+
+ DOM.addEvent(id, 'input', function() {
+ RateManager.setRate(id);
+ });
+ });
+
+ DOM.addEvent('rate_lock', 'change', function() {
+ if (DOM.getValue('rate')) {
+ RateManager.setRate('rate');
+ }
+ });
+
+ DOM.addEvent('lookup_rates_button', 'click', RateManager.lookupRates);
+
+ UIEnhancer.handleErrorHighlighting();
+ UIEnhancer.updateDisabledStyles();
+ UIEnhancer.setupCustomSelects();
+}
+
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', initializeApp);
+} else {
+ initializeApp();
}
diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js
index 83b5864..1d534cf 100644
--- a/basicswap/static/js/offers.js
+++ b/basicswap/static/js/offers.js
@@ -1,68 +1,3 @@
-// EVENT MANAGER
-const EventManager = {
- listeners: new Map(),
-
- add(element, type, handler, options = false) {
- if (!this.listeners.has(element)) {
- this.listeners.set(element, new Map());
- }
-
- const elementListeners = this.listeners.get(element);
- if (!elementListeners.has(type)) {
- elementListeners.set(type, new Set());
- }
-
- const handlerInfo = { handler, options };
- elementListeners.get(type).add(handlerInfo);
- element.addEventListener(type, handler, options);
-
- return handlerInfo;
- },
-
- remove(element, type, handler, options = false) {
- const elementListeners = this.listeners.get(element);
- if (!elementListeners) return;
-
- const typeListeners = elementListeners.get(type);
- if (!typeListeners) return;
-
- typeListeners.forEach(info => {
- if (info.handler === handler) {
- element.removeEventListener(type, handler, options);
- typeListeners.delete(info);
- }
- });
-
- if (typeListeners.size === 0) {
- elementListeners.delete(type);
- }
- if (elementListeners.size === 0) {
- this.listeners.delete(element);
- }
- },
-
- removeAll(element) {
- const elementListeners = this.listeners.get(element);
- if (!elementListeners) return;
-
- elementListeners.forEach((typeListeners, type) => {
- typeListeners.forEach(info => {
- element.removeEventListener(type, info.handler, info.options);
- });
- });
-
- this.listeners.delete(element);
- },
-
- clearAll() {
- this.listeners.forEach((elementListeners, element) => {
- this.removeAll(element);
- });
- this.listeners.clear();
- }
-};
-
-// GLOBAL STATE VARIABLES
let latestPrices = null;
let lastRefreshTime = null;
let currentPage = 1;
@@ -72,68 +7,11 @@ let currentSortColumn = 0;
let currentSortDirection = 'desc';
let filterTimeout = null;
-// CONFIGURATION CONSTANTS
-// TIME CONSTANTS
-const CACHE_DURATION = 10 * 60 * 1000;
-const wsPort = config.port || window.ws_port || '11700';
-
-// APP CONSTANTS
-const itemsPerPage = 50;
const isSentOffers = window.offersTableConfig.isSentOffers;
+const CACHE_DURATION = window.config.cacheConfig.defaultTTL;
+const wsPort = window.config.wsPort;
+const itemsPerPage = window.config.itemsPerPage;
-const offersConfig = {
- apiEndpoints: {
- coinGecko: 'https://api.coingecko.com/api/v3',
- cryptoCompare: 'https://min-api.cryptocompare.com/data'
- },
- apiKeys: getAPIKeys()
-};
-
-// MAPPING OBJECTS
-const coinNameToSymbol = {
- 'Bitcoin': 'bitcoin',
- 'Particl': 'particl',
- 'Particl Blind': 'particl',
- 'Particl Anon': 'particl',
- 'Monero': 'monero',
- 'Wownero': 'wownero',
- 'Litecoin': 'litecoin',
- 'Firo': 'firo',
- 'Zcoin': 'firo',
- 'Dash': 'dash',
- 'PIVX': 'pivx',
- 'Decred': 'decred',
- 'Zano': 'zano',
- 'Dogecoin': 'dogecoin',
- 'Bitcoin Cash': 'bitcoin-cash'
-};
-
-const coinNameToDisplayName = {
- 'Bitcoin': 'Bitcoin',
- 'Litecoin': 'Litecoin',
- 'Monero': 'Monero',
- 'Particl': 'Particl',
- 'Particl Blind': 'Particl Blind',
- 'Particl Anon': 'Particl Anon',
- 'PIVX': 'PIVX',
- 'Firo': 'Firo',
- 'Zcoin': 'Firo',
- 'Dash': 'Dash',
- 'Decred': 'Decred',
- 'Wownero': 'Wownero',
- 'Bitcoin Cash': 'Bitcoin Cash',
- 'Dogecoin': 'Dogecoin',
- 'Zano': 'Zano'
-};
-
-const coinIdToName = {
- 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
- 6: 'monero', 7: 'particl blind', 8: 'particl anon',
- 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash',
- 18: 'dogecoin'
-};
-
-// DOM ELEMENT REFERENCES
const offersBody = document.getElementById('offers-body');
const filterForm = document.getElementById('filterForm');
const prevPageButton = document.getElementById('prevPage');
@@ -143,582 +21,6 @@ const totalPagesSpan = document.getElementById('totalPages');
const lastRefreshTimeSpan = document.getElementById('lastRefreshTime');
const newEntriesCountSpan = document.getElementById('newEntriesCount');
-
-// MANAGER OBJECTS
-const WebSocketManager = {
- ws: null,
- messageQueue: [],
- processingQueue: false,
- debounceTimeout: null,
- reconnectTimeout: null,
- maxReconnectAttempts: 5,
- reconnectAttempts: 0,
- reconnectDelay: 5000,
- maxQueueSize: 1000,
- isIntentionallyClosed: false,
- handlers: {},
- isPageHidden: document.hidden,
- priceUpdatePaused: false,
- lastVisibilityChange: Date.now(),
-
- connectionState: {
- isConnecting: false,
- lastConnectAttempt: null,
- connectTimeout: null,
- lastHealthCheck: null,
- healthCheckInterval: null
- },
-
- initialize() {
- this.setupPageVisibilityHandler();
- this.connect();
- this.startHealthCheck();
- },
-
- setupPageVisibilityHandler() {
- this.handlers.visibilityChange = () => {
- if (document.hidden) {
- this.handlePageHidden();
- } else {
- this.handlePageVisible();
- }
- };
- document.addEventListener('visibilitychange', this.handlers.visibilityChange);
- },
-
- handlePageHidden() {
- this.isPageHidden = true;
- this.priceUpdatePaused = true;
- this.stopHealthCheck();
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
- this.isIntentionallyClosed = true;
- this.ws.close(1000, 'Page hidden');
- }
- },
-
- handlePageVisible() {
- this.isPageHidden = false;
- this.lastVisibilityChange = Date.now();
- this.isIntentionallyClosed = false;
-
- setTimeout(() => {
- this.priceUpdatePaused = false;
- if (!this.isConnected()) {
- this.connect();
- }
- this.startHealthCheck();
- }, 0);
- },
-
- startHealthCheck() {
- this.stopHealthCheck();
- this.connectionState.healthCheckInterval = setInterval(() => {
- this.performHealthCheck();
- }, 30000);
- },
-
- stopHealthCheck() {
- if (this.connectionState.healthCheckInterval) {
- clearInterval(this.connectionState.healthCheckInterval);
- this.connectionState.healthCheckInterval = null;
- }
- },
-
- performHealthCheck() {
- if (!this.isConnected()) {
- this.handleReconnect();
- return;
- }
-
- const now = Date.now();
- const lastCheck = this.connectionState.lastHealthCheck;
- if (lastCheck && (now - lastCheck) > 60000) {
- this.handleReconnect();
- return;
- }
-
- this.connectionState.lastHealthCheck = now;
- },
-
- connect() {
- if (this.connectionState.isConnecting || this.isIntentionallyClosed) {
- return false;
- }
-
- this.cleanup();
- this.connectionState.isConnecting = true;
- this.connectionState.lastConnectAttempt = Date.now();
-
- try {
- let wsPort;
-
- if (typeof getWebSocketConfig === 'function') {
- const wsConfig = getWebSocketConfig();
- wsPort = wsConfig.port || wsConfig.fallbackPort;
- console.log("Using WebSocket port:", wsPort);
- } else {
- wsPort = config?.port || window.ws_port || '11700';
- }
-
- if (!wsPort) {
- this.connectionState.isConnecting = false;
- return false;
- }
-
- this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
- this.setupEventHandlers();
-
- this.connectionState.connectTimeout = setTimeout(() => {
- if (this.connectionState.isConnecting) {
- this.cleanup();
- this.handleReconnect();
- }
- }, 5000);
-
- return true;
- } catch (error) {
- this.connectionState.isConnecting = false;
- this.handleReconnect();
- return false;
- }
-},
- setupEventHandlers() {
- if (!this.ws) return;
-
- this.handlers.open = () => {
- this.connectionState.isConnecting = false;
- this.reconnectAttempts = 0;
- clearTimeout(this.connectionState.connectTimeout);
- this.connectionState.lastHealthCheck = Date.now();
- window.ws = this.ws;
- console.log('🟢 WebSocket connection established for Offers');
- updateConnectionStatus('connected');
- };
-
- this.handlers.message = (event) => {
- try {
- const message = JSON.parse(event.data);
- this.handleMessage(message);
- } catch (error) {
- updateConnectionStatus('error');
- }
- };
-
- this.handlers.error = (error) => {
- updateConnectionStatus('error');
- };
-
- this.handlers.close = (event) => {
- this.connectionState.isConnecting = false;
- window.ws = null;
- updateConnectionStatus('disconnected');
-
- if (!this.isIntentionallyClosed) {
- this.handleReconnect();
- }
- };
-
- this.ws.onopen = this.handlers.open;
- this.ws.onmessage = this.handlers.message;
- this.ws.onerror = this.handlers.error;
- this.ws.onclose = this.handlers.close;
- },
-
- handleMessage(message) {
- if (this.messageQueue.length >= this.maxQueueSize) {
- this.messageQueue.shift();
- }
-
- if (this.debounceTimeout) {
- clearTimeout(this.debounceTimeout);
- }
-
- this.messageQueue.push(message);
-
- this.debounceTimeout = setTimeout(() => {
- this.processMessageQueue();
- }, 200);
- },
-
- async processMessageQueue() {
- if (this.processingQueue || this.messageQueue.length === 0) return;
-
- this.processingQueue = true;
- const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
-
- try {
- const response = await fetch(endpoint);
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
-
- const newData = await response.json();
- const fetchedOffers = Array.isArray(newData) ? newData : Object.values(newData);
-
- jsonData = formatInitialData(fetchedOffers);
- originalJsonData = [...jsonData];
-
- requestAnimationFrame(() => {
- updateOffersTable();
- updatePaginationInfo();
- });
-
- this.messageQueue = [];
- } catch (error) {
- } finally {
- this.processingQueue = false;
- }
- },
-
- handleReconnect() {
- if (this.reconnectTimeout) {
- clearTimeout(this.reconnectTimeout);
- }
-
- this.reconnectAttempts++;
- if (this.reconnectAttempts <= this.maxReconnectAttempts) {
- const delay = Math.min(
- this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
- 30000
- );
-
- this.reconnectTimeout = setTimeout(() => {
- if (!this.isIntentionallyClosed) {
- this.connect();
- }
- }, delay);
- } else {
- updateConnectionStatus('error');
- setTimeout(() => {
- this.reconnectAttempts = 0;
- this.connect();
- }, 60000);
- }
- },
-
- cleanup() {
- clearTimeout(this.debounceTimeout);
- clearTimeout(this.reconnectTimeout);
- clearTimeout(this.connectionState.connectTimeout);
- this.stopHealthCheck();
-
- this.messageQueue = [];
- this.processingQueue = false;
- this.connectionState.isConnecting = false;
-
- if (this.ws) {
- this.ws.onopen = null;
- this.ws.onmessage = null;
- this.ws.onerror = null;
- this.ws.onclose = null;
-
- if (this.ws.readyState === WebSocket.OPEN) {
- this.ws.close(1000, 'Cleanup');
- }
-
- this.ws = null;
- window.ws = null;
- }
-
- if (this.handlers.visibilityChange) {
- document.removeEventListener('visibilitychange', this.handlers.visibilityChange);
- }
-
- this.handlers = {};
- },
-
- disconnect() {
- this.isIntentionallyClosed = true;
- this.cleanup();
- this.stopHealthCheck();
- },
-
- isConnected() {
- return this.ws && this.ws.readyState === WebSocket.OPEN;
- }
-};
-
-window.WebSocketManager = WebSocketManager;
-
-const CacheManager = {
- maxItems: 100,
- maxSize: 5 * 1024 * 1024, // 5MB
-
- set: function(key, value, customTtl = null) {
- try {
- this.cleanup();
-
- if (!value) {
- console.warn('Attempted to cache null/undefined value for key:', key);
- return false;
- }
-
- const item = {
- value: value,
- timestamp: Date.now(),
- expiresAt: Date.now() + (customTtl || CACHE_DURATION)
- };
-
- try {
- JSON.stringify(item);
- } catch (e) {
- console.error('Failed to serialize cache item:', e);
- return false;
- }
-
- const itemSize = new Blob([JSON.stringify(item)]).size;
- if (itemSize > this.maxSize) {
- console.warn(`Cache item exceeds maximum size (${(itemSize/1024/1024).toFixed(2)}MB)`);
- return false;
- }
-
- try {
- localStorage.setItem(key, JSON.stringify(item));
- return true;
- } catch (storageError) {
- if (storageError.name === 'QuotaExceededError') {
- this.cleanup(true);
- try {
- localStorage.setItem(key, JSON.stringify(item));
- return true;
- } catch (retryError) {
- console.error('Storage quota exceeded even after cleanup:', retryError);
- return false;
- }
- }
- throw storageError;
- }
-
- } catch (error) {
- console.error('Cache set error:', error);
- return false;
- }
- },
-
- get: function(key) {
- try {
- const itemStr = localStorage.getItem(key);
- if (!itemStr) {
- return null;
- }
-
- let item;
- try {
- item = JSON.parse(itemStr);
- } catch (parseError) {
- console.error('Failed to parse cached item:', parseError);
- localStorage.removeItem(key);
- return null;
- }
-
- if (!item || typeof item.expiresAt !== 'number' || !Object.prototype.hasOwnProperty.call(item, 'value')) {
- console.warn('Invalid cache item structure for key:', key);
- localStorage.removeItem(key);
- return null;
- }
-
- const now = Date.now();
- if (now < item.expiresAt) {
- return {
- value: item.value,
- remainingTime: item.expiresAt - now
- };
- }
-
- localStorage.removeItem(key);
- return null;
-
- } catch (error) {
- console.error("Cache retrieval error:", error);
- try {
- localStorage.removeItem(key);
- } catch (removeError) {
- console.error("Failed to remove invalid cache entry:", removeError);
- }
- return null;
- }
-},
-
- cleanup: function(aggressive = false) {
- const now = Date.now();
- let totalSize = 0;
- let itemCount = 0;
- const items = [];
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (!key.startsWith('offers_') && !key.startsWith('prices_')) continue;
-
- try {
- const itemStr = localStorage.getItem(key);
- const size = new Blob([itemStr]).size;
- const item = JSON.parse(itemStr);
-
- if (now >= item.expiresAt) {
- localStorage.removeItem(key);
- continue;
- }
-
- items.push({
- key,
- size,
- expiresAt: item.expiresAt,
- timestamp: item.timestamp
- });
-
- totalSize += size;
- itemCount++;
- } catch (error) {
- console.error("Error processing cache item:", error);
- localStorage.removeItem(key);
- }
- }
-
- if (aggressive || totalSize > this.maxSize || itemCount > this.maxItems) {
- items.sort((a, b) => b.timestamp - a.timestamp);
-
- while ((totalSize > this.maxSize || itemCount > this.maxItems) && items.length > 0) {
- const item = items.pop();
- try {
- localStorage.removeItem(item.key);
- totalSize -= item.size;
- itemCount--;
- } catch (error) {
- console.error("Error removing cache item:", error);
- }
- }
- }
-
- return {
- totalSize,
- itemCount,
- cleaned: items.length
- };
- },
-
- clear: function() {
- const keys = [];
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key.startsWith('offers_') || key.startsWith('prices_')) {
- keys.push(key);
- }
- }
-
- keys.forEach(key => {
- try {
- localStorage.removeItem(key);
- } catch (error) {
- console.error("Error clearing cache item:", error);
- }
- });
- },
-
- getStats: function() {
- let totalSize = 0;
- let itemCount = 0;
- let expiredCount = 0;
- const now = Date.now();
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (!key.startsWith('offers_') && !key.startsWith('prices_')) continue;
-
- try {
- const itemStr = localStorage.getItem(key);
- const size = new Blob([itemStr]).size;
- const item = JSON.parse(itemStr);
-
- totalSize += size;
- itemCount++;
-
- if (now >= item.expiresAt) {
- expiredCount++;
- }
- } catch (error) {
- console.error("Error getting cache stats:", error);
- }
- }
-
- return {
- totalSizeMB: (totalSize / 1024 / 1024).toFixed(2),
- itemCount,
- expiredCount,
- utilization: ((totalSize / this.maxSize) * 100).toFixed(1) + '%'
- };
- }
-};
-
-window.CacheManager = CacheManager;
-
-// IDENTITY CACHE MANAGEMENT
-const IdentityManager = {
- cache: new Map(),
- pendingRequests: new Map(),
- retryDelay: 2000,
- maxRetries: 3,
- cacheTimeout: 5 * 60 * 1000,
- async getIdentityData(address) {
-
- const cachedData = this.getCachedIdentity(address);
- if (cachedData) {
- return cachedData;
- }
-
- if (this.pendingRequests.has(address)) {
- return this.pendingRequests.get(address);
- }
-
- const request = this.fetchWithRetry(address);
- this.pendingRequests.set(address, request);
-
- try {
- const data = await request;
- this.cache.set(address, {
- data,
- timestamp: Date.now()
- });
- return data;
- } finally {
- this.pendingRequests.delete(address);
- }
- },
-
- getCachedIdentity(address) {
- const cached = this.cache.get(address);
- if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
- return cached.data;
- }
- return null;
- },
-
- async fetchWithRetry(address, attempt = 1) {
- try {
- const response = await fetch(`/json/identities/${address}`, {
- signal: AbortSignal.timeout(5000)
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- return await response.json();
- } catch (error) {
- if (attempt >= this.maxRetries) {
- console.error("An error occured:", error.message);
- console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
- return null;
- }
-
- await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
- return this.fetchWithRetry(address, attempt + 1);
- }
- },
-
- clearCache() {
- this.cache.clear();
- this.pendingRequests.clear();
- }
-};
-
window.tableRateModule = {
coinNameToSymbol: {
'Bitcoin': 'BTC',
@@ -753,7 +55,12 @@ window.tableRateModule = {
return null;
},
- setCachedValue(key, value, ttl = 900000) {
+ setCachedValue(key, value, resourceType = null) {
+ const ttl = resourceType ?
+ window.config.cacheConfig.ttlSettings[resourceType] ||
+ window.config.cacheConfig.defaultTTL :
+ 900000;
+
const item = {
value: value,
expiry: Date.now() + ttl,
@@ -762,7 +69,7 @@ window.tableRateModule = {
},
setFallbackValue(coinSymbol, value) {
- this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 24 * 60 * 60 * 1000);
+ this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 'fallback');
},
isNewOffer(offerId) {
@@ -802,7 +109,6 @@ window.tableRateModule = {
document.querySelectorAll('.coinname-value').forEach(coinNameValue => {
const coinFullNameOrSymbol = coinNameValue.getAttribute('data-coinname');
if (!coinFullNameOrSymbol || coinFullNameOrSymbol === 'Unknown') {
- //console.warn('Missing or unknown coin name/symbol in data-coinname attribute');
return;
}
coinNameValue.classList.remove('hidden');
@@ -825,19 +131,15 @@ window.tableRateModule = {
},
init() {
- //console.log('Initializing TableRateModule');
this.initializeTable();
}
};
-// CORE SYSTEM FUNCTIONS
function initializeTableRateModule() {
if (typeof window.tableRateModule !== 'undefined') {
tableRateModule = window.tableRateModule;
- //console.log('tableRateModule loaded successfully');
return true;
} else {
- //console.warn('tableRateModule not found. Waiting for it to load...');
return false;
}
}
@@ -855,8 +157,6 @@ function continueInitialization() {
if (listingLabel) {
listingLabel.textContent = isSentOffers ? 'Total Listings: ' : 'Network Listings: ';
}
- //console.log('Initialization completed');
-
}
function initializeTooltips() {
@@ -865,36 +165,42 @@ function initializeTooltips() {
}
}
-// DATA PROCESSING FUNCTIONS
function getValidOffers() {
if (!jsonData) {
- //console.warn('jsonData is undefined or null');
return [];
}
const filteredData = filterAndSortData();
- //console.log(`getValidOffers: Found ${filteredData.length} valid offers`);
return filteredData;
}
+function saveFilterSettings() {
+ const formData = new FormData(filterForm);
+ const filters = Object.fromEntries(formData);
+
+ const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings';
+
+ localStorage.setItem(storageKey, JSON.stringify({
+ coin_to: filters.coin_to,
+ coin_from: filters.coin_from,
+ status: filters.status,
+ sent_from: filters.sent_from,
+ sortColumn: currentSortColumn,
+ sortDirection: currentSortDirection
+ }));
+}
+
function filterAndSortData() {
const formData = new FormData(filterForm);
const filters = Object.fromEntries(formData);
- localStorage.setItem('offersTableSettings', JSON.stringify({
- coin_to: filters.coin_to,
- coin_from: filters.coin_from,
- status: filters.status,
- sent_from: filters.sent_from,
- sortColumn: currentSortColumn,
- sortDirection: currentSortDirection
- }));
+ saveFilterSettings();
if (filters.coin_to !== 'any') {
- filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to;
+ filters.coin_to = window.config.coinMappings.idToName[filters.coin_to] || filters.coin_to;
}
if (filters.coin_from !== 'any') {
- filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from;
+ filters.coin_from = window.config.coinMappings.idToName[filters.coin_from] || filters.coin_from;
}
let filteredData = [...originalJsonData];
@@ -959,14 +265,14 @@ function filterAndSortData() {
filteredData.sort((a, b) => {
let comparison;
switch(currentSortColumn) {
- case 0: // Time
+ case 0:
comparison = a.created_at - b.created_at;
break;
- case 5: // Rate
- case 6: // Market +/-
+ case 5:
+ case 6:
comparison = sortValues.get(a.offer_id) - sortValues.get(b.offer_id);
break;
- case 7: // Trade
+ case 7:
comparison = a.offer_id.localeCompare(b.offer_id);
break;
default:
@@ -989,69 +295,77 @@ async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwn
const getPriceKey = (coin) => {
const lowerCoin = coin.toLowerCase();
- if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
- return 'zcoin';
- }
- if (lowerCoin === 'bitcoin cash') {
- return 'bitcoin-cash';
- }
- if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') {
- return 'particl';
- }
- return coinNameToSymbol[coin] || lowerCoin;
+ const symbolToName = {
+ 'btc': 'bitcoin',
+ 'xmr': 'monero',
+ 'part': 'particl',
+ 'bch': 'bitcoin-cash',
+ 'pivx': 'pivx',
+ 'firo': 'firo',
+ 'dash': 'dash',
+ 'ltc': 'litecoin',
+ 'doge': 'dogecoin',
+ 'dcr': 'decred',
+ 'wow': 'wownero'
+ };
+
+ if (lowerCoin === 'zcoin') return 'firo';
+ if (lowerCoin === 'bitcoin cash') return 'bitcoin-cash';
+ if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') return 'particl';
+
+ return symbolToName[lowerCoin] || lowerCoin;
};
const fromSymbol = getPriceKey(fromCoin);
const toSymbol = getPriceKey(toCoin);
- let fromPriceUSD = latestPrices[fromSymbol]?.usd;
- let toPriceUSD = latestPrices[toSymbol]?.usd;
+
+ let fromPriceUSD = latestPrices && latestPrices[fromSymbol] ? latestPrices[fromSymbol].usd : null;
+ let toPriceUSD = latestPrices && latestPrices[toSymbol] ? latestPrices[toSymbol].usd : null;
if (!fromPriceUSD || !toPriceUSD) {
fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol);
toPriceUSD = tableRateModule.getFallbackValue(toSymbol);
}
+
if (!fromPriceUSD || !toPriceUSD || isNaN(fromPriceUSD) || isNaN(toPriceUSD)) {
resolve(null);
return;
}
+
const fromValueUSD = fromAmount * fromPriceUSD;
const toValueUSD = toAmount * toPriceUSD;
+
if (isNaN(fromValueUSD) || isNaN(toValueUSD) || fromValueUSD === 0 || toValueUSD === 0) {
resolve(null);
return;
}
+
let percentDiff;
if (isOwnOffer) {
percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
} else {
percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
}
+
if (isNaN(percentDiff)) {
resolve(null);
return;
}
+
resolve(percentDiff);
});
}
function getEmptyPriceData() {
- return {
- 'bitcoin': { usd: null, btc: null },
- 'bitcoin-cash': { usd: null, btc: null },
- 'dash': { usd: null, btc: null },
- 'dogecoin': { usd: null, btc: null },
- 'decred': { usd: null, btc: null },
- 'litecoin': { usd: null, btc: null },
- 'particl': { usd: null, btc: null },
- 'pivx': { usd: null, btc: null },
- 'monero': { usd: null, btc: null },
- 'zano': { usd: null, btc: null },
- 'wownero': { usd: null, btc: null },
- 'zcoin': { usd: null, btc: null }
- };
+ return window.config.utils.getEmptyPriceData();
}
async function fetchLatestPrices() {
+ if (!NetworkManager.isOnline()) {
+ const cachedData = CacheManager.get('prices_coingecko');
+ return cachedData?.value || getEmptyPriceData();
+ }
+
if (WebSocketManager.isPageHidden || WebSocketManager.priceUpdatePaused) {
const cachedData = CacheManager.get('prices_coingecko');
return cachedData?.value || getEmptyPriceData();
@@ -1086,48 +400,65 @@ async function fetchLatestPrices() {
}
try {
- const existingCache = CacheManager.get(PRICES_CACHE_KEY, true);
+ const existingCache = CacheManager.get(PRICES_CACHE_KEY);
const fallbackData = existingCache ? existingCache.value : null;
- const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
- const response = await fetch('/json/readurl', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- url: url,
- headers: {
- 'User-Agent': 'Mozilla/5.0',
- 'Accept': 'application/json',
- 'Accept-Language': 'en-US,en;q=0.5'
+ const coinIds = [
+ 'bitcoin', 'particl', 'monero', 'litecoin',
+ 'dogecoin', 'firo', 'dash', 'pivx',
+ 'decred', 'bitcoincash'
+ ];
+
+ let processedData = {};
+ const MAX_RETRIES = 3;
+
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
+ try {
+ const mainResponse = await Api.fetchCoinPrices(coinIds);
+
+ if (mainResponse && mainResponse.rates) {
+ Object.entries(mainResponse.rates).forEach(([coinId, price]) => {
+ const normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
+
+ processedData[normalizedCoinId] = {
+ usd: price,
+ btc: normalizedCoinId === 'bitcoin' ? 1 : price / (mainResponse.rates.bitcoin || 1)
+ };
+ });
}
- })
- });
- if (!response.ok) {
- throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
- }
+ try {
+ const wowResponse = await Api.fetchCoinPrices("wownero");
- const data = await response.json();
+ if (wowResponse && wowResponse.rates && wowResponse.rates.wownero) {
+ processedData['wownero'] = {
+ usd: wowResponse.rates.wownero,
+ btc: processedData.bitcoin ? wowResponse.rates.wownero / processedData.bitcoin.usd : 0
+ };
+ }
+ } catch (wowError) {
+ console.error('Error fetching WOW price:', wowError);
+ }
- if (data.Error) {
- if (fallbackData) {
- return fallbackData;
+ latestPrices = processedData;
+ CacheManager.set(PRICES_CACHE_KEY, processedData, 'prices');
+
+ Object.entries(processedData).forEach(([coin, prices]) => {
+ if (prices.usd) {
+ tableRateModule.setFallbackValue(coin, prices.usd);
+ }
+ });
+
+ return processedData;
+ } catch (error) {
+ console.error(`Price fetch attempt ${attempt + 1} failed:`, error);
+ NetworkManager.handleNetworkError(error);
+
+ if (attempt < MAX_RETRIES - 1) {
+ const delay = Math.min(500 * Math.pow(2, attempt), 5000);
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
}
- throw new Error(data.Error);
- }
-
- if (data && Object.keys(data).length > 0) {
- latestPrices = data;
- CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
-
- Object.entries(data).forEach(([coin, prices]) => {
- if (prices.usd) {
- tableRateModule.setFallbackValue(coin, prices.usd);
- }
- });
- return data;
}
if (fallbackData) {
@@ -1146,16 +477,11 @@ async function fetchLatestPrices() {
return fallbackPrices;
}
- return null;
+ return getEmptyPriceData();
} catch (error) {
- const fallbackPrices = {};
- Object.keys(getEmptyPriceData()).forEach(coin => {
- const fallbackValue = tableRateModule.getFallbackValue(coin);
- if (fallbackValue !== null) {
- fallbackPrices[coin] = { usd: fallbackValue, btc: null };
- }
- });
- return Object.keys(fallbackPrices).length > 0 ? fallbackPrices : null;
+ console.error('Unexpected error in fetchLatestPrices:', error);
+ NetworkManager.handleNetworkError(error);
+ return getEmptyPriceData();
} finally {
window.isManualRefresh = false;
}
@@ -1167,6 +493,10 @@ async function fetchOffers() {
const refreshText = document.getElementById('refreshText');
try {
+ if (!NetworkManager.isOnline()) {
+ throw new Error('Network is offline');
+ }
+
if (refreshButton) {
refreshButton.disabled = true;
refreshIcon.classList.add('animate-spin');
@@ -1190,12 +520,15 @@ async function fetchOffers() {
originalJsonData = [...jsonData];
latestPrices = pricesData || getEmptyPriceData();
+
+ CacheManager.set('offers_cached', jsonData, 'offers');
await updateOffersTable();
updatePaginationInfo();
} catch (error) {
console.error('[Debug] Error fetching offers:', error);
+ NetworkManager.handleNetworkError(error);
const cachedOffers = CacheManager.get('offers_cached');
if (cachedOffers?.value) {
@@ -1235,13 +568,11 @@ function formatInitialData(data) {
}));
}
-// UI COMPONENT FUNCTIONS
function updateConnectionStatus(status) {
const dot = document.getElementById('status-dot');
const text = document.getElementById('status-text');
if (!dot || !text) {
- //console.warn('Status indicators not found in DOM');
return;
}
@@ -1375,7 +706,180 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe
profitLossElement.textContent = `${percentDiffDisplay}%`;
profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
- // Update tooltip if it exists
+ const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
+ const tooltipElement = document.getElementById(tooltipId);
+ if (tooltipElement) {
+ const tooltipContent = createTooltipContent(isSentOffers || isOwnOffer, fromCoin, toCoin, fromAmount, toAmount);
+ tooltipElement.innerHTML = `
+
+ ${tooltipContent}
+
+
+ `;
+ }
+ })
+ .catch(error => {
+ console.error('Error in updateProfitLoss:', error);
+ profitLossElement.textContent = 'N/A';
+ profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
+ });
+}
+
+function updateClearFiltersButton() {
+ const clearButton = document.getElementById('clearFilters');
+ if (clearButton) {
+ const hasFilters = hasActiveFilters();
+ clearButton.classList.toggle('opacity-50', !hasFilters);
+ clearButton.disabled = !hasFilters;
+
+ if (hasFilters) {
+ clearButton.classList.add('hover:bg-green-600', 'hover:text-white');
+ clearButton.classList.remove('cursor-not-allowed');
+ } else {
+ clearButton.classList.remove('hover:bg-green-600', 'hover:text-white');
+ clearButton.classList.add('cursor-not-allowed');
+ }
+ }
+}
+
+function updateConnectionStatus(status) {
+ const dot = document.getElementById('status-dot');
+ const text = document.getElementById('status-text');
+
+ if (!dot || !text) {
+ return;
+ }
+
+ switch(status) {
+ case 'connected':
+ dot.className = 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2';
+ text.textContent = 'Connected';
+ text.className = 'text-sm text-green-500';
+ break;
+ case 'disconnected':
+ dot.className = 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2';
+ text.textContent = 'Disconnected - Reconnecting...';
+ text.className = 'text-sm text-red-500';
+ break;
+ case 'error':
+ dot.className = 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2';
+ text.textContent = 'Connection Error';
+ text.className = 'text-sm text-yellow-500';
+ break;
+ default:
+ dot.className = 'w-2.5 h-2.5 rounded-full bg-gray-500 mr-2';
+ text.textContent = 'Connecting...';
+ text.className = 'text-sm text-gray-500';
+ }
+}
+
+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 newPostedTime = formatTime(offer.created_at, true);
+ const newExpiresIn = formatTimeLeft(offer.expire_at);
+
+ const postedElement = row.querySelector('.text-xs:first-child');
+ const expiresElement = row.querySelector('.text-xs:last-child');
+
+ if (postedElement && postedElement.textContent !== `Posted: ${newPostedTime}`) {
+ postedElement.textContent = `Posted: ${newPostedTime}`;
+ }
+ if (expiresElement && expiresElement.textContent !== `Expires in: ${newExpiresIn}`) {
+ expiresElement.textContent = `Expires in: ${newExpiresIn}`;
+ }
+ });
+ });
+}
+
+function updateLastRefreshTime() {
+ if (lastRefreshTimeSpan) {
+ lastRefreshTimeSpan.textContent = lastRefreshTime ? new Date(lastRefreshTime).toLocaleTimeString() : 'Never';
+ }
+}
+
+function stopRefreshAnimation() {
+ const refreshButton = document.getElementById('refreshOffers');
+ const refreshIcon = document.getElementById('refreshIcon');
+ const refreshText = document.getElementById('refreshText');
+
+ if (refreshButton) {
+ refreshButton.disabled = false;
+ refreshButton.classList.remove('opacity-75', 'cursor-wait');
+ }
+ if (refreshIcon) {
+ refreshIcon.classList.remove('animate-spin');
+ }
+ if (refreshText) {
+ refreshText.textContent = 'Refresh';
+ }
+}
+
+function updatePaginationInfo() {
+ const validOffers = getValidOffers();
+ const totalItems = validOffers.length;
+ const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
+
+ currentPage = Math.max(1, Math.min(currentPage, totalPages));
+
+ currentPageSpan.textContent = currentPage;
+ totalPagesSpan.textContent = totalPages;
+
+ const showPrev = currentPage > 1;
+ const showNext = currentPage < totalPages && totalItems > 0;
+
+ prevPageButton.style.display = showPrev ? 'inline-flex' : 'none';
+ nextPageButton.style.display = showNext ? 'inline-flex' : 'none';
+
+ if (lastRefreshTime) {
+ lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString();
+ }
+
+ if (newEntriesCountSpan) {
+ newEntriesCountSpan.textContent = totalItems;
+ }
+}
+
+function updatePaginationControls(totalPages) {
+ prevPageButton.style.display = currentPage > 1 ? 'inline-flex' : 'none';
+ nextPageButton.style.display = currentPage < totalPages ? 'inline-flex' : 'none';
+ currentPageSpan.textContent = currentPage;
+ totalPagesSpan.textContent = totalPages;
+}
+
+function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
+ const profitLossElement = row.querySelector('.profit-loss');
+ if (!profitLossElement) {
+ return;
+ }
+
+ if (!fromCoin || !toCoin) {
+ profitLossElement.textContent = 'N/A';
+ profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
+ return;
+ }
+
+ calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
+ .then(percentDiff => {
+ if (percentDiff === null || isNaN(percentDiff)) {
+ profitLossElement.textContent = 'N/A';
+ profitLossElement.className = 'profit-loss text-lg font-bold text-gray-300';
+ return;
+ }
+
+ const formattedPercentDiff = percentDiff.toFixed(2);
+ const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
+ (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
+
+ const colorClass = getProfitColorClass(percentDiff);
+ profitLossElement.textContent = `${percentDiffDisplay}%`;
+ profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
+
const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
const tooltipElement = document.getElementById(tooltipId);
if (tooltipElement) {
@@ -1406,12 +910,14 @@ function updateCoinFilterImages() {
const imagePath = selectedOption.getAttribute('data-image');
if (imagePath && select.value !== 'any') {
button.style.backgroundImage = `url(${imagePath})`;
- button.style.backgroundSize = 'contain';
- button.style.backgroundRepeat = 'no-repeat';
+ button.style.backgroundSize = '25px 25px';
button.style.backgroundPosition = 'center';
+ button.style.backgroundRepeat = 'no-repeat';
} else {
button.style.backgroundImage = 'none';
}
+ button.style.minWidth = '25px';
+ button.style.minHeight = '25px';
}
updateButtonImage(coinToSelect, coinToButton);
@@ -1436,23 +942,33 @@ function updateClearFiltersButton() {
}
function cleanupRow(row) {
+ if (!row) return;
+
const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]');
tooltipTriggers.forEach(trigger => {
if (window.TooltipManager) {
window.TooltipManager.destroy(trigger);
}
});
- EventManager.removeAll(row);
+
+ CleanupManager.removeListenersByElement(row);
+
+ row.removeAttribute('data-offer-id');
+
+ while (row.firstChild) {
+ const child = row.firstChild;
+ row.removeChild(child);
+ }
}
-
function cleanupTable() {
- EventManager.clearAll();
- if (offersBody) {
- const existingRows = offersBody.querySelectorAll('tr');
- existingRows.forEach(row => cleanupRow(row));
- offersBody.innerHTML = '';
- }
+ if (!offersBody) return;
+
+ const existingRows = offersBody.querySelectorAll('tr');
+ existingRows.forEach(row => cleanupRow(row));
+
+ offersBody.innerHTML = '';
+
if (window.TooltipManager) {
window.TooltipManager.cleanup();
}
@@ -1645,8 +1161,8 @@ function createTableRow(offer, identity = null) {
is_public: isPublic
} = offer;
- const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase();
- const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase();
+ const coinFromSymbol = window.config.coinMappings.nameToSymbol[coinFrom] || coinFrom.toLowerCase();
+ const coinToSymbol = window.config.coinMappings.nameToSymbol[coinTo] || coinTo.toLowerCase();
const coinFromDisplay = getDisplayName(coinFrom);
const coinToDisplay = getDisplayName(coinTo);
const postedTime = formatTime(createdAt, true);
@@ -1663,7 +1179,7 @@ function createTableRow(offer, identity = null) {
${createDetailsColumn(offer, identity)}
${createTakerAmountColumn(offer, coinTo, coinFrom)}
${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)}
- ${createOrderbookColumn(offer, coinFrom, coinTo)}
+ ${createOrderbookColumn(offer, coinFrom)}
${createRateColumn(offer, coinFrom, coinTo)}
${createPercentageColumn(offer)}
${createActionColumn(offer, isActuallyExpired)}
@@ -1698,11 +1214,11 @@ function createTimeColumn(offer, postedTime, expiresIn) {
const now = Math.floor(Date.now() / 1000);
const timeLeft = offer.expire_at - now;
- let strokeColor = '#10B981'; // Default green for > 30 min
+ let strokeColor = '#10B981';
if (timeLeft <= 300) {
- strokeColor = '#9CA3AF'; // Grey for 5 min or less
+ strokeColor = '#9CA3AF';
} else if (timeLeft <= 1800) {
- strokeColor = '#3B82F6'; // Blue for 5-30 min
+ strokeColor = '#3B82F6';
}
return `
@@ -1836,22 +1352,31 @@ function createRateColumn(offer, coinFrom, coinTo) {
const inverseRate = rate ? (1 / rate) : 0;
const getPriceKey = (coin) => {
- if (!coin) return null;
const lowerCoin = coin.toLowerCase();
- if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
- return 'zcoin';
- }
- if (lowerCoin === 'bitcoin cash') {
- return 'bitcoin-cash';
- }
- if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') {
- return 'particl';
- }
- return coinNameToSymbol[coin] || lowerCoin;
+
+ const symbolToName = {
+ 'btc': 'bitcoin',
+ 'xmr': 'monero',
+ 'part': 'particl',
+ 'bch': 'bitcoin-cash',
+ 'pivx': 'pivx',
+ 'firo': 'firo',
+ 'dash': 'dash',
+ 'ltc': 'litecoin',
+ 'doge': 'dogecoin',
+ 'dcr': 'decred',
+ 'wow': 'wownero'
+ };
+
+ if (lowerCoin === 'zcoin') return 'firo';
+ if (lowerCoin === 'bitcoin cash') return 'bitcoin-cash';
+ if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') return 'particl';
+
+ return symbolToName[lowerCoin] || lowerCoin;
};
const toSymbolKey = getPriceKey(coinTo);
- let toPriceUSD = latestPrices && toSymbolKey ? latestPrices[toSymbolKey]?.usd : null;
+ let toPriceUSD = latestPrices && latestPrices[toSymbolKey] ? latestPrices[toSymbolKey].usd : null;
if (!toPriceUSD || isNaN(toPriceUSD)) {
toPriceUSD = tableRateModule.getFallbackValue(toSymbolKey);
@@ -1927,7 +1452,6 @@ function createActionColumn(offer, isActuallyExpired = false) {
`;
}
-// TOOLTIP FUNCTIONS
function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked, identity = null) {
const uniqueId = `${offer.offer_id}_${offer.created_at}`;
@@ -2119,19 +1643,46 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
const getPriceKey = (coin) => {
const lowerCoin = coin.toLowerCase();
- return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' :
- lowerCoin === 'bitcoin cash' ? 'bitcoin-cash' :
- lowerCoin === 'particl anon' || lowerCoin === 'particl blind' ? 'particl' :
- coinNameToSymbol[coin] || lowerCoin;
+
+ const symbolToName = {
+ 'btc': 'bitcoin',
+ 'xmr': 'monero',
+ 'part': 'particl',
+ 'bch': 'bitcoin-cash',
+ 'pivx': 'pivx',
+ 'firo': 'firo',
+ 'dash': 'dash',
+ 'ltc': 'litecoin',
+ 'doge': 'dogecoin',
+ 'dcr': 'decred',
+ 'wow': 'wownero'
+ };
+
+ if (lowerCoin === 'zcoin') return 'firo';
+ if (lowerCoin === 'bitcoin cash') return 'bitcoin-cash';
+ if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') return 'particl';
+
+ return symbolToName[lowerCoin] || lowerCoin;
};
+
+ if (latestPrices && latestPrices['firo'] && !latestPrices['zcoin']) {
+ latestPrices['zcoin'] = JSON.parse(JSON.stringify(latestPrices['firo']));
+ }
const fromSymbol = getPriceKey(coinFrom);
const toSymbol = getPriceKey(coinTo);
- const fromPriceUSD = latestPrices[fromSymbol]?.usd;
- const toPriceUSD = latestPrices[toSymbol]?.usd;
+
+ let fromPriceUSD = latestPrices && latestPrices[fromSymbol] ? latestPrices[fromSymbol].usd : null;
+ let toPriceUSD = latestPrices && latestPrices[toSymbol] ? latestPrices[toSymbol].usd : null;
+
+ if (!fromPriceUSD || !toPriceUSD) {
+ fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol);
+ toPriceUSD = tableRateModule.getFallbackValue(toSymbol);
+ }
if (fromPriceUSD === null || toPriceUSD === null ||
- fromPriceUSD === undefined || toPriceUSD === undefined) {
+ fromPriceUSD === undefined || toPriceUSD === undefined ||
+ isNaN(fromPriceUSD) || isNaN(toPriceUSD)) {
return ` Price Information Unavailable
Current market prices are temporarily unavailable.
You are ${isSentOffers ? 'selling' : 'buying'} ${fromAmount.toFixed(8)} ${coinFrom}
@@ -2194,25 +1745,46 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
const getPriceKey = (coin) => {
const lowerCoin = coin.toLowerCase();
- if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
- return 'zcoin';
- }
- if (lowerCoin === 'bitcoin cash') {
- return 'bitcoin-cash';
- }
- if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') {
- return 'particl';
- }
- return coinNameToSymbol[coin] || lowerCoin;
+
+ const symbolToName = {
+ 'btc': 'bitcoin',
+ 'xmr': 'monero',
+ 'part': 'particl',
+ 'bch': 'bitcoin-cash',
+ 'pivx': 'pivx',
+ 'firo': 'firo',
+ 'dash': 'dash',
+ 'ltc': 'litecoin',
+ 'doge': 'dogecoin',
+ 'dcr': 'decred',
+ 'wow': 'wownero'
+ };
+
+ if (lowerCoin === 'zcoin') return 'firo';
+ if (lowerCoin === 'bitcoin cash') return 'bitcoin-cash';
+ if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') return 'particl';
+
+ return symbolToName[lowerCoin] || lowerCoin;
};
+ if (latestPrices && latestPrices['firo'] && !latestPrices['zcoin']) {
+ latestPrices['zcoin'] = JSON.parse(JSON.stringify(latestPrices['firo']));
+ }
+
const fromSymbol = getPriceKey(coinFrom);
const toSymbol = getPriceKey(coinTo);
- const fromPriceUSD = latestPrices[fromSymbol]?.usd;
- const toPriceUSD = latestPrices[toSymbol]?.usd;
+
+ let fromPriceUSD = latestPrices && latestPrices[fromSymbol] ? latestPrices[fromSymbol].usd : null;
+ let toPriceUSD = latestPrices && latestPrices[toSymbol] ? latestPrices[toSymbol].usd : null;
+
+ if (!fromPriceUSD || !toPriceUSD) {
+ fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol);
+ toPriceUSD = tableRateModule.getFallbackValue(toSymbol);
+ }
if (fromPriceUSD === null || toPriceUSD === null ||
- fromPriceUSD === undefined || toPriceUSD === undefined) {
+ fromPriceUSD === undefined || toPriceUSD === undefined ||
+ isNaN(fromPriceUSD) || isNaN(toPriceUSD)) {
return `
Exchange Rate Information
Market price data is temporarily unavailable.
@@ -2267,7 +1839,6 @@ function updateTooltipTargets(row, uniqueId) {
});
}
-// FILTER FUNCTIONS
function applyFilters() {
if (filterTimeout) {
clearTimeout(filterTimeout);
@@ -2289,7 +1860,6 @@ function applyFilters() {
}
function clearFilters() {
-
filterForm.reset();
const selectElements = filterForm.querySelectorAll('select');
@@ -2306,6 +1876,9 @@ function clearFilters() {
jsonData = [...originalJsonData];
currentPage = 1;
+
+ const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings';
+ localStorage.removeItem(storageKey);
updateOffersTable();
updateCoinFilterImages();
@@ -2324,18 +1897,16 @@ function hasActiveFilters() {
return hasChangedFilters;
}
-// UTILITY FUNCTIONS
+
function formatTimeLeft(timestamp) {
- const now = Math.floor(Date.now() / 1000);
- if (timestamp <= now) return "Expired";
- return formatTime(timestamp);
+ return window.config.utils.formatTimeLeft(timestamp);
}
function getDisplayName(coinName) {
if (coinName.toLowerCase() === 'zcoin') {
return 'Firo';
}
- return coinNameToDisplayName[coinName] || coinName;
+ return window.config.coinMappings.nameToDisplayName[coinName] || coinName;
}
function getCoinSymbolLowercase(coin) {
@@ -2343,43 +1914,16 @@ function getCoinSymbolLowercase(coin) {
if (coin.toLowerCase() === 'bitcoin cash') {
return 'bitcoin-cash';
}
- return (coinNameToSymbol[coin] || coin).toLowerCase();
+ return (window.config.coinMappings.nameToSymbol[coin] || coin).toLowerCase();
} else if (coin && typeof coin === 'object' && coin.symbol) {
return coin.symbol.toLowerCase();
} else {
- //console.warn('Invalid coin input:', coin);
return 'unknown';
}
}
function coinMatches(offerCoin, filterCoin) {
- if (!offerCoin || !filterCoin) return false;
-
- offerCoin = offerCoin.toLowerCase();
- filterCoin = filterCoin.toLowerCase();
-
- 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')) {
- return true;
- }
-
- const particlVariants = ['particl', 'particl anon', 'particl blind'];
- if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
- return true;
- }
-
- if (particlVariants.includes(filterCoin)) {
- return offerCoin === filterCoin;
- }
-
- return false;
+ return window.config.coinMatches(offerCoin, filterCoin);
}
function getProfitColorClass(percentage) {
@@ -2396,68 +1940,30 @@ function isOfferExpired(offer) {
}
const currentTime = Math.floor(Date.now() / 1000);
const isExpired = offer.expire_at <= currentTime;
- if (isExpired) {
- // console.log(`Offer ${offer.offer_id} is expired. Expire time: ${offer.expire_at}, Current time: ${currentTime}`);
- }
return isExpired;
}
function formatTime(timestamp, addAgoSuffix = false) {
- const now = Math.floor(Date.now() / 1000);
- const diff = Math.abs(now - timestamp);
-
- let timeString;
- if (diff < 60) {
- timeString = `${diff} seconds`;
- } else if (diff < 3600) {
- timeString = `${Math.floor(diff / 60)} minutes`;
- } else if (diff < 86400) {
- timeString = `${Math.floor(diff / 3600)} hours`;
- } else if (diff < 2592000) {
- timeString = `${Math.floor(diff / 86400)} days`;
- } else if (diff < 31536000) {
- timeString = `${Math.floor(diff / 2592000)} months`;
- } else {
- timeString = `${Math.floor(diff / 31536000)} years`;
- }
-
- return addAgoSuffix ? `${timeString} ago` : timeString;
+ return window.config.utils.formatTime(timestamp, addAgoSuffix);
}
function escapeHtml(unsafe) {
- if (typeof unsafe !== 'string') {
- //console.warn('escapeHtml received a non-string value:', unsafe);
- return '';
- }
- return unsafe
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
+ return window.config.utils.escapeHtml(unsafe);
}
function getCoinSymbol(fullName) {
- const symbolMap = {
- 'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR',
- 'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART',
- 'PIVX': 'PIVX', 'Firo': 'FIRO', 'Zcoin': 'FIRO',
- 'Dash': 'DASH', 'Decred': 'DCR', 'Wownero': 'WOW',
- 'Bitcoin Cash': 'BCH', 'Dogecoin': 'DOGE'
- };
- return symbolMap[fullName] || fullName;
+ return window.config.coinMappings.nameToSymbol[fullName] || fullName;
}
-// EVENT LISTENERS
function initializeTableEvents() {
const filterForm = document.getElementById('filterForm');
if (filterForm) {
- EventManager.add(filterForm, 'submit', (e) => {
+ CleanupManager.addListener(filterForm, 'submit', (e) => {
e.preventDefault();
applyFilters();
});
- EventManager.add(filterForm, 'change', () => {
+ CleanupManager.addListener(filterForm, 'change', () => {
applyFilters();
updateClearFiltersButton();
});
@@ -2467,14 +1973,14 @@ function initializeTableEvents() {
const coinFromSelect = document.getElementById('coin_from');
if (coinToSelect) {
- EventManager.add(coinToSelect, 'change', () => {
+ CleanupManager.addListener(coinToSelect, 'change', () => {
applyFilters();
updateCoinFilterImages();
});
}
if (coinFromSelect) {
- EventManager.add(coinFromSelect, 'change', () => {
+ CleanupManager.addListener(coinFromSelect, 'change', () => {
applyFilters();
updateCoinFilterImages();
});
@@ -2482,116 +1988,117 @@ function initializeTableEvents() {
const clearFiltersBtn = document.getElementById('clearFilters');
if (clearFiltersBtn) {
- EventManager.add(clearFiltersBtn, 'click', () => {
+ CleanupManager.addListener(clearFiltersBtn, 'click', () => {
clearFilters();
updateCoinFilterImages();
});
}
-const refreshButton = document.getElementById('refreshOffers');
-if (refreshButton) {
- let lastRefreshTime = 0;
- const REFRESH_COOLDOWN = 6000;
- let countdownInterval;
+ const refreshButton = document.getElementById('refreshOffers');
+ if (refreshButton) {
+ let lastRefreshTime = 0;
+ const REFRESH_COOLDOWN = 6000;
+ let countdownInterval;
- EventManager.add(refreshButton, 'click', async () => {
- const now = Date.now();
- if (now - lastRefreshTime < REFRESH_COOLDOWN) {
- console.log('Refresh rate limited. Please wait before refreshing again.');
- const startTime = now;
- const refreshText = document.getElementById('refreshText');
+ CleanupManager.addListener(refreshButton, 'click', async () => {
+ const now = Date.now();
+ if (now - lastRefreshTime < REFRESH_COOLDOWN) {
+ console.log('Refresh rate limited. Please wait before refreshing again.');
+ const startTime = now;
+ const refreshText = document.getElementById('refreshText');
- refreshButton.classList.remove('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
- refreshButton.classList.add('bg-red-600', 'border-red-500', 'cursor-not-allowed');
+ 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 (countdownInterval) clearInterval(countdownInterval);
- countdownInterval = setInterval(() => {
- const currentTime = Date.now();
- const elapsedTime = currentTime - startTime;
- const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
+ countdownInterval = setInterval(() => {
+ const currentTime = Date.now();
+ const elapsedTime = currentTime - startTime;
+ const remainingTime = Math.ceil((REFRESH_COOLDOWN - elapsedTime) / 1000);
- if (remainingTime <= 0) {
- clearInterval(countdownInterval);
- refreshText.textContent = 'Refresh';
+ if (remainingTime <= 0) {
+ clearInterval(countdownInterval);
+ refreshText.textContent = 'Refresh';
- refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
- refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
- } else {
- refreshText.textContent = `Refresh (${remainingTime}s)`;
- }
- }, 100);
- return;
- }
-
- console.log('Manual refresh initiated');
- lastRefreshTime = now;
- const refreshIcon = document.getElementById('refreshIcon');
- const refreshText = document.getElementById('refreshText');
- refreshButton.disabled = true;
- refreshIcon.classList.add('animate-spin');
- refreshText.textContent = 'Refreshing...';
- refreshButton.classList.add('opacity-75', 'cursor-wait');
-
- try {
- const cachedPrices = CacheManager.get('prices_coingecko');
- const previousPrices = cachedPrices ? cachedPrices.value : null;
- CacheManager.clear();
- window.isManualRefresh = true;
- const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
- const response = await fetch(endpoint);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
+ refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
+ refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
+ } else {
+ refreshText.textContent = `Refresh (${remainingTime}s)`;
+ }
+ }, 100);
+ return;
}
- const newData = await response.json();
- const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
- jsonData = formatInitialData(processedNewData);
- originalJsonData = [...jsonData];
- const priceData = await fetchLatestPrices();
- if (!priceData && previousPrices) {
- console.log('Using previous price data after failed refresh');
- latestPrices = previousPrices;
- await updateOffersTable();
- } else if (priceData) {
- latestPrices = priceData;
- await updateOffersTable();
- } else {
- throw new Error('Unable to fetch price data');
- }
- updatePaginationInfo();
+
+ console.log('Manual refresh initiated');
lastRefreshTime = now;
- updateLastRefreshTime();
+ const refreshIcon = document.getElementById('refreshIcon');
+ const refreshText = document.getElementById('refreshText');
+ refreshButton.disabled = true;
+ refreshIcon.classList.add('animate-spin');
+ refreshText.textContent = 'Refreshing...';
+ refreshButton.classList.add('opacity-75', 'cursor-wait');
- console.log('Manual refresh completed successfully');
+ try {
+ const cachedPrices = CacheManager.get('prices_coingecko');
+ const previousPrices = cachedPrices ? cachedPrices.value : null;
+ CacheManager.clear();
+ window.isManualRefresh = true;
+ const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
+ const response = await fetch(endpoint);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const newData = await response.json();
+ const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
+ jsonData = formatInitialData(processedNewData);
+ originalJsonData = [...jsonData];
+ const priceData = await fetchLatestPrices();
+ if (!priceData && previousPrices) {
+ console.log('Using previous price data after failed refresh');
+ latestPrices = previousPrices;
+ await updateOffersTable();
+ } else if (priceData) {
+ latestPrices = priceData;
+ await updateOffersTable();
+ } else {
+ throw new Error('Unable to fetch price data');
+ }
+ updatePaginationInfo();
+ lastRefreshTime = now;
+ updateLastRefreshTime();
- } catch (error) {
- console.error('Error during manual refresh:', error);
- ui.displayErrorMessage('Unable to refresh data. Previous data will be preserved.');
+ console.log('Manual refresh completed successfully');
- const cachedData = CacheManager.get('prices_coingecko');
- if (cachedData?.value) {
- latestPrices = cachedData.value;
- await updateOffersTable();
+ } catch (error) {
+ console.error('Error during manual refresh:', error);
+ NetworkManager.handleNetworkError(error);
+ ui.displayErrorMessage('Unable to refresh data. Previous data will be preserved.');
+
+ const cachedData = CacheManager.get('prices_coingecko');
+ if (cachedData?.value) {
+ latestPrices = cachedData.value;
+ await updateOffersTable();
+ }
+ } finally {
+ window.isManualRefresh = false;
+ refreshButton.disabled = false;
+ refreshIcon.classList.remove('animate-spin');
+ refreshText.textContent = 'Refresh';
+ refreshButton.classList.remove('opacity-75', 'cursor-wait');
+
+ refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
+ refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
+
+ if (countdownInterval) {
+ clearInterval(countdownInterval);
+ }
}
- } finally {
- window.isManualRefresh = false;
- refreshButton.disabled = false;
- refreshIcon.classList.remove('animate-spin');
- refreshText.textContent = 'Refresh';
- refreshButton.classList.remove('opacity-75', 'cursor-wait');
-
- refreshButton.classList.remove('bg-red-600', 'border-red-500', 'cursor-not-allowed');
- refreshButton.classList.add('bg-blue-600', 'hover:bg-green-600', 'border-blue-500', 'hover:border-green-600');
-
- if (countdownInterval) {
- clearInterval(countdownInterval);
- }
- }
- });
-}
+ });
+ }
document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
- EventManager.add(header, 'click', () => {
+ CleanupManager.addListener(header, 'click', async () => {
const columnIndex = parseInt(header.getAttribute('data-column-index'));
handleTableSort(columnIndex, header);
});
@@ -2601,7 +2108,7 @@ if (refreshButton) {
const nextPageButton = document.getElementById('nextPage');
if (prevPageButton) {
- EventManager.add(prevPageButton, 'click', () => {
+ CleanupManager.addListener(prevPageButton, 'click', () => {
if (currentPage > 1) {
currentPage--;
updateOffersTable();
@@ -2610,7 +2117,7 @@ if (refreshButton) {
}
if (nextPageButton) {
- EventManager.add(nextPageButton, 'click', () => {
+ CleanupManager.addListener(nextPageButton, 'click', () => {
const totalPages = Math.ceil(jsonData.length / itemsPerPage);
if (currentPage < totalPages) {
currentPage++;
@@ -2628,14 +2135,7 @@ function handleTableSort(columnIndex, header) {
currentSortDirection = 'desc';
}
- localStorage.setItem('offersTableSettings', JSON.stringify({
- coin_to: document.getElementById('coin_to').value,
- coin_from: document.getElementById('coin_from').value,
- status: document.getElementById('status')?.value || 'any',
- sent_from: document.getElementById('sent_from').value,
- sortColumn: currentSortColumn,
- sortDirection: currentSortDirection
- }));
+ saveFilterSettings();
document.querySelectorAll('th[data-sortable="true"]').forEach(th => {
const columnSpan = th.querySelector('span:not(.sort-icon)');
@@ -2674,39 +2174,6 @@ function handleTableSort(columnIndex, header) {
}, 100);
}
-// TIMER MANAGEMENT
-const timerManager = {
- intervals: [],
- timeouts: [],
-
- addInterval(callback, delay) {
- const intervalId = setInterval(callback, delay);
- this.intervals.push(intervalId);
- return intervalId;
- },
-
- addTimeout(callback, delay) {
- const timeoutId = setTimeout(callback, delay);
- this.timeouts.push(timeoutId);
- return timeoutId;
- },
-
- clearAllIntervals() {
- this.intervals.forEach(clearInterval);
- this.intervals = [];
- },
-
- clearAllTimeouts() {
- this.timeouts.forEach(clearTimeout);
- this.timeouts = [];
- },
-
- clearAll() {
- this.clearAllIntervals();
- this.clearAllTimeouts();
- }
-};
-
async function initializeTableAndData() {
loadSavedSettings();
updateClearFiltersButton();
@@ -2719,12 +2186,15 @@ async function initializeTableAndData() {
applyFilters();
} catch (error) {
console.error('Error loading initial data:', error);
+ NetworkManager.handleNetworkError(error);
ui.displayErrorMessage('Error loading data. Retrying in background...');
}
}
function loadSavedSettings() {
- const saved = localStorage.getItem('offersTableSettings');
+ const storageKey = isSentOffers ? 'sentOffersTableSettings' : 'networkOffersTableSettings';
+ const saved = localStorage.getItem(storageKey);
+
if (saved) {
const settings = JSON.parse(saved);
@@ -2755,174 +2225,162 @@ function updateSortIndicators() {
}
document.addEventListener('DOMContentLoaded', async () => {
- const tableLoadPromise = initializeTableAndData();
+ if (window.NetworkManager && !window.networkManagerInitialized) {
+ NetworkManager.initialize({
+ connectionTestEndpoint: '/json',
+ connectionTestTimeout: 3000,
+ reconnectDelay: 5000,
+ maxReconnectAttempts: 5
+ });
+ window.networkManagerInitialized = true;
+ }
- WebSocketManager.initialize();
+ NetworkManager.addHandler('offline', () => {
+ ui.displayErrorMessage("Network connection lost. Will automatically retry when connection is restored.");
+ updateConnectionStatus('disconnected');
+ });
+
+ NetworkManager.addHandler('reconnected', () => {
+ ui.hideErrorMessage();
+ updateConnectionStatus('connected');
+ fetchOffers();
+ });
+
+ NetworkManager.addHandler('maxAttemptsReached', () => {
+ ui.displayErrorMessage("Server connection lost. Please check your internet connection and try refreshing the page.");
+ updateConnectionStatus('error');
+ });
+
+ const tableLoadPromise = initializeTableAndData();
+
+ WebSocketManager.initialize({
+ debug: false
+ });
+
+ WebSocketManager.addMessageHandler('message', async (message) => {
+ try {
+ if (!NetworkManager.isOnline()) {
+ return;
+ }
+
+ const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
+ const response = await fetch(endpoint);
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+
+ const newData = await response.json();
+ const fetchedOffers = Array.isArray(newData) ? newData : Object.values(newData);
+
+ jsonData = formatInitialData(fetchedOffers);
+ originalJsonData = [...jsonData];
+
+ CacheManager.set('offers_cached', jsonData, 'offers');
+
+ requestAnimationFrame(() => {
+ updateOffersTable();
+ updatePaginationInfo();
+ });
+ } catch (error) {
+ console.error('[Debug] Error processing WebSocket message:', error);
+ NetworkManager.handleNetworkError(error);
+ }
+ });
await tableLoadPromise;
- timerManager.addInterval(() => {
- if (WebSocketManager.isConnected()) {
- console.log('🟢 WebSocket connection established for Offers');
- }
- }, 30000);
-
- timerManager.addInterval(() => {
+ CleanupManager.setInterval(() => {
CacheManager.cleanup();
}, 300000);
- timerManager.addInterval(updateRowTimes, 900000);
+ CleanupManager.setInterval(updateRowTimes, 900000);
- EventManager.add(document, 'visibilitychange', () => {
+ if (window.MemoryManager) {
+ MemoryManager.enableAutoCleanup();
+ }
+
+ CleanupManager.addListener(document, 'visibilitychange', () => {
if (!document.hidden) {
if (!WebSocketManager.isConnected()) {
WebSocketManager.connect();
}
+
+ if (NetworkManager.isOnline()) {
+ fetchLatestPrices().then(priceData => {
+ if (priceData) {
+ latestPrices = priceData;
+ updateProfitLossDisplays();
+ }
+ });
+ }
}
});
- EventManager.add(window, 'beforeunload', () => {
+ CleanupManager.addListener(window, 'beforeunload', () => {
cleanup();
});
});
async function cleanup() {
- const debug = {
- startTime: Date.now(),
- steps: [],
- errors: [],
- addStep: function(step, details = null) {
- const timeFromStart = Date.now() - this.startTime;
- console.log(`[Cleanup ${timeFromStart}ms] ${step}`, details || '');
- this.steps.push({ step, time: timeFromStart, details });
- },
- addError: function(step, error) {
- const timeFromStart = Date.now() - this.startTime;
- console.error(`[Cleanup Error ${timeFromStart}ms] ${step}:`, error);
- this.errors.push({ step, error, time: timeFromStart });
- },
- summarizeLogs: function() {
- console.log('Cleanup Summary:');
- console.log(`Total cleanup time: ${Date.now() - this.startTime}ms`);
- console.log(`Steps completed: ${this.steps.length}`);
- console.log(`Errors encountered: ${this.errors.length}`);
- }
- };
+ console.log('Starting cleanup process');
try {
- debug.addStep('Starting cleanup process');
- debug.addStep('Starting tooltip cleanup');
+ if (filterTimeout) {
+ clearTimeout(filterTimeout);
+ filterTimeout = null;
+ }
+
+ if (window.WebSocketManager) {
+ WebSocketManager.disconnect();
+ WebSocketManager.dispose();
+ }
+
if (window.TooltipManager) {
window.TooltipManager.cleanup();
+ window.TooltipManager.dispose();
}
- debug.addStep('Tooltip cleanup completed');
- debug.addStep('Clearing timers');
- const timerCount = timerManager.intervals.length + timerManager.timeouts.length;
- timerManager.clearAll();
- debug.addStep('Timers cleared', `Cleaned up ${timerCount} timers`);
-
- debug.addStep('Starting WebSocket cleanup');
- await Promise.resolve(WebSocketManager.cleanup()).catch(error => {
- debug.addError('WebSocket cleanup', error);
- });
- debug.addStep('WebSocket cleanup completed');
-
- debug.addStep('Clearing event listeners');
- const listenerCount = EventManager.listeners.size;
- EventManager.clearAll();
- debug.addStep('Event listeners cleared', `Cleaned up ${listenerCount} listeners`);
-
- debug.addStep('Starting table cleanup');
- const rowCount = offersBody ? offersBody.querySelectorAll('tr').length : 0;
cleanupTable();
- debug.addStep('Table cleanup completed', `Cleaned up ${rowCount} rows`);
- debug.addStep('Resetting global state');
- const globals = {
- currentPage: currentPage,
- dataLength: jsonData.length,
- originalDataLength: originalJsonData.length
- };
- currentPage = 1;
+ CleanupManager.clearAll();
+
+ latestPrices = null;
jsonData = [];
originalJsonData = [];
+ lastRefreshTime = null;
+
+ const domRefs = [
+ 'offersBody', 'filterForm', 'prevPageButton', 'nextPageButton',
+ 'currentPageSpan', 'totalPagesSpan', 'lastRefreshTimeSpan', 'newEntriesCountSpan'
+ ];
+
+ domRefs.forEach(ref => {
+ if (window[ref]) window[ref] = null;
+ });
+
+ if (window.tableRateModule) {
+ window.tableRateModule.cache = {};
+ window.tableRateModule.processedOffers.clear();
+ }
+
+ currentPage = 1;
currentSortColumn = 0;
currentSortDirection = 'desc';
- filterTimeout = null;
- latestPrices = null;
- lastRefreshTime = null;
- debug.addStep('Global state reset', globals);
- debug.addStep('Clearing global references');
- [
- 'WebSocketManager',
- 'tableRateModule',
- 'offersBody',
- 'filterForm',
- 'prevPageButton',
- 'nextPageButton',
- 'currentPageSpan',
- 'totalPagesSpan',
- 'lastRefreshTimeSpan',
- 'newEntriesCountSpan'
- ].forEach(ref => {
- if (window[ref]) {
- window[ref] = null;
- }
- });
- debug.addStep('Global references cleared');
-
- debug.addStep('Cleaning up tooltip containers');
- const tooltipContainers = document.querySelectorAll('.tooltip-container');
- tooltipContainers.forEach(container => {
- if (container && container.parentNode) {
- container.parentNode.removeChild(container);
- }
- });
- debug.addStep('Tooltip containers cleaned up');
-
- debug.addStep('Clearing document/window events');
- ['visibilitychange', 'beforeunload', 'scroll'].forEach(event => {
- document.removeEventListener(event, null);
- window.removeEventListener(event, null);
- });
- debug.addStep('Document/window events cleared');
-
- debug.addStep('Clearing localStorage items');
- try {
- localStorage.removeItem('tableSortColumn');
- localStorage.removeItem('tableSortDirection');
- debug.addStep('localStorage items cleared');
- } catch (e) {
- debug.addError('localStorage cleanup', e);
+ if (window.MemoryManager) {
+ MemoryManager.forceCleanup();
}
+ console.log('Offers table cleanup completed');
} catch (error) {
- debug.addError('Main cleanup process', error);
+ console.error('Error during offers cleanup:', error);
- debug.addStep('Starting failsafe cleanup');
try {
- if (window.TooltipManager) {
- window.TooltipManager.cleanup();
- }
- WebSocketManager.cleanup();
- EventManager.clearAll();
- timerManager.clearAll();
- if (window.ws) {
- window.ws.close();
- window.ws = null;
- }
- debug.addStep('Failsafe cleanup completed');
- } catch (criticalError) {
- debug.addError('Critical failsafe cleanup', criticalError);
+ CleanupManager.clearAll();
+ cleanupTable();
+ } catch (e) {
+ console.error('Failsafe cleanup failed:', e);
}
- } finally {
- debug.summarizeLogs();
}
}
window.cleanup = cleanup;
-
-//console.log('Offers Table Module fully initialized');
diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js
index 010ab5e..05e7147 100644
--- a/basicswap/static/js/pricechart.js
+++ b/basicswap/static/js/pricechart.js
@@ -1,184 +1,22 @@
-// CLEANUP
-const cleanupManager = {
- eventListeners: [],
- timeouts: [],
- intervals: [],
- animationFrames: [],
+const chartConfig = window.config.chartConfig;
+const coins = window.config.coins;
+const apiKeys = window.config.getAPIKeys();
- addListener: function(element, type, handler, options) {
- if (!element) return null;
- element.addEventListener(type, handler, options);
- this.eventListeners.push({ element, type, handler, options });
- return handler;
- },
-
- setTimeout: function(callback, delay) {
- const id = setTimeout(callback, delay);
- this.timeouts.push(id);
- return id;
- },
-
- setInterval: function(callback, delay) {
- const id = setInterval(callback, delay);
- this.intervals.push(id);
- return id;
- },
-
- requestAnimationFrame: function(callback) {
- const id = requestAnimationFrame(callback);
- this.animationFrames.push(id);
- return id;
- },
-
- clearAll: function() {
- this.eventListeners.forEach(({ element, type, handler, options }) => {
- if (element) {
- try {
- element.removeEventListener(type, handler, options);
- } catch (e) {
- console.warn('Error removing event listener:', e);
- }
- }
- });
- this.eventListeners = [];
-
- this.timeouts.forEach(id => clearTimeout(id));
- this.timeouts = [];
-
- this.intervals.forEach(id => clearInterval(id));
- this.intervals = [];
-
- this.animationFrames.forEach(id => cancelAnimationFrame(id));
- this.animationFrames = [];
-
- console.log('All resources cleaned up');
- },
-
- clearTimeouts: function() {
- this.timeouts.forEach(id => clearTimeout(id));
- this.timeouts = [];
- },
-
- clearIntervals: function() {
- this.intervals.forEach(id => clearInterval(id));
- this.intervals = [];
- },
-
- removeListenersByElement: function(element) {
- if (!element) return;
-
- const listenersToRemove = this.eventListeners.filter(
- listener => listener.element === element
- );
-
- listenersToRemove.forEach(({ element, type, handler, options }) => {
- try {
- element.removeEventListener(type, handler, options);
- } catch (e) {
- console.warn('Error removing event listener:', e);
- }
- });
-
- this.eventListeners = this.eventListeners.filter(
- listener => listener.element !== element
- );
- }
-};
-
-// MEMORY
-const memoryMonitor = {
- isEnabled: true,
- lastLogTime: 0,
- logInterval: 5 * 60 * 1000,
- monitorInterval: null,
-
- startMonitoring: function() {
- console.log('Starting memory monitoring');
- if (!this.isEnabled) return;
-
- if (this.monitorInterval) {
- clearInterval(this.monitorInterval);
- }
-
- this.monitorInterval = setInterval(() => {
- this.logMemoryUsage();
- }, this.logInterval);
-
- this.logMemoryUsage();
- },
-
- logMemoryUsage: function() {
- console.log('Logging memory usage');
- if (window.performance && window.performance.memory) {
- const memory = window.performance.memory;
- console.log(`Memory Usage: ${Math.round(memory.usedJSHeapSize / (1024 * 1024))}MB / ${Math.round(memory.jsHeapSizeLimit / (1024 * 1024))}MB`);
- }
- },
-
- stopMonitoring: function() {
- if (this.monitorInterval) {
- clearInterval(this.monitorInterval);
- this.monitorInterval = null;
- }
- }
-};
-
-// CONFIG
-const config = {
- apiKeys: getAPIKeys(),
- coins: [
- { symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: true, 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: 'bitcoin-cash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
- { symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
- { symbol: 'FIRO', name: 'zcoin', 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: 'ETH', name: 'ethereum', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
- { symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
- { symbol: 'ZANO', name: 'zano', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
- { symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }
- ],
- apiEndpoints: {
- cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
- coinGecko: 'https://api.coingecko.com/api/v3',
- cryptoCompareHistorical: 'https://min-api.cryptocompare.com/data/v2/histoday'
- },
- chartColors: {
- default: {
- lineColor: 'rgba(77, 132, 240, 1)',
- backgroundColor: 'rgba(77, 132, 240, 0.1)'
- }
- },
- showVolume: false,
- cacheTTL: 10 * 60 * 1000,
- specialCoins: [''],
- resolutions: {
- year: { days: 365, interval: 'month' },
- sixMonths: { days: 180, interval: 'daily' },
- day: { days: 1, interval: 'hourly' }
- },
- currentResolution: 'year',
- requestTimeout: 60000, // 60 sec
- retryDelays: [5000, 15000, 30000],
- rateLimits: {
- coingecko: {
- requestsPerMinute: 50,
- minInterval: 1200 // 1.2 sec
- },
- cryptocompare: {
- requestsPerMinute: 30,
- minInterval: 2000 // 2 sec
- }
- }
-};
-
-// UTILS
const utils = {
- formatNumber: (number, decimals = 2) =>
- number.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ','),
+ 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 = {
@@ -188,7 +26,6 @@ const utils = {
};
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
},
-
debounce: (func, delay) => {
let timeoutId;
return (...args) => {
@@ -198,7 +35,6 @@ const utils = {
}
};
-// ERROR
class AppError extends Error {
constructor(message, type = 'AppError') {
super(message);
@@ -206,7 +42,6 @@ class AppError extends Error {
}
}
-// LOG
const logger = {
log: (message) => console.log(`[AppLog] ${new Date().toISOString()}: ${message}`),
warn: (message) => console.warn(`[AppWarn] ${new Date().toISOString()}: ${message}`),
@@ -214,153 +49,192 @@ const logger = {
};
const api = {
- makePostRequest: (url, headers = {}) => {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.open('POST', '/json/readurl');
- xhr.setRequestHeader('Content-Type', 'application/json');
- xhr.timeout = config.requestTimeout;
-
- xhr.ontimeout = () => {
- logger.warn(`Request timed out for ${url}`);
- reject(new AppError('Request timed out'));
- };
-
- xhr.onload = () => {
- logger.log(`Response for ${url}:`, xhr.responseText);
- if (xhr.status === 200) {
- try {
- const response = JSON.parse(xhr.responseText);
- if (response.Error) {
- logger.error(`API Error for ${url}:`, response.Error);
- reject(new AppError(response.Error, 'APIError'));
- } else {
- resolve(response);
- }
- } catch (error) {
- logger.error(`Invalid JSON response for ${url}:`, xhr.responseText);
- reject(new AppError(`Invalid JSON response: ${error.message}`, 'ParseError'));
+ fetchVolumeDataXHR: async () => {
+ const cacheKey = 'volumeData';
+ const cachedData = CacheManager.get(cacheKey);
+
+ if (cachedData) {
+ console.log("Using cached volume data");
+ return cachedData.value;
+ }
+
+ try {
+ if (!NetworkManager.isOnline()) {
+ throw new Error('Network is offline');
+ }
+
+ const volumeData = await Api.fetchVolumeData({
+ cryptoCompare: apiKeys.cryptoCompare,
+ coinGecko: apiKeys.coinGecko
+ });
+
+ if (Object.keys(volumeData).length > 0) {
+ CacheManager.set(cacheKey, volumeData, 'volume');
+ return volumeData;
+ }
+
+ throw new Error("No volume data found in the response");
+ } catch (error) {
+ console.error("Error fetching volume data:", error);
+
+ NetworkManager.handleNetworkError(error);
+
+ try {
+ const existingCache = localStorage.getItem(cacheKey);
+ if (existingCache) {
+ const fallbackData = JSON.parse(existingCache).value;
+ if (fallbackData && Object.keys(fallbackData).length > 0) {
+ return fallbackData;
}
- } else {
- logger.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`);
- reject(new AppError(`HTTP Error: ${xhr.status} ${xhr.statusText}`, 'HTTPError'));
}
- };
-
- xhr.onerror = () => {
- logger.error(`Network error occurred for ${url}`);
- reject(new AppError('Network error occurred', 'NetworkError'));
- };
-
- xhr.send(JSON.stringify({
- url: url,
- headers: headers
- }));
- });
+ } catch (e) {
+ console.warn("Error accessing cached volume data:", e);
+ }
+ return {};
+ }
},
fetchCryptoCompareDataXHR: (coin) => {
- return rateLimiter.queueRequest('cryptocompare', async () => {
- const url = `${config.apiEndpoints.cryptoCompare}?fsyms=${coin}&tsyms=USD,BTC&api_key=${config.apiKeys.cryptoCompare}`;
- const headers = {
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
- 'Accept-Language': 'en-US,en;q=0.5',
- };
- try {
- return await api.makePostRequest(url, headers);
- } catch (error) {
- logger.error(`CryptoCompare request failed for ${coin}:`, error);
- const cachedData = cache.get(`coinData_${coin}`);
- if (cachedData) {
- logger.info(`Using cached data for ${coin}`);
- return cachedData.value;
- }
- return { error: error.message };
+ 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 () => {
const cacheKey = 'coinGeckoOneLiner';
- const cachedData = cache.get(cacheKey);
+ const cachedData = CacheManager.get(cacheKey);
if (cachedData) {
- //console.log('Using cached CoinGecko data');
return cachedData.value;
}
- return rateLimiter.queueRequest('coingecko', async () => {
+ try {
+ if (!NetworkManager.isOnline()) {
+ throw new Error('Network is offline');
+ }
+
+ const existingCache = localStorage.getItem(cacheKey);
+ let fallbackData = null;
+
+ if (existingCache) {
+ try {
+ const parsed = JSON.parse(existingCache);
+ fallbackData = parsed.value;
+ } catch (e) {
+ console.warn('Failed to parse existing cache:', e);
+ }
+ }
+
+ const apiResponse = await Api.fetchCoinGeckoData({
+ coinGecko: window.config.getAPIKeys().coinGecko
+ });
+
+ if (!apiResponse || !apiResponse.rates) {
+ if (fallbackData) {
+ return fallbackData;
+ }
+ throw new Error('Invalid data structure received from API');
+ }
+
+ const transformedData = {};
+ window.config.coins.forEach(coin => {
+ const coinName = coin.name;
+ const coinRate = apiResponse.rates[coinName];
+ if (coinRate) {
+ const symbol = coin.symbol.toLowerCase();
+ transformedData[symbol] = {
+ current_price: coinRate,
+ price_btc: coinName === 'bitcoin' ? 1 : coinRate / (apiResponse.rates.bitcoin || 1),
+ total_volume: fallbackData && fallbackData[symbol] ? fallbackData[symbol].total_volume : null,
+ price_change_percentage_24h: fallbackData && fallbackData[symbol] ? fallbackData[symbol].price_change_percentage_24h : null,
+ displayName: coin.displayName || coin.symbol || coinName
+ };
+ }
+ });
+
+ try {
+ if (!transformedData['wow'] && config.coins.some(c => c.symbol === 'WOW')) {
+ const wowResponse = await Api.fetchCoinPrices("wownero", {
+ coinGecko: window.config.getAPIKeys().coinGecko
+ });
+
+ if (wowResponse && wowResponse.rates && wowResponse.rates.wownero) {
+ transformedData['wow'] = {
+ current_price: wowResponse.rates.wownero,
+ price_btc: transformedData.btc ? wowResponse.rates.wownero / transformedData.btc.current_price : 0,
+ total_volume: fallbackData && fallbackData['wow'] ? fallbackData['wow'].total_volume : null,
+ price_change_percentage_24h: fallbackData && fallbackData['wow'] ? fallbackData['wow'].price_change_percentage_24h : null,
+ displayName: 'Wownero'
+ };
+ }
+ }
+ } catch (wowError) {
+ console.error('Error fetching WOW price:', wowError);
+ }
+
+ const missingCoins = window.config.coins.filter(coin =>
+ !transformedData[coin.symbol.toLowerCase()] &&
+ fallbackData &&
+ fallbackData[coin.symbol.toLowerCase()]
+ );
+
+ missingCoins.forEach(coin => {
+ const symbol = coin.symbol.toLowerCase();
+ if (fallbackData && fallbackData[symbol]) {
+ transformedData[symbol] = fallbackData[symbol];
+ }
+ });
+
+ CacheManager.set(cacheKey, transformedData, 'prices');
+
+ if (NetworkManager.getReconnectAttempts() > 0) {
+ NetworkManager.resetReconnectAttempts();
+ }
+
+ return transformedData;
+ } catch (error) {
+ console.error('Error fetching coin data:', error);
+
+ NetworkManager.handleNetworkError(error);
+
+ const cachedData = CacheManager.get(cacheKey);
+ if (cachedData) {
+ console.log('Using cached data due to error');
+ return cachedData.value;
+ }
+
try {
const existingCache = localStorage.getItem(cacheKey);
- let fallbackData = null;
-
if (existingCache) {
- try {
- const parsed = JSON.parse(existingCache);
- fallbackData = parsed.value;
- } catch (e) {
- console.warn('Failed to parse existing cache:', e);
+ const parsed = JSON.parse(existingCache);
+ if (parsed.value) {
+ console.log('Using expired cache as last resort');
+ return parsed.value;
}
}
-
- const coinIds = config.coins
- .filter(coin => coin.usesCoinGecko)
- .map(coin => coin.name)
- .join(',');
-
- const url = `${config.apiEndpoints.coinGecko}/simple/price?ids=${coinIds}&vs_currencies=usd,btc&include_24hr_vol=true&include_24hr_change=true&api_key=${config.apiKeys.coinGecko}`;
-
- const response = await api.makePostRequest(url, {
- 'User-Agent': 'Mozilla/5.0',
- 'Accept': 'application/json',
- 'Accept-Language': 'en-US,en;q=0.5'
- });
-
- if (typeof response !== 'object' || response === null) {
- if (fallbackData) {
- //console.log('Using fallback data due to invalid response');
- return fallbackData;
- }
- throw new AppError('Invalid data structure received from CoinGecko');
- }
-
- if (response.error || response.Error) {
- if (fallbackData) {
- //console.log('Using fallback data due to API error');
- return fallbackData;
- }
- throw new AppError(response.error || response.Error);
- }
-
- const transformedData = {};
- Object.entries(response).forEach(([id, values]) => {
- const coinConfig = config.coins.find(coin => coin.name === id);
- const symbol = coinConfig?.symbol.toLowerCase() || id;
- transformedData[symbol] = {
- current_price: values.usd,
- price_btc: values.btc,
- total_volume: values.usd_24h_vol,
- price_change_percentage_24h: values.usd_24h_change,
- displayName: coinConfig?.displayName || coinConfig?.symbol || id
- };
- });
-
- cache.set(cacheKey, transformedData);
- return transformedData;
-
- } catch (error) {
- console.error('Error fetching CoinGecko data:', error);
-
- const cachedData = cache.get(cacheKey);
- if (cachedData) {
- //console.log('Using expired cache data due to error');
- return cachedData.value;
- }
-
- throw error;
+ } catch (e) {
+ console.warn('Failed to parse expired cache:', e);
}
- });
+
+ throw error;
+ }
},
fetchHistoricalDataXHR: async (coinSymbols) => {
@@ -369,78 +243,57 @@ const api = {
}
const results = {};
- const fetchPromises = coinSymbols.map(async coin => {
- const coinConfig = config.coins.find(c => c.symbol === coin);
- if (!coinConfig) return;
- const cacheKey = `historical_${coin}_${config.currentResolution}`;
- const cachedData = cache.get(cacheKey);
- if (cachedData) {
- results[coin] = cachedData.value;
- return;
+ try {
+ if (!NetworkManager.isOnline()) {
+ throw new Error('Network is offline');
}
- if (coin === 'WOW') {
- return rateLimiter.queueRequest('coingecko', async () => {
- const url = `${config.apiEndpoints.coinGecko}/coins/wownero/market_chart?vs_currency=usd&days=1&api_key=${config.apiKeys.coinGecko}`;
- try {
- const response = await api.makePostRequest(url);
- if (response && response.prices) {
- results[coin] = response.prices;
- cache.set(cacheKey, response.prices);
- }
- } catch (error) {
- console.error(`Error fetching CoinGecko data for WOW:`, error);
- if (cachedData) {
- results[coin] = cachedData.value;
- }
- }
- });
- } else {
- return rateLimiter.queueRequest('cryptocompare', async () => {
- const resolution = config.resolutions[config.currentResolution];
- let url;
- if (resolution.interval === 'hourly') {
- url = `https://min-api.cryptocompare.com/data/v2/histohour?fsym=${coin}&tsym=USD&limit=${resolution.days * 24}&api_key=${config.apiKeys.cryptoCompare}`;
- } else {
- url = `${config.apiEndpoints.cryptoCompareHistorical}?fsym=${coin}&tsym=USD&limit=${resolution.days}&api_key=${config.apiKeys.cryptoCompare}`;
- }
+ const historicalData = await Api.fetchHistoricalData(
+ coinSymbols,
+ window.config.currentResolution,
+ {
+ cryptoCompare: window.config.getAPIKeys().cryptoCompare
+ }
+ );
- try {
- const response = await api.makePostRequest(url);
- if (response.Response === "Error") {
- console.error(`API Error for ${coin}:`, response.Message);
- if (cachedData) {
- results[coin] = cachedData.value;
- }
- } else if (response.Data && response.Data.Data) {
- results[coin] = response.Data;
- cache.set(cacheKey, response.Data);
- }
- } catch (error) {
- console.error(`Error fetching CryptoCompare data for ${coin}:`, error);
- if (cachedData) {
- results[coin] = cachedData.value;
- }
- }
- });
+ Object.keys(historicalData).forEach(coin => {
+ if (historicalData[coin]) {
+ results[coin] = historicalData[coin];
+
+ const cacheKey = `historical_${coin}_${window.config.currentResolution}`;
+ CacheManager.set(cacheKey, historicalData[coin], 'historical');
+ }
+ });
+
+ return results;
+ } catch (error) {
+ console.error('Error fetching historical data:', error);
+
+ NetworkManager.handleNetworkError(error);
+
+ for (const coin of coinSymbols) {
+ const cacheKey = `historical_${coin}_${window.config.currentResolution}`;
+ const cachedData = CacheManager.get(cacheKey);
+ if (cachedData) {
+ results[coin] = cachedData.value;
+ }
}
- });
-
- await Promise.all(fetchPromises);
- return results;
- }
+
+ return results;
+ }
+ },
};
const rateLimiter = {
lastRequestTime: {},
minRequestInterval: {
- coingecko: config.rateLimits.coingecko.minInterval,
- cryptocompare: config.rateLimits.cryptocompare.minInterval
+ coingecko: window.config.rateLimits.coingecko.minInterval,
+ cryptocompare: window.config.rateLimits.cryptocompare.minInterval
},
requestQueue: {},
- retryDelays: config.retryDelays,
-
+ retryDelays: window.config.retryDelays,
+
canMakeRequest: function(apiName) {
const now = Date.now();
const lastRequest = this.lastRequestTime[apiName] || 0;
@@ -485,11 +338,7 @@ const rateLimiter = {
if ((error.message.includes('timeout') || error.name === 'NetworkError') &&
retryCount < this.retryDelays.length) {
const delay = this.retryDelays[retryCount];
- logger.warn(`Request failed, retrying in ${delay/1000} seconds...`, {
- apiName,
- retryCount,
- error: error.message
- });
+ logger.warn(`Request failed, retrying in ${delay/1000} seconds...`);
await new Promise(resolve => setTimeout(resolve, delay));
return this.queueRequest(apiName, requestFn, retryCount + 1);
}
@@ -500,14 +349,15 @@ const rateLimiter = {
this.requestQueue[apiName] = executeRequest();
return await this.requestQueue[apiName];
-
} catch (error) {
if (error.message.includes('429') ||
error.message.includes('timeout') ||
error.name === 'NetworkError') {
- const cachedData = cache.get(`coinData_${apiName}`);
+
+ NetworkManager.handleNetworkError(error);
+
+ const cachedData = CacheManager.get(`coinData_${apiName}`);
if (cachedData) {
- //console.log('Using cached data due to request failure');
return cachedData.value;
}
}
@@ -516,190 +366,65 @@ const rateLimiter = {
}
};
-// CACHE
-const cache = {
- maxSizeBytes: 10 * 1024 * 1024,
- maxItems: 200,
- cacheTTL: 5 * 60 * 1000,
-
- set: function(key, value, customTtl = null) {
- this.cleanup();
-
- const item = {
- value: value,
- timestamp: Date.now(),
- expiresAt: Date.now() + (customTtl || this.cacheTTL)
- };
-
- try {
- const serialized = JSON.stringify(item);
- localStorage.setItem(key, serialized);
- } catch (e) {
- console.warn('Cache set error:', e);
- this.clear();
- try {
- const serialized = JSON.stringify(item);
- localStorage.setItem(key, serialized);
- } catch (e2) {
- console.error('Failed to store in cache even after cleanup:', e2);
- }
- }
- },
-
- get: function(key) {
- const itemStr = localStorage.getItem(key);
- if (!itemStr) {
- return null;
- }
-
- try {
- const item = JSON.parse(itemStr);
- const now = Date.now();
-
- if (now < item.expiresAt) {
- return {
- value: item.value,
- remainingTime: item.expiresAt - now
- };
- } else {
- localStorage.removeItem(key);
- }
- } catch (error) {
- console.error('Error parsing cache item:', error.message);
- localStorage.removeItem(key);
- }
-
- return null;
- },
-
- isValid: function(key) {
- return this.get(key) !== null;
- },
-
- clear: function() {
- const keysToRemove = [];
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
- keysToRemove.push(key);
- }
- }
-
- keysToRemove.forEach(key => {
- localStorage.removeItem(key);
- });
-
- console.log(`Cache cleared: removed ${keysToRemove.length} items`);
- },
-
- cleanup: function() {
- let totalSize = 0;
- const items = [];
- const keysToRemove = [];
- const now = Date.now();
-
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i);
- if (key.startsWith('coinData_') || key.startsWith('chartData_') || key === 'coinGeckoOneLiner') {
- try {
- const value = localStorage.getItem(key);
- const size = new Blob([value]).size;
-
- const item = JSON.parse(value);
-
- if (item.expiresAt && item.expiresAt < now) {
- keysToRemove.push(key);
- continue;
- }
-
- totalSize += size;
- items.push({
- key,
- size,
- timestamp: item.timestamp || 0,
- expiresAt: item.expiresAt || 0
- });
- } catch (e) {
- keysToRemove.push(key);
- }
- }
- }
-
- keysToRemove.forEach(key => {
- localStorage.removeItem(key);
- });
-
- if (totalSize > this.maxSizeBytes || items.length > this.maxItems) {
- items.sort((a, b) => a.timestamp - b.timestamp);
-
- const itemsToRemove = Math.max(
- Math.ceil(items.length * 0.2),
- items.length - this.maxItems
- );
-
- items.slice(0, itemsToRemove).forEach(item => {
- localStorage.removeItem(item.key);
- });
-
- console.log(`Cache cleanup: removed ${itemsToRemove} items, freed ${Math.round((totalSize - this.maxSizeBytes) / 1024)}KB`);
- }
-
- return {
- totalSize,
- itemCount: items.length,
- removedCount: keysToRemove.length
- };
- }
-};
-
-// UI
const ui = {
-displayCoinData: (coin, data) => {
+ displayCoinData: (coin, data) => {
let priceUSD, priceBTC, priceChange1d, volume24h;
const updateUI = (isError = false) => {
- const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`);
- const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`);
- const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
- const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
- const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
- if (priceUsdElement) {
- priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
+ const priceUsdElement = document.querySelector(`#${coin.toLowerCase()}-price-usd`);
+ const volumeDiv = document.querySelector(`#${coin.toLowerCase()}-volume-div`);
+ const volumeElement = document.querySelector(`#${coin.toLowerCase()}-volume-24h`);
+ const btcPriceDiv = document.querySelector(`#${coin.toLowerCase()}-btc-price-div`);
+ const priceBtcElement = document.querySelector(`#${coin.toLowerCase()}-price-btc`);
+
+ if (priceUsdElement) {
+ priceUsdElement.textContent = isError ? 'N/A' : `$ ${ui.formatPrice(coin, priceUSD)}`;
+ }
+
+ if (volumeDiv && volumeElement) {
+ if (isError || volume24h === null || volume24h === undefined) {
+ volumeElement.textContent = 'N/A';
+ } else {
+ volumeElement.textContent = `${utils.formatNumber(volume24h, 0)} USD`;
}
- if (volumeDiv && volumeElement) {
- volumeElement.textContent = isError ? 'N/A' : `${utils.formatNumber(volume24h, 0)} USD`;
- volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
+ volumeDiv.style.display = volumeToggle.isVisible ? 'flex' : 'none';
+ }
+
+ if (btcPriceDiv && priceBtcElement) {
+ if (coin === 'BTC') {
+ btcPriceDiv.style.display = 'none';
+ } else {
+ priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`;
+ btcPriceDiv.style.display = 'flex';
}
- if (btcPriceDiv && priceBtcElement) {
- if (coin === 'BTC') {
- btcPriceDiv.style.display = 'none';
- } else {
- priceBtcElement.textContent = isError ? 'N/A' : `${priceBTC.toFixed(8)}`;
- btcPriceDiv.style.display = 'flex';
- }
- }
- ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
+ }
+
+ ui.updatePriceChangeContainer(coin, isError ? null : priceChange1d);
};
+
try {
- if (data.error) {
- throw new Error(data.error);
- }
- if (!data || !data.current_price) {
- throw new Error(`Invalid CoinGecko data structure for ${coin}`);
- }
- priceUSD = data.current_price;
- priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD);
- priceChange1d = data.price_change_percentage_24h;
- volume24h = data.total_volume;
- if (isNaN(priceUSD) || isNaN(priceBTC) || isNaN(volume24h)) {
- throw new Error(`Invalid numeric values in data for ${coin}`);
- }
- updateUI(false);
+ if (data.error) {
+ throw new Error(data.error);
+ }
+
+ if (!data || !data.current_price) {
+ throw new Error(`Invalid data structure for ${coin}`);
+ }
+
+ priceUSD = data.current_price;
+ priceBTC = coin === 'BTC' ? 1 : data.price_btc || (data.current_price / app.btcPriceUSD);
+ priceChange1d = data.price_change_percentage_24h || 0;
+ volume24h = data.total_volume || 0;
+
+ if (isNaN(priceUSD) || isNaN(priceBTC)) {
+ throw new Error(`Invalid numeric values in data for ${coin}`);
+ }
+
+ updateUI(false);
} catch (error) {
- logger.error(`Failed to display data for ${coin}:`, error.message);
- updateUI(true); // Show error state in UI
-}
-},
+ logger.error(`Failed to display data for ${coin}:`, error.message);
+ updateUI(true);
+ }
+ },
showLoader: () => {
const loader = document.getElementById('loader');
@@ -762,9 +487,13 @@ displayCoinData: (coin, data) => {
updatePriceChangeContainer: (coin, priceChange) => {
const container = document.querySelector(`#${coin.toLowerCase()}-price-change-container`);
if (container) {
- container.innerHTML = priceChange !== null ?
- (priceChange >= 0 ? ui.positivePriceChangeHTML(priceChange) : ui.negativePriceChangeHTML(priceChange)) :
- 'N/A';
+ if (priceChange === null || priceChange === undefined) {
+ container.innerHTML = 'N/A';
+ } else {
+ container.innerHTML = priceChange >= 0 ?
+ ui.positivePriceChangeHTML(priceChange) :
+ ui.negativePriceChangeHTML(priceChange);
+ }
}
},
@@ -775,6 +504,16 @@ displayCoinData: (coin, data) => {
lastRefreshedElement.textContent = `Last Refreshed: ${formattedTime}`;
}
},
+
+ updateConnectionStatus: () => {
+ const statusElement = document.getElementById('connection-status');
+ if (statusElement) {
+ const online = NetworkManager.isOnline();
+ statusElement.textContent = online ? 'Connected' : 'Disconnected';
+ statusElement.classList.toggle('text-green-500', online);
+ statusElement.classList.toggle('text-red-500', !online);
+ }
+ },
positivePriceChangeHTML: (value) => `
@@ -819,7 +558,7 @@ displayCoinData: (coin, data) => {
});
},
- displayErrorMessage: (message) => {
+ displayErrorMessage: (message, duration = 0) => {
const errorOverlay = document.getElementById('error-overlay');
const errorMessage = document.getElementById('error-message');
const chartContainer = document.querySelector('.container-to-blur');
@@ -827,6 +566,12 @@ displayCoinData: (coin, data) => {
errorOverlay.classList.remove('hidden');
errorMessage.textContent = message;
chartContainer.classList.add('blurred');
+
+ if (duration > 0) {
+ setTimeout(() => {
+ ui.hideErrorMessage();
+ }, duration);
+ }
}
},
@@ -837,16 +582,42 @@ displayCoinData: (coin, data) => {
errorOverlay.classList.add('hidden');
containersToBlur.forEach(container => container.classList.remove('blurred'));
}
+ },
+
+ showNetworkErrorMessage: () => {
+ ui.displayErrorMessage(
+ "Network connection lost. Data shown may be outdated. We'll automatically refresh once connection is restored.",
+ 0
+ );
+
+ const errorOverlay = document.getElementById('error-overlay');
+ if (errorOverlay) {
+ const reconnectBtn = document.createElement('button');
+ reconnectBtn.className = "mt-4 bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded";
+ reconnectBtn.textContent = "Try to Reconnect";
+ reconnectBtn.onclick = () => {
+ NetworkManager.manualReconnect();
+ };
+
+ const buttonContainer = errorOverlay.querySelector('.button-container') ||
+ document.createElement('div');
+ buttonContainer.className = "button-container mt-4";
+ buttonContainer.innerHTML = '';
+ buttonContainer.appendChild(reconnectBtn);
+
+ if (!errorOverlay.querySelector('.button-container')) {
+ errorOverlay.querySelector('div').appendChild(buttonContainer);
+ }
+ }
}
};
-// CHART
const chartModule = {
chart: null,
currentCoin: 'BTC',
loadStartTime: 0,
chartRefs: new WeakMap(),
-
+
verticalLinePlugin: {
id: 'verticalLine',
beforeDraw: (chart, args, options) => {
@@ -879,29 +650,54 @@ const chartModule = {
destroyChart: function() {
if (chartModule.chart) {
try {
+ const canvas = document.getElementById('coin-chart');
+ if (canvas) {
+ const events = ['click', 'mousemove', 'mouseout', 'mouseover', 'mousedown', 'mouseup'];
+ events.forEach(eventType => {
+ canvas.removeEventListener(eventType, null);
+ });
+ }
+
chartModule.chart.destroy();
+ chartModule.chart = null;
+
+ if (canvas) {
+ chartModule.chartRefs.delete(canvas);
+ }
} catch (e) {
- console.error('Error destroying chart:', e);
+ try {
+ if (chartModule.chart) {
+ if (chartModule.chart.destroy && typeof chartModule.chart.destroy === 'function') {
+ chartModule.chart.destroy();
+ }
+ chartModule.chart = null;
+ }
+ } catch (finalError) {}
}
- chartModule.chart = null;
}
},
initChart: function() {
this.destroyChart();
-
+
const canvas = document.getElementById('coin-chart');
if (!canvas) {
- logger.error('Chart canvas element not found');
+ console.error('Chart canvas element not found');
return;
}
-
+
+ canvas.style.display = 'block';
+ if (canvas.style.width === '1px' || canvas.style.height === '1px') {
+ canvas.style.width = '100%';
+ canvas.style.height = '100%';
+ }
+
const ctx = canvas.getContext('2d');
if (!ctx) {
- logger.error('Failed to get chart context. Make sure the canvas element exists.');
+ console.error('Failed to get chart context. Make sure the canvas element exists.');
return;
}
-
+
const gradient = ctx.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(77, 132, 240, 0.2)');
gradient.addColorStop(1, 'rgba(77, 132, 240, 0)');
@@ -923,6 +719,9 @@ const chartModule = {
options: {
responsive: true,
maintainAspectRatio: false,
+ animation: {
+ duration: 750
+ },
interaction: {
intersect: false,
mode: 'index'
@@ -945,7 +744,7 @@ const chartModule = {
}
},
ticks: {
- source: 'data',
+ source: 'auto',
maxTicksLimit: 12,
font: {
size: 12,
@@ -956,14 +755,14 @@ const chartModule = {
minRotation: 0,
callback: function(value) {
const date = new Date(value);
- if (config.currentResolution === 'day') {
+ if (window.config.currentResolution === 'day') {
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
timeZone: 'UTC'
});
- } else if (config.currentResolution === 'year') {
+ } else if (window.config.currentResolution === 'year') {
return date.toLocaleDateString('en-US', {
month: 'short',
year: 'numeric',
@@ -1015,7 +814,7 @@ const chartModule = {
callbacks: {
title: (tooltipItems) => {
const date = new Date(tooltipItems[0].parsed.x);
- if (config.currentResolution === 'day') {
+ if (window.config.currentResolution === 'day') {
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
@@ -1024,7 +823,7 @@ const chartModule = {
hour12: true,
timeZone: 'UTC'
});
- } else if (config.currentResolution === 'year') {
+ } else if (window.config.currentResolution === 'year') {
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'short',
@@ -1058,6 +857,12 @@ const chartModule = {
});
this.setChartReference(canvas, chartModule.chart);
+
+ if (window.CleanupManager) {
+ window.CleanupManager.registerResource('chart', chartModule.chart, () => {
+ chartModule.destroyChart();
+ });
+ }
},
prepareChartData: function(coinSymbol, data) {
@@ -1066,68 +871,85 @@ const chartModule = {
}
try {
- let preparedData;
-
- if (coinSymbol === 'WOW' && Array.isArray(data)) {
- const endTime = new Date(data[data.length - 1][0]);
- endTime.setUTCMinutes(0, 0, 0);
- const endUnix = endTime.getTime();
- const startUnix = endUnix - (24 * 3600000);
- const hourlyPoints = [];
-
- for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) {
- const targetHour = new Date(hourUnix);
- targetHour.setUTCMinutes(0, 0, 0);
-
- const closestPoint = data.reduce((prev, curr) => {
- const prevTime = new Date(prev[0]);
- const currTime = new Date(curr[0]);
- const prevDiff = Math.abs(prevTime - targetHour);
- const currDiff = Math.abs(currTime - targetHour);
- return currDiff < prevDiff ? curr : prev;
- });
-
- hourlyPoints.push({
- x: targetHour,
- y: closestPoint[1]
- });
- }
-
- const lastTime = new Date(data[data.length - 1][0]);
- if (lastTime.getUTCMinutes() !== 0) {
- hourlyPoints.push({
- x: lastTime,
- y: data[data.length - 1][1]
- });
- }
-
- preparedData = hourlyPoints;
+ let rawDataPoints = [];
+ if (Array.isArray(data)) {
+ rawDataPoints = data.map(([timestamp, price]) => ({
+ time: new Date(timestamp).getTime(),
+ close: price
+ }));
} else if (data.Data && Array.isArray(data.Data)) {
- preparedData = data.Data.map(d => ({
- x: new Date(d.time * 1000),
- y: d.close
+ rawDataPoints = data.Data.map(d => ({
+ time: d.time * 1000,
+ close: d.close
}));
} else if (data.Data && data.Data.Data && Array.isArray(data.Data.Data)) {
- preparedData = data.Data.Data.map(d => ({
- x: new Date(d.time * 1000),
- y: d.close
- }));
- } else if (Array.isArray(data)) {
- preparedData = data.map(([timestamp, price]) => ({
- x: new Date(timestamp),
- y: price
+ rawDataPoints = data.Data.Data.map(d => ({
+ time: d.time * 1000,
+ close: d.close
}));
} else {
- console.warn('Unknown data format for chartData:', data);
return [];
}
- return preparedData.map(point => ({
- x: new Date(point.x).getTime(),
- y: point.y
- }));
+
+ if (rawDataPoints.length === 0) {
+ return [];
+ }
+
+ rawDataPoints.sort((a, b) => a.time - b.time);
+
+ let preparedData = [];
+
+ if (window.config.currentResolution === 'day') {
+ const endTime = new Date(rawDataPoints[rawDataPoints.length - 1].time);
+ endTime.setUTCMinutes(0, 0, 0);
+
+ const endUnix = endTime.getTime();
+ const startUnix = endUnix - (24 * 3600000);
+
+ for (let hourUnix = startUnix; hourUnix <= endUnix; hourUnix += 3600000) {
+ let closestPoint = null;
+ let closestDiff = Infinity;
+
+ for (const point of rawDataPoints) {
+ const diff = Math.abs(point.time - hourUnix);
+ if (diff < closestDiff) {
+ closestDiff = diff;
+ closestPoint = point;
+ }
+ }
+
+ if (closestPoint) {
+ preparedData.push({
+ x: hourUnix,
+ y: closestPoint.close
+ });
+ }
+ }
+
+ const lastTime = rawDataPoints[rawDataPoints.length - 1].time;
+ if (lastTime > endUnix) {
+ preparedData.push({
+ x: lastTime,
+ y: rawDataPoints[rawDataPoints.length - 1].close
+ });
+ }
+ } else {
+ preparedData = rawDataPoints.map(point => ({
+ x: point.time,
+ y: point.close
+ }));
+ }
+
+ if (preparedData.length === 0 && rawDataPoints.length > 0) {
+ preparedData = rawDataPoints.map(point => ({
+ x: point.time,
+ y: point.close
+ }));
+ }
+
+ return preparedData;
} catch (error) {
- console.error(`Error preparing chart data for ${coinSymbol}:`, error);
return [];
}
},
@@ -1142,10 +964,10 @@ const chartModule = {
const targetTime = new Date(twentyFourHoursAgo.getTime() + i * 60 * 60 * 1000);
if (data.length > 0) {
- const closestDataPoint = data.reduce((prev, curr) =>
- Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) <
- Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev
-);
+ const closestDataPoint = data.reduce((prev, curr) =>
+ Math.abs(new Date(curr.x).getTime() - targetTime.getTime()) <
+ Math.abs(new Date(prev.x).getTime() - targetTime.getTime()) ? curr : prev
+ , data[0]);
hourlyData.push({
x: targetTime.getTime(),
y: closestDataPoint.y
@@ -1166,13 +988,17 @@ const chartModule = {
chartModule.showChartLoader();
}
chartModule.loadStartTime = Date.now();
- const cacheKey = `chartData_${coinSymbol}_${config.currentResolution}`;
- let cachedData = !forceRefresh ? cache.get(cacheKey) : null;
+ const cacheKey = `chartData_${coinSymbol}_${window.config.currentResolution}`;
+ let cachedData = !forceRefresh ? CacheManager.get(cacheKey) : null;
let data;
if (cachedData && Object.keys(cachedData.value).length > 0) {
data = cachedData.value;
} else {
try {
+ if (!NetworkManager.isOnline()) {
+ throw new Error('Network is offline');
+ }
+
const allData = await api.fetchHistoricalDataXHR([coinSymbol]);
data = allData[coinSymbol];
@@ -1180,8 +1006,10 @@ const chartModule = {
throw new Error(`No data returned for ${coinSymbol}`);
}
- cache.set(cacheKey, data, config.cacheTTL);
+ CacheManager.set(cacheKey, data, 'chart');
} catch (error) {
+ NetworkManager.handleNetworkError(error);
+
if (error.message.includes('429') && currentChartData.length > 0) {
console.warn(`Rate limit hit for ${coinSymbol}, maintaining current chart`);
chartModule.hideChartLoader();
@@ -1212,10 +1040,10 @@ const chartModule = {
if (coinSymbol === 'WOW') {
chartModule.chart.options.scales.x.time.unit = 'hour';
} else {
- const resolution = config.resolutions[config.currentResolution];
+ const resolution = window.config.chartConfig.resolutions[window.config.currentResolution];
chartModule.chart.options.scales.x.time.unit =
- resolution.interval === 'hourly' ? 'hour' :
- config.currentResolution === 'year' ? 'month' : 'day';
+ resolution && resolution.interval === 'hourly' ? 'hour' :
+ window.config.currentResolution === 'year' ? 'month' : 'day';
}
chartModule.chart.update('active');
chartModule.currentCoin = coinSymbol;
@@ -1224,8 +1052,7 @@ const chartModule = {
}
} catch (error) {
console.error(`Error updating chart for ${coinSymbol}:`, error);
-
- // Keep existing chart data if possible /todo
+
if (!(chartModule.chart?.data?.datasets[0]?.data?.length > 0)) {
if (!chartModule.chart) {
chartModule.initChart();
@@ -1259,11 +1086,12 @@ const chartModule = {
loader.classList.add('hidden');
chart.classList.remove('hidden');
},
+
cleanup: function() {
this.destroyChart();
this.currentCoin = null;
this.loadStartTime = 0;
- console.log('Chart module cleaned up');
+ this.chartRefs = new WeakMap();
}
};
@@ -1274,8 +1102,8 @@ const volumeToggle = {
init: function() {
const toggleButton = document.getElementById('toggle-volume');
if (toggleButton) {
- if (typeof cleanupManager !== 'undefined') {
- cleanupManager.addListener(toggleButton, 'click', volumeToggle.toggle);
+ if (typeof CleanupManager !== 'undefined') {
+ CleanupManager.addListener(toggleButton, 'click', volumeToggle.toggle);
} else {
toggleButton.addEventListener('click', volumeToggle.toggle);
}
@@ -1311,12 +1139,12 @@ const volumeToggle = {
}
};
- function updateButtonStyles(button, isActive, color) {
- button.classList.toggle('text-' + color + '-500', isActive);
- button.classList.toggle('text-gray-600', !isActive);
- button.classList.toggle('dark:text-' + color + '-400', isActive);
- button.classList.toggle('dark:text-gray-400', !isActive);
- }
+function updateButtonStyles(button, isActive, color) {
+ button.classList.toggle('text-' + color + '-500', isActive);
+ button.classList.toggle('text-gray-600', !isActive);
+ button.classList.toggle('dark:text-' + color + '-400', isActive);
+ button.classList.toggle('dark:text-gray-400', !isActive);
+}
const app = {
btcPriceUSD: 0,
@@ -1330,92 +1158,122 @@ const app = {
disabled: 'Auto-refresh: disabled',
justRefreshed: 'Just refreshed',
},
- cacheTTL: 5 * 60 * 1000, // 5 min
- minimumRefreshInterval: 60 * 1000, // 1 min
+ cacheTTL: window.config.cacheConfig.ttlSettings.prices,
+ minimumRefreshInterval: 60 * 1000,
- init: () => {
- console.log('Init');
+ init: function() {
window.addEventListener('load', app.onLoad);
app.loadLastRefreshedTime();
app.updateAutoRefreshButton();
- //console.log('App initialized');
+
+ NetworkManager.addHandler('offline', () => {
+ ui.showNetworkErrorMessage();
+ });
+
+ NetworkManager.addHandler('reconnected', () => {
+ ui.hideErrorMessage();
+ app.refreshAllData();
+ });
+
+ NetworkManager.addHandler('maxAttemptsReached', () => {
+ ui.displayErrorMessage(
+ "Server connection lost. Please check your internet connection and try refreshing the page.",
+ 0
+ );
+ });
+
+ return app;
},
- onLoad: async () => {
- //console.log('App onLoad event triggered');
+ onLoad: async function() {
ui.showLoader();
try {
- volumeToggle.init();
- await app.updateBTCPrice();
- const chartContainer = document.getElementById('coin-chart');
- if (chartContainer) {
- chartModule.initChart();
- chartModule.showChartLoader();
+ volumeToggle.init();
+ await app.updateBTCPrice();
+ const chartContainer = document.getElementById('coin-chart');
+ if (chartContainer) {
+ chartModule.initChart();
+ chartModule.showChartLoader();
+ }
+
+ await app.loadAllCoinData();
+
+ if (chartModule.chart) {
+ window.config.currentResolution = 'day';
+ await chartModule.updateChart('BTC');
+ app.updateResolutionButtons('BTC');
+
+ const chartTitle = document.getElementById('chart-title');
+ if (chartTitle) {
+ chartTitle.textContent = 'Price Chart (BTC)';
}
+ }
+ ui.setActiveContainer('btc-container');
- //console.log('Loading all coin data...');
- await app.loadAllCoinData();
-
- if (chartModule.chart) {
- config.currentResolution = 'day';
- await chartModule.updateChart('BTC');
- app.updateResolutionButtons('BTC');
-
- const chartTitle = document.getElementById('chart-title');
- if (chartTitle) {
- chartTitle.textContent = 'Price Chart (BTC)';
- }
- }
- ui.setActiveContainer('btc-container');
-
- app.setupEventListeners();
- app.initializeSelectImages();
- app.initAutoRefresh();
+ app.setupEventListeners();
+ app.initAutoRefresh();
} catch (error) {
- ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.');
+ ui.displayErrorMessage('Failed to initialize the dashboard. Please try refreshing the page.');
+ NetworkManager.handleNetworkError(error);
} finally {
- ui.hideLoader();
- if (chartModule.chart) {
- chartModule.hideChartLoader();
- }
- //console.log('App onLoad completed');
+ ui.hideLoader();
+ if (chartModule.chart) {
+ chartModule.hideChartLoader();
+ }
}
-},
- loadAllCoinData: async () => {
- //console.log('Loading data for all coins...');
- try {
- const allCoinData = await api.fetchCoinGeckoDataXHR();
- if (allCoinData.error) {
- throw new Error(allCoinData.error);
- }
+ },
+
+ loadAllCoinData: async function() {
+ try {
+ if (!NetworkManager.isOnline()) {
+ throw new Error('Network is offline');
+ }
+
+ const allCoinData = await api.fetchCoinGeckoDataXHR();
+ if (allCoinData.error) {
+ throw new Error(allCoinData.error);
+ }
- for (const coin of config.coins) {
- const coinData = allCoinData[coin.symbol.toLowerCase()];
- if (coinData) {
- coinData.displayName = coin.displayName || coin.symbol;
- ui.displayCoinData(coin.symbol, coinData);
- const cacheKey = `coinData_${coin.symbol}`;
- cache.set(cacheKey, coinData);
- } else {
- //console.warn(`No data found for ${coin.symbol}`);
- }
+ let volumeData = {};
+ try {
+ volumeData = await api.fetchVolumeDataXHR();
+ } catch (volumeError) {}
+
+ for (const coin of window.config.coins) {
+ const coinData = allCoinData[coin.symbol.toLowerCase()];
+
+ 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;
}
- } catch (error) {
- //console.error('Error loading all coin data:', error);
- ui.displayErrorMessage('Failed to load coin data. Please try refreshing the page.');
- } finally {
- //console.log('All coin data loaded');
+ }
+
+ ui.displayCoinData(coin.symbol, coinData);
+
+ const cacheKey = `coinData_${coin.symbol}`;
+ CacheManager.set(cacheKey, coinData);
+ } else {
+ console.warn(`No data found for ${coin.symbol}`);
}
- },
+ }
+ } catch (error) {
+ console.error('Error loading all coin data:', error);
+ NetworkManager.handleNetworkError(error);
+ ui.displayErrorMessage('Failed to load coin data. Please try refreshing the page.');
+ }
+ },
- loadCoinData: async (coin) => {
- //console.log(`Loading data for ${coin.symbol}...`);
+ loadCoinData: async function(coin) {
const cacheKey = `coinData_${coin.symbol}`;
- let cachedData = cache.get(cacheKey);
+ let cachedData = CacheManager.get(cacheKey);
let data;
if (cachedData) {
- //console.log(`Using cached data for ${coin.symbol}`);
data = cachedData.value;
} else {
try {
@@ -1428,11 +1286,10 @@ const app = {
if (data.error) {
throw new Error(data.error);
}
- //console.log(`Caching new data for ${coin.symbol}`);
- cache.set(cacheKey, data);
+ CacheManager.set(cacheKey, data, 'prices');
cachedData = null;
} catch (error) {
- //console.error(`Error fetching ${coin.symbol} data:`, error.message);
+ NetworkManager.handleNetworkError(error);
data = {
error: error.message
};
@@ -1442,49 +1299,51 @@ const app = {
}
ui.displayCoinData(coin.symbol, data);
ui.updateLoadTimeAndCache(0, cachedData);
- //console.log(`Data loaded for ${coin.symbol}`);
},
-setupEventListeners: () => {
- config.coins.forEach(coin => {
- const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
- if (container) {
- container.addEventListener('click', () => {
- const chartTitle = document.getElementById('chart-title');
- if (chartTitle) {
- chartTitle.textContent = `Price Chart (${coin.symbol})`;
- }
- ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
- if (chartModule.chart) {
- if (coin.symbol === 'WOW') {
- config.currentResolution = 'day';
- }
- chartModule.updateChart(coin.symbol);
- app.updateResolutionButtons(coin.symbol);
- }
- });
- }
+ setupEventListeners: function() {
+ window.config.coins.forEach(coin => {
+ const container = document.getElementById(`${coin.symbol.toLowerCase()}-container`);
+ if (container) {
+ CleanupManager.addListener(container, 'click', () => {
+ const chartTitle = document.getElementById('chart-title');
+ if (chartTitle) {
+ chartTitle.textContent = `Price Chart (${coin.symbol})`;
+ }
+ ui.setActiveContainer(`${coin.symbol.toLowerCase()}-container`);
+ if (chartModule.chart) {
+ if (coin.symbol === 'WOW') {
+ window.config.currentResolution = 'day';
+ }
+ chartModule.updateChart(coin.symbol);
+ app.updateResolutionButtons(coin.symbol);
+ }
+ });
+ }
});
const refreshAllButton = document.getElementById('refresh-all');
if (refreshAllButton) {
- refreshAllButton.addEventListener('click', app.refreshAllData);
+ CleanupManager.addListener(refreshAllButton, 'click', app.refreshAllData);
}
const headers = document.querySelectorAll('th');
headers.forEach((header, index) => {
- header.addEventListener('click', () => app.sortTable(index, header.classList.contains('disabled')));
+ CleanupManager.addListener(header, 'click', () => app.sortTable(index, header.classList.contains('disabled')));
});
const closeErrorButton = document.getElementById('close-error');
if (closeErrorButton) {
- closeErrorButton.addEventListener('click', ui.hideErrorMessage);
+ CleanupManager.addListener(closeErrorButton, 'click', ui.hideErrorMessage);
+ }
+
+ const reconnectButton = document.getElementById('network-reconnect');
+ if (reconnectButton) {
+ CleanupManager.addListener(reconnectButton, 'click', NetworkManager.manualReconnect);
}
- //console.log('Event listeners set up');
},
- initAutoRefresh: () => {
- //console.log('Initializing auto-refresh...');
+ initAutoRefresh: function() {
const toggleAutoRefreshButton = document.getElementById('toggle-auto-refresh');
if (toggleAutoRefreshButton) {
toggleAutoRefreshButton.addEventListener('click', app.toggleAutoRefresh);
@@ -1492,15 +1351,11 @@ setupEventListeners: () => {
}
if (app.isAutoRefreshEnabled) {
- console.log('Auto-refresh is enabled, scheduling next refresh');
app.scheduleNextRefresh();
- } else {
- console.log('Auto-refresh is disabled');
}
},
- scheduleNextRefresh: () => {
- //console.log('Scheduling next refresh...');
+ scheduleNextRefresh: function() {
if (app.autoRefreshInterval) {
clearTimeout(app.autoRefreshInterval);
}
@@ -1516,7 +1371,6 @@ setupEventListeners: () => {
earliestExpiration = Math.min(earliestExpiration, cachedItem.expiresAt);
}
} catch (error) {
- //console.error(`Error parsing cached item ${key}:`, error);
localStorage.removeItem(key);
}
}
@@ -1526,22 +1380,30 @@ setupEventListeners: () => {
if (earliestExpiration !== Infinity) {
nextRefreshTime = Math.max(earliestExpiration, now + app.minimumRefreshInterval);
} else {
- nextRefreshTime = now + config.cacheTTL;
+ nextRefreshTime = now + window.config.cacheTTL;
}
const timeUntilRefresh = nextRefreshTime - now;
- console.log(`Next refresh scheduled in ${timeUntilRefresh / 1000} seconds`);
app.nextRefreshTime = nextRefreshTime;
app.autoRefreshInterval = setTimeout(() => {
- console.log('Auto-refresh triggered');
- app.refreshAllData();
+ if (NetworkManager.isOnline()) {
+ app.refreshAllData();
+ } else {
+ app.scheduleNextRefresh();
+ }
}, timeUntilRefresh);
localStorage.setItem('nextRefreshTime', app.nextRefreshTime.toString());
app.updateNextRefreshTime();
},
- refreshAllData: async () => {
+
+ refreshAllData: async function() {
if (app.isRefreshing) {
- console.log('Refresh already in progress, skipping...');
- return;
+ console.log('Refresh already in progress, skipping...');
+ return;
+ }
+
+ if (!NetworkManager.isOnline()) {
+ ui.displayErrorMessage("Network connection unavailable. Please check your connection.");
+ return;
}
const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0;
@@ -1549,131 +1411,159 @@ setupEventListeners: () => {
const waitTime = Math.max(0, rateLimiter.minRequestInterval.coingecko - timeSinceLastRequest);
if (waitTime > 0) {
- const seconds = Math.ceil(waitTime / 1000);
- ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
+ const seconds = Math.ceil(waitTime / 1000);
+ ui.displayErrorMessage(`Rate limit: Please wait ${seconds} seconds before refreshing`);
- let remainingTime = seconds;
- const countdownInterval = setInterval(() => {
- remainingTime--;
- if (remainingTime > 0) {
- ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
- } else {
- clearInterval(countdownInterval);
- ui.hideErrorMessage();
- }
- }, 1000);
+ let remainingTime = seconds;
+ const countdownInterval = setInterval(() => {
+ remainingTime--;
+ if (remainingTime > 0) {
+ ui.displayErrorMessage(`Rate limit: Please wait ${remainingTime} seconds before refreshing`);
+ } else {
+ clearInterval(countdownInterval);
+ ui.hideErrorMessage();
+ }
+ }, 1000);
- return;
+ return;
}
- //console.log('Starting refresh of all data...');
+ console.log('Starting refresh of all data...');
app.isRefreshing = true;
ui.showLoader();
chartModule.showChartLoader();
try {
- ui.hideErrorMessage();
- cache.clear();
+ ui.hideErrorMessage();
+ CacheManager.clear();
- const btcUpdateSuccess = await app.updateBTCPrice();
- if (!btcUpdateSuccess) {
- console.warn('BTC price update failed, continuing with cached or default value');
- }
+ const btcUpdateSuccess = await app.updateBTCPrice();
+ if (!btcUpdateSuccess) {
+ console.warn('BTC price update failed, continuing with cached or default value');
+ }
- await new Promise(resolve => setTimeout(resolve, 1000));
+ await new Promise(resolve => setTimeout(resolve, 1000));
- const allCoinData = await api.fetchCoinGeckoDataXHR();
- if (allCoinData.error) {
- throw new Error(`CoinGecko API Error: ${allCoinData.error}`);
- }
+ const allCoinData = await api.fetchCoinGeckoDataXHR();
+ if (allCoinData.error) {
+ throw new Error(`CoinGecko API Error: ${allCoinData.error}`);
+ }
- const failedCoins = [];
+ let volumeData = {};
+ try {
+ volumeData = await api.fetchVolumeDataXHR();
+ } catch (volumeError) {}
- for (const coin of config.coins) {
- const symbol = coin.symbol.toLowerCase();
- const coinData = allCoinData[symbol];
+ const failedCoins = [];
- try {
- if (!coinData) {
- throw new Error(`No data received`);
- }
+ for (const coin of window.config.coins) {
+ const symbol = coin.symbol.toLowerCase();
+ const coinData = allCoinData[symbol];
- coinData.displayName = coin.displayName || coin.symbol;
- ui.displayCoinData(coin.symbol, coinData);
+ try {
+ if (!coinData) {
+ throw new Error(`No data received`);
+ }
- const cacheKey = `coinData_${coin.symbol}`;
- cache.set(cacheKey, coinData);
+ coinData.displayName = coin.displayName || coin.symbol;
- } catch (coinError) {
- console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
- failedCoins.push(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;
}
- }
-
- await new Promise(resolve => setTimeout(resolve, 1000));
-
- if (chartModule.currentCoin) {
+ } else {
try {
- await chartModule.updateChart(chartModule.currentCoin, true);
- } catch (chartError) {
- console.error('Chart update failed:', chartError);
+ const cacheKey = `coinData_${coin.symbol}`;
+ const cachedData = CacheManager.get(cacheKey);
+ if (cachedData && cachedData.value && cachedData.value.total_volume) {
+ coinData.total_volume = cachedData.value.total_volume;
+ }
+ if (cachedData && cachedData.value && cachedData.value.price_change_percentage_24h &&
+ !coinData.price_change_percentage_24h) {
+ coinData.price_change_percentage_24h = cachedData.value.price_change_percentage_24h;
+ }
+ } catch (e) {
+ console.warn(`Failed to retrieve cached volume data for ${coin.symbol}:`, e);
}
+ }
+
+ ui.displayCoinData(coin.symbol, coinData);
+
+ const cacheKey = `coinData_${coin.symbol}`;
+ CacheManager.set(cacheKey, coinData, 'prices');
+
+ } catch (coinError) {
+ console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
+ failedCoins.push(coin.symbol);
}
+ }
- app.lastRefreshedTime = new Date();
- localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
- ui.updateLastRefreshedTime();
+ await new Promise(resolve => setTimeout(resolve, 1000));
- if (failedCoins.length > 0) {
- const failureMessage = failedCoins.length === config.coins.length
- ? 'Failed to update any coin data'
- : `Failed to update some coins: ${failedCoins.join(', ')}`;
+ if (chartModule.currentCoin) {
+ try {
+ await chartModule.updateChart(chartModule.currentCoin, true);
+ } catch (chartError) {
+ console.error('Chart update failed:', chartError);
+ }
+ }
- let countdown = 5;
+ app.lastRefreshedTime = new Date();
+ localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString());
+ ui.updateLastRefreshedTime();
+
+ if (failedCoins.length > 0) {
+ const failureMessage = failedCoins.length === window.config.coins.length
+ ? 'Failed to update any coin data'
+ : `Failed to update some coins: ${failedCoins.join(', ')}`;
+
+ let countdown = 5;
+ ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
+
+ const countdownInterval = setInterval(() => {
+ countdown--;
+ if (countdown > 0) {
ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
+ } else {
+ clearInterval(countdownInterval);
+ ui.hideErrorMessage();
+ }
+ }, 1000);
+ }
- const countdownInterval = setInterval(() => {
- countdown--;
- if (countdown > 0) {
- ui.displayErrorMessage(`${failureMessage} (${countdown}s)`);
- } else {
- clearInterval(countdownInterval);
- ui.hideErrorMessage();
- }
- }, 1000);
- }
-
- console.log(`Refresh completed. Failed coins: ${failedCoins.length}`);
+ console.log(`Refresh completed. Failed coins: ${failedCoins.length}`);
} catch (error) {
- console.error('Critical error during refresh:', error);
+ console.error('Critical error during refresh:', error);
+ NetworkManager.handleNetworkError(error);
- let countdown = 10;
- ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
-
- const countdownInterval = setInterval(() => {
- countdown--;
- if (countdown > 0) {
- ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
- } else {
- clearInterval(countdownInterval);
- ui.hideErrorMessage();
- }
- }, 1000);
+ let countdown = 10;
+ ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
+
+ const countdownInterval = setInterval(() => {
+ countdown--;
+ if (countdown > 0) {
+ ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`);
+ } else {
+ clearInterval(countdownInterval);
+ ui.hideErrorMessage();
+ }
+ }, 1000);
} finally {
- ui.hideLoader();
- chartModule.hideChartLoader();
- app.isRefreshing = false;
+ ui.hideLoader();
+ chartModule.hideChartLoader();
+ app.isRefreshing = false;
- if (app.isAutoRefreshEnabled) {
- app.scheduleNextRefresh();
- }
+ if (app.isAutoRefreshEnabled) {
+ app.scheduleNextRefresh();
+ }
}
-},
+ },
- updateNextRefreshTime: () => {
- //console.log('Updating next refresh time display');
+ updateNextRefreshTime: function() {
const nextRefreshSpan = document.getElementById('next-refresh-time');
const labelElement = document.getElementById('next-refresh-label');
const valueElement = document.getElementById('next-refresh-value');
@@ -1708,8 +1598,7 @@ setupEventListeners: () => {
}
},
- updateAutoRefreshButton: () => {
- //console.log('Updating auto-refresh button state');
+ updateAutoRefreshButton: function() {
const button = document.getElementById('toggle-auto-refresh');
if (button) {
if (app.isAutoRefreshEnabled) {
@@ -1725,8 +1614,7 @@ setupEventListeners: () => {
}
},
- startSpinAnimation: () => {
- //console.log('Starting spin animation on auto-refresh button');
+ startSpinAnimation: function() {
const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) {
svg.classList.add('animate-spin');
@@ -1736,16 +1624,14 @@ setupEventListeners: () => {
}
},
- stopSpinAnimation: () => {
- //console.log('Stopping spin animation on auto-refresh button');
+ stopSpinAnimation: function() {
const svg = document.querySelector('#toggle-auto-refresh svg');
if (svg) {
svg.classList.remove('animate-spin');
}
},
- updateLastRefreshedTime: () => {
- //console.log('Updating last refreshed time');
+ updateLastRefreshedTime: function() {
const lastRefreshedElement = document.getElementById('last-refreshed-time');
if (lastRefreshedElement && app.lastRefreshedTime) {
const formattedTime = app.lastRefreshedTime.toLocaleTimeString();
@@ -1753,8 +1639,7 @@ setupEventListeners: () => {
}
},
- loadLastRefreshedTime: () => {
- //console.log('Loading last refreshed time from storage');
+ loadLastRefreshedTime: function() {
const storedTime = localStorage.getItem('lastRefreshedTime');
if (storedTime) {
app.lastRefreshedTime = new Date(parseInt(storedTime));
@@ -1762,199 +1647,57 @@ setupEventListeners: () => {
}
},
-updateBTCPrice: async () => {
- //console.log('Updating BTC price...');
+ updateBTCPrice: async function() {
try {
- const priceData = await api.fetchCoinGeckoDataXHR();
+ if (!NetworkManager.isOnline()) {
+ throw new Error('Network is offline');
+ }
+
+ const response = await Api.fetchCoinPrices("bitcoin");
- if (priceData.error) {
- console.warn('API error when fetching BTC price:', priceData.error);
- return false;
- }
+ if (response && response.rates && response.rates.bitcoin) {
+ app.btcPriceUSD = response.rates.bitcoin;
+ return true;
+ }
- if (priceData.btc && typeof priceData.btc.current_price === 'number') {
- app.btcPriceUSD = priceData.btc.current_price;
- return true;
- } else if (priceData.bitcoin && typeof priceData.bitcoin.usd === 'number') {
- app.btcPriceUSD = priceData.bitcoin.usd;
- return true;
- }
-
- console.warn('Unexpected BTC price data structure:', priceData);
- return false;
+ console.warn('Unexpected BTC price data structure:', response);
+ return false;
} catch (error) {
- console.error('Error fetching BTC price:', error);
- return false;
+ console.error('Error fetching BTC price:', error);
+ NetworkManager.handleNetworkError(error);
+ return false;
}
-},
+ },
-sortTable: (columnIndex) => {
- //console.log(`Sorting column: ${columnIndex}`);
- const sortableColumns = [0, 5, 6, 7]; // 0: Time, 5: Rate, 6: Market +/-, 7: Trade
- if (!sortableColumns.includes(columnIndex)) {
- //console.log(`Column ${columnIndex} is not sortable`);
- return;
- }
- const table = document.querySelector('table');
- if (!table) {
- //console.error("Table not found for sorting.");
- return;
- }
- const rows = Array.from(table.querySelectorAll('tbody tr'));
- console.log(`Found ${rows.length} rows to sort`);
- const sortIcon = document.getElementById(`sort-icon-${columnIndex}`);
- if (!sortIcon) {
- //console.error("Sort icon not found.");
- return;
- }
- const sortOrder = sortIcon.textContent === '↓' ? 1 : -1;
- sortIcon.textContent = sortOrder === 1 ? '↑' : '↓';
-
- const getSafeTextContent = (element) => element ? element.textContent.trim() : '';
-
- rows.sort((a, b) => {
- let aValue, bValue;
- switch (columnIndex) {
- case 1: // Time column
- aValue = getSafeTextContent(a.querySelector('td:first-child .text-xs:first-child'));
- bValue = getSafeTextContent(b.querySelector('td:first-child .text-xs:first-child'));
- //console.log(`Comparing times: "${aValue}" vs "${bValue}"`);
-
- const parseTime = (timeStr) => {
- const [value, unit] = timeStr.split(' ');
- const numValue = parseFloat(value);
- switch(unit) {
- case 'seconds': return numValue;
- case 'minutes': return numValue * 60;
- case 'hours': return numValue * 3600;
- case 'days': return numValue * 86400;
- default: return 0;
- }
- };
- return (parseTime(bValue) - parseTime(aValue)) * sortOrder;
-
- case 5: // Rate
- case 6: // Market +/-
- aValue = getSafeTextContent(a.cells[columnIndex]);
- bValue = getSafeTextContent(b.cells[columnIndex]);
- //console.log(`Comparing values: "${aValue}" vs "${bValue}"`);
-
- aValue = parseFloat(aValue.replace(/[^\d.-]/g, '') || '0');
- bValue = parseFloat(bValue.replace(/[^\d.-]/g, '') || '0');
- return (aValue - bValue) * sortOrder;
-
- case 7: // Trade
- const aCell = a.cells[columnIndex];
- const bCell = b.cells[columnIndex];
- //console.log('aCell:', aCell ? aCell.outerHTML : 'null');
- //console.log('bCell:', bCell ? bCell.outerHTML : 'null');
-
- aValue = getSafeTextContent(aCell.querySelector('a')) ||
- getSafeTextContent(aCell.querySelector('button')) ||
- getSafeTextContent(aCell);
- bValue = getSafeTextContent(bCell.querySelector('a')) ||
- getSafeTextContent(bCell.querySelector('button')) ||
- getSafeTextContent(bCell);
-
- aValue = aValue.toLowerCase();
- bValue = bValue.toLowerCase();
-
- //console.log(`Comparing trade actions: "${aValue}" vs "${bValue}"`);
-
- if (aValue === bValue) return 0;
- if (aValue === "swap") return -1 * sortOrder;
- if (bValue === "swap") return 1 * sortOrder;
- return aValue.localeCompare(bValue) * sortOrder;
-
- default:
- aValue = getSafeTextContent(a.cells[columnIndex]);
- bValue = getSafeTextContent(b.cells[columnIndex]);
- //console.log(`Comparing default values: "${aValue}" vs "${bValue}"`);
- return aValue.localeCompare(bValue, undefined, {
- numeric: true,
- sensitivity: 'base'
- }) * sortOrder;
- }
- });
-
- const tbody = table.querySelector('tbody');
- if (tbody) {
- rows.forEach(row => tbody.appendChild(row));
- } else {
- //console.error("Table body not found.");
- }
- //console.log('Sorting completed');
-},
-
- initializeSelectImages: () => {
- const updateSelectedImage = (selectId) => {
- const select = document.getElementById(selectId);
- const button = document.getElementById(`${selectId}_button`);
- if (!select || !button) {
- //console.error(`Elements not found for ${selectId}`);
- return;
- }
- const selectedOption = select.options[select.selectedIndex];
- const imageURL = selectedOption?.getAttribute('data-image');
- requestAnimationFrame(() => {
- if (imageURL) {
- button.style.backgroundImage = `url('${imageURL}')`;
- button.style.backgroundSize = '25px 25px';
- button.style.backgroundPosition = 'center';
- button.style.backgroundRepeat = 'no-repeat';
+ updateResolutionButtons: function(coinSymbol) {
+ const resolutionButtons = document.querySelectorAll('.resolution-button');
+ resolutionButtons.forEach(button => {
+ const resolution = button.id.split('-')[1];
+ if (coinSymbol === 'WOW') {
+ if (resolution === 'day') {
+ button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
+ button.classList.add('active');
+ button.disabled = false;
} else {
- button.style.backgroundImage = 'none';
+ button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
+ button.classList.remove('active');
+ button.disabled = true;
}
- button.style.minWidth = '25px';
- button.style.minHeight = '25px';
- });
- };
- const handleSelectChange = (event) => {
- updateSelectedImage(event.target.id);
- };
- ['coin_to', 'coin_from'].forEach(selectId => {
- const select = document.getElementById(selectId);
- if (select) {
- select.addEventListener('change', handleSelectChange);
- updateSelectedImage(selectId);
} else {
- //console.error(`Select element not found for ${selectId}`);
+ button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
+ button.classList.toggle('active', resolution === window.config.currentResolution);
+ button.disabled = false;
}
});
},
-updateResolutionButtons: (coinSymbol) => {
- const resolutionButtons = document.querySelectorAll('.resolution-button');
- resolutionButtons.forEach(button => {
- const resolution = button.id.split('-')[1];
- if (coinSymbol === 'WOW') {
- if (resolution === 'day') {
- button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
- button.classList.add('active');
- button.disabled = false;
- } else {
- button.classList.add('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
- button.classList.remove('active');
- button.disabled = true;
- }
- } else {
- button.classList.remove('text-gray-400', 'cursor-not-allowed', 'opacity-50', 'outline-none');
- button.classList.toggle('active', resolution === config.currentResolution);
- button.disabled = false;
- }
- });
-},
-
- toggleAutoRefresh: () => {
- console.log('Toggling auto-refresh');
+ toggleAutoRefresh: function() {
app.isAutoRefreshEnabled = !app.isAutoRefreshEnabled;
localStorage.setItem('autoRefreshEnabled', app.isAutoRefreshEnabled.toString());
if (app.isAutoRefreshEnabled) {
- console.log('Auto-refresh enabled, scheduling next refresh');
app.scheduleNextRefresh();
} else {
- console.log('Auto-refresh disabled, clearing interval');
if (app.autoRefreshInterval) {
clearTimeout(app.autoRefreshInterval);
app.autoRefreshInterval = null;
@@ -1974,55 +1717,158 @@ resolutionButtons.forEach(button => {
const currentCoin = chartModule.currentCoin;
if (currentCoin !== 'WOW' || resolution === 'day') {
- config.currentResolution = resolution;
+ window.config.currentResolution = resolution;
chartModule.updateChart(currentCoin, true);
app.updateResolutionButtons(currentCoin);
}
});
});
-// LOAD
+function cleanup() {
+ console.log('Starting cleanup process');
+
+ try {
+ if (window.MemoryManager) {
+ MemoryManager.forceCleanup();
+ }
+
+ if (chartModule) {
+ CleanupManager.registerResource('chartModule', chartModule, (cm) => {
+ cm.cleanup();
+ });
+ }
+
+ if (volumeToggle) {
+ CleanupManager.registerResource('volumeToggle', volumeToggle, (vt) => {
+ vt.cleanup();
+ });
+ }
+
+ ['chartModule', 'volumeToggle', 'app'].forEach(ref => {
+ if (window[ref]) {
+ window[ref] = null;
+ }
+ });
+
+ const cleanupCounts = CleanupManager.clearAll();
+ console.log('All resources cleaned up:', cleanupCounts);
+
+ } catch (error) {
+ console.error('Error during cleanup:', error);
+ CleanupManager.clearAll();
+ }
+}
+
+window.cleanup = cleanup;
+
const appCleanup = {
init: function() {
- memoryMonitor.startMonitoring();
window.addEventListener('beforeunload', this.globalCleanup);
},
globalCleanup: function() {
try {
+ if (window.MemoryManager) {
+ MemoryManager.forceCleanup();
+ }
+
if (app.autoRefreshInterval) {
- clearTimeout(app.autoRefreshInterval);
+ CleanupManager.clearTimeout(app.autoRefreshInterval);
}
if (chartModule) {
- chartModule.cleanup();
+ CleanupManager.registerResource('chartModule', chartModule, (cm) => {
+ cm.cleanup();
+ });
}
if (volumeToggle) {
- volumeToggle.cleanup();
+ CleanupManager.registerResource('volumeToggle', volumeToggle, (vt) => {
+ vt.cleanup();
+ });
}
- cleanupManager.clearAll();
- memoryMonitor.stopMonitoring();
- cache.clear();
-
- console.log('Global application cleanup completed');
- } catch (error) {
- console.error('Error during global cleanup:', error);
- }
+ CleanupManager.clearAll();
+ CacheManager.clear();
+ } catch (error) {}
},
+
manualCleanup: function() {
this.globalCleanup();
window.location.reload();
}
};
-app.init = () => {
- //console.log('Init');
+document.addEventListener('DOMContentLoaded', () => {
+ if (window.NetworkManager && !window.networkManagerInitialized) {
+ NetworkManager.initialize({
+ connectionTestEndpoint: '/json',
+ connectionTestTimeout: 3000,
+ reconnectDelay: 5000,
+ maxReconnectAttempts: 5
+ });
+ window.networkManagerInitialized = true;
+ }
+
+ app.init();
+
+ if (window.MemoryManager) {
+ MemoryManager.enableAutoCleanup();
+ }
+
+ CleanupManager.setInterval(() => {
+ CacheManager.cleanup();
+ }, 300000); // Every 5 minutes
+
+ CleanupManager.setInterval(() => {
+ if (chartModule && chartModule.currentCoin && NetworkManager.isOnline()) {
+ chartModule.updateChart(chartModule.currentCoin);
+ }
+ }, 900000); // Every 15 minutes
+
+ CleanupManager.addListener(document, 'visibilitychange', () => {
+ if (!document.hidden) {
+ console.log('Page is now visible');
+
+ if (NetworkManager.isOnline()) {
+ if (chartModule && chartModule.currentCoin) {
+ chartModule.updateChart(chartModule.currentCoin);
+ }
+ } else {
+
+ NetworkManager.attemptReconnect();
+ }
+ }
+ });
+
+ CleanupManager.addListener(window, 'beforeunload', () => {
+ cleanup();
+ });
+
+ appCleanup.init();
+});
+
+app.init = function() {
window.addEventListener('load', app.onLoad);
- appCleanup.init();
app.loadLastRefreshedTime();
app.updateAutoRefreshButton();
- memoryMonitor.startMonitoring();
- //console.log('App initialized');
+
+ if (window.NetworkManager) {
+ NetworkManager.addHandler('offline', () => {
+ ui.showNetworkErrorMessage();
+ });
+
+ NetworkManager.addHandler('reconnected', () => {
+ ui.hideErrorMessage();
+ app.refreshAllData();
+ });
+
+ NetworkManager.addHandler('maxAttemptsReached', () => {
+ ui.displayErrorMessage(
+ "Server connection lost. Please check your internet connection and try refreshing the page.",
+ 0
+ );
+ });
+ }
+
+ return app;
};
-// LOAD
app.init();
diff --git a/basicswap/static/js/active.js b/basicswap/static/js/swaps_in_progress.js
similarity index 78%
rename from basicswap/static/js/active.js
rename to basicswap/static/js/swaps_in_progress.js
index 2189979..6efe3d7 100644
--- a/basicswap/static/js/active.js
+++ b/basicswap/static/js/swaps_in_progress.js
@@ -1,4 +1,3 @@
-// Constants and State
const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
@@ -16,7 +15,6 @@ const COIN_NAME_TO_SYMBOL = {
'Dogecoin': 'DOGE'
};
-// Global state
const state = {
identities: new Map(),
currentPage: 1,
@@ -27,7 +25,6 @@ const state = {
refreshPromise: null
};
-// DOM
const elements = {
swapsBody: document.getElementById('active-swaps-body'),
prevPageButton: document.getElementById('prevPage'),
@@ -40,105 +37,6 @@ const elements = {
statusText: document.getElementById('status-text')
};
-// Identity Manager
-const IdentityManager = {
- cache: new Map(),
- pendingRequests: new Map(),
- retryDelay: 2000,
- maxRetries: 3,
- cacheTimeout: 5 * 60 * 1000, // 5 minutes
-
- async getIdentityData(address) {
- if (!address) {
- return { address: '' };
- }
-
- const cachedData = this.getCachedIdentity(address);
- if (cachedData) {
- return { ...cachedData, address };
- }
-
- if (this.pendingRequests.has(address)) {
- const pendingData = await this.pendingRequests.get(address);
- return { ...pendingData, address };
- }
-
- const request = this.fetchWithRetry(address);
- this.pendingRequests.set(address, request);
-
- try {
- const data = await request;
- this.cache.set(address, {
- data,
- timestamp: Date.now()
- });
- return { ...data, address };
- } catch (error) {
- console.warn(`Error fetching identity for ${address}:`, error);
- return { address };
- } finally {
- this.pendingRequests.delete(address);
- }
- },
-
- getCachedIdentity(address) {
- const cached = this.cache.get(address);
- if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) {
- return cached.data;
- }
- if (cached) {
- this.cache.delete(address);
- }
- return null;
- },
-
- async fetchWithRetry(address, attempt = 1) {
- try {
- const response = await fetch(`/json/identities/${address}`, {
- signal: AbortSignal.timeout(5000)
- });
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data = await response.json();
- return {
- ...data,
- address,
- num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful),
- num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful),
- num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed),
- num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed),
- num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected),
- num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected),
- label: data.label || '',
- note: data.note || '',
- automation_override: safeParseInt(data.automation_override)
- };
- } catch (error) {
- if (attempt >= this.maxRetries) {
- console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`);
- return {
- address,
- num_sent_bids_successful: 0,
- num_recv_bids_successful: 0,
- num_sent_bids_failed: 0,
- num_recv_bids_failed: 0,
- num_sent_bids_rejected: 0,
- num_recv_bids_rejected: 0,
- label: '',
- note: '',
- automation_override: 0
- };
- }
-
- await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt));
- return this.fetchWithRetry(address, attempt + 1);
- }
- }
-};
-
const safeParseInt = (value) => {
const parsed = parseInt(value);
return isNaN(parsed) ? 0 : parsed;
@@ -200,7 +98,6 @@ const getTxStatusClass = (status) => {
return 'text-blue-500';
};
-// Util
const formatTimeAgo = (timestamp) => {
const now = Math.floor(Date.now() / 1000);
const diff = now - timestamp;
@@ -211,7 +108,6 @@ const formatTimeAgo = (timestamp) => {
return `${Math.floor(diff / 86400)} days ago`;
};
-
const formatTime = (timestamp) => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
@@ -251,111 +147,6 @@ const getTimeStrokeColor = (expireTime) => {
return '#10B981'; // More than 30 minutes
};
-// WebSocket Manager
-const WebSocketManager = {
- ws: null,
- processingQueue: false,
- reconnectTimeout: null,
- maxReconnectAttempts: 5,
- reconnectAttempts: 0,
- reconnectDelay: 5000,
-
- initialize() {
- this.connect();
- this.startHealthCheck();
- },
-
- connect() {
- if (this.ws?.readyState === WebSocket.OPEN) return;
-
- try {
-
- let wsPort;
-
- if (typeof getWebSocketConfig === 'function') {
- const wsConfig = getWebSocketConfig();
- wsPort = wsConfig?.port || wsConfig?.fallbackPort;
- }
-
- if (!wsPort && window.config?.port) {
- wsPort = window.config.port;
- }
-
- if (!wsPort) {
- wsPort = window.ws_port || '11700';
- }
-
- console.log("Using WebSocket port:", wsPort);
- this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`);
- this.setupEventHandlers();
- } catch (error) {
- console.error('WebSocket connection error:', error);
- this.handleReconnect();
- }
-},
- setupEventHandlers() {
- this.ws.onopen = () => {
- state.wsConnected = true;
- this.reconnectAttempts = 0;
- updateConnectionStatus('connected');
- console.log('🟢 WebSocket connection established for Swaps in Progress');
- updateSwapsTable({ resetPage: true, refreshData: true });
- };
-
- this.ws.onmessage = () => {
- if (!this.processingQueue) {
- this.processingQueue = true;
- setTimeout(async () => {
- try {
- if (!state.isRefreshing) {
- await updateSwapsTable({ resetPage: false, refreshData: true });
- }
- } finally {
- this.processingQueue = false;
- }
- }, 200);
- }
- };
-
- this.ws.onclose = () => {
- state.wsConnected = false;
- updateConnectionStatus('disconnected');
- this.handleReconnect();
- };
-
- this.ws.onerror = () => {
- updateConnectionStatus('error');
- };
- },
-
- startHealthCheck() {
- setInterval(() => {
- if (this.ws?.readyState !== WebSocket.OPEN) {
- this.handleReconnect();
- }
- }, 30000);
- },
-
- handleReconnect() {
- if (this.reconnectTimeout) {
- clearTimeout(this.reconnectTimeout);
- }
-
- this.reconnectAttempts++;
- if (this.reconnectAttempts <= this.maxReconnectAttempts) {
- const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
- this.reconnectTimeout = setTimeout(() => this.connect(), delay);
- } else {
- updateConnectionStatus('error');
- setTimeout(() => {
- this.reconnectAttempts = 0;
- this.connect();
- }, 60000);
- }
- }
-};
-
-// UI
const updateConnectionStatus = (status) => {
const { statusDot, statusText } = elements;
if (!statusDot || !statusText) return;
@@ -528,7 +319,7 @@ const createSwapTableRow = async (swap) => {
|
-
+
@@ -575,13 +366,14 @@ const createSwapTableRow = async (swap) => {
|
-
-
+
- ${toAmount.toFixed(8)}
- ${toSymbol}
+
+ ${fromAmount.toFixed(8)}
+ ${fromSymbol}
+
|
@@ -592,8 +384,8 @@ const createSwapTableRow = async (swap) => {
|
-
+
-
- ${fromAmount.toFixed(8)}
- ${fromSymbol}
-
+ ${toAmount.toFixed(8)}
+ ${toSymbol}
|
-
-
+
${swap.bid_state}
@@ -727,6 +516,8 @@ 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;
@@ -752,9 +543,19 @@ async function updateSwapsTable(options = {}) {
}
const data = await response.json();
- state.swapsData = Array.isArray(data) ? data : [];
+ //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);
+ //console.error('Error fetching swap data:', error);
state.swapsData = [];
} finally {
state.refreshPromise = null;
@@ -780,13 +581,14 @@ 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));
const rows = await Promise.all(rowPromises);
elements.swapsBody.innerHTML = rows.join('');
- // Initialize tooltips
if (window.TooltipManager) {
window.TooltipManager.cleanup();
const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]');
@@ -801,6 +603,7 @@ async function updateSwapsTable(options = {}) {
});
}
} else {
+ //console.log('No active swaps found, displaying empty state');
elements.swapsBody.innerHTML = `
|
@@ -810,22 +613,6 @@ async function updateSwapsTable(options = {}) {
}
}
- if (elements.paginationControls) {
- elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none';
- }
-
- if (elements.currentPageSpan) {
- elements.currentPageSpan.textContent = state.currentPage;
- }
-
- if (elements.prevPageButton) {
- elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none';
- }
-
- if (elements.nextPageButton) {
- elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none';
- }
-
} catch (error) {
console.error('Error updating swaps table:', error);
if (elements.swapsBody) {
@@ -841,7 +628,34 @@ async function updateSwapsTable(options = {}) {
}
}
-// Event
+function isActiveSwap(swap) {
+ const activeStates = [
+
+ 'InProgress',
+ 'Accepted',
+ 'Delaying',
+ 'Auto accept delay',
+ 'Request accepted',
+ //'Received',
+
+ 'Script coin locked',
+ 'Scriptless coin locked',
+ 'Script coin lock released',
+
+ 'SendingInitialTx',
+ 'SendingPaymentTx',
+
+ 'Exchanged script lock tx sigs msg',
+ 'Exchanged script lock spend tx msg',
+
+ 'Script tx redeemed',
+ 'Scriptless tx redeemed',
+ 'Scriptless tx recovered'
+ ];
+
+ return activeStates.includes(swap.bid_state);
+}
+
const setupEventListeners = () => {
if (elements.refreshSwapsButton) {
elements.refreshSwapsButton.addEventListener('click', async (e) => {
@@ -881,8 +695,11 @@ const setupEventListeners = () => {
}
};
-// Init
-document.addEventListener('DOMContentLoaded', () => {
+document.addEventListener('DOMContentLoaded', async () => {
WebSocketManager.initialize();
setupEventListeners();
+ await updateSwapsTable({ resetPage: true, refreshData: true });
+ const autoRefreshInterval = setInterval(async () => {
+ await updateSwapsTable({ resetPage: false, refreshData: true });
+ }, 10000); // 30 seconds
});
diff --git a/basicswap/static/js/tooltips.js b/basicswap/static/js/tooltips.js
deleted file mode 100644
index 9249c56..0000000
--- a/basicswap/static/js/tooltips.js
+++ /dev/null
@@ -1,387 +0,0 @@
-class TooltipManager {
- constructor() {
- this.activeTooltips = new WeakMap();
- this.sizeCheckIntervals = new WeakMap();
- this.tooltipIdCounter = 0;
- this.setupStyles();
- this.setupCleanupEvents();
- this.initializeMutationObserver();
- }
-
- static initialize() {
- if (!window.TooltipManager) {
- window.TooltipManager = new TooltipManager();
- }
- return window.TooltipManager;
- }
-
- create(element, content, options = {}) {
- if (!element) return null;
-
- this.destroy(element);
-
- const checkSize = () => {
- if (!document.body.contains(element)) {
- return;
- }
-
- const rect = element.getBoundingClientRect();
- if (rect.width && rect.height) {
- delete element._tooltipRetryCount;
- this.createTooltip(element, content, options, rect);
- } else {
- const retryCount = element._tooltipRetryCount || 0;
- if (retryCount < 5) {
- element._tooltipRetryCount = retryCount + 1;
- requestAnimationFrame(checkSize);
- } else {
- delete element._tooltipRetryCount;
- }
- }
- };
-
- requestAnimationFrame(checkSize);
- return null;
- }
-
- createTooltip(element, content, options, rect) {
- const targetId = element.getAttribute('data-tooltip-target');
- let bgClass = 'bg-gray-400';
- let arrowColor = 'rgb(156 163 175)';
-
- if (targetId?.includes('tooltip-offer-')) {
- const offerId = targetId.split('tooltip-offer-')[1];
- const [actualOfferId] = offerId.split('_');
-
- if (window.jsonData) {
- const offer = window.jsonData.find(o =>
- o.unique_id === offerId ||
- o.offer_id === actualOfferId
- );
-
- if (offer) {
- if (offer.is_revoked) {
- bgClass = 'bg-red-500';
- arrowColor = 'rgb(239 68 68)';
- } else if (offer.is_own_offer) {
- bgClass = 'bg-gray-300';
- arrowColor = 'rgb(209 213 219)';
- } else {
- bgClass = 'bg-green-700';
- arrowColor = 'rgb(21 128 61)';
- }
- }
- }
- }
-
- const tooltipId = `tooltip-${++this.tooltipIdCounter}`;
-
- const instance = tippy(element, {
- content,
- allowHTML: true,
- placement: options.placement || 'top',
- appendTo: document.body,
- animation: false,
- duration: 0,
- delay: 0,
- interactive: true,
- arrow: false,
- theme: '',
- moveTransition: 'none',
- offset: [0, 10],
- onShow(instance) {
- if (!document.body.contains(element)) {
- return false;
- }
-
- const rect = element.getBoundingClientRect();
- if (!rect.width || !rect.height) {
- return false;
- }
-
- return true;
- },
- onMount(instance) {
- if (instance.popper.firstElementChild) {
- instance.popper.firstElementChild.classList.add(bgClass);
- instance.popper.setAttribute('data-for-tooltip-id', tooltipId);
- }
- const arrow = instance.popper.querySelector('.tippy-arrow');
- if (arrow) {
- arrow.style.setProperty('color', arrowColor, 'important');
- }
- },
- popperOptions: {
- strategy: 'fixed',
- modifiers: [
- {
- name: 'preventOverflow',
- options: {
- boundary: 'viewport',
- padding: 10
- }
- },
- {
- name: 'flip',
- options: {
- padding: 10,
- fallbackPlacements: ['top', 'bottom', 'right', 'left']
- }
- }
- ]
- }
- });
-
- element.setAttribute('data-tooltip-trigger-id', tooltipId);
- this.activeTooltips.set(element, instance);
-
- return instance;
- }
-
- destroy(element) {
- if (!element) return;
-
- delete element._tooltipRetryCount;
-
- const id = element.getAttribute('data-tooltip-trigger-id');
- if (!id) return;
-
- const instance = this.activeTooltips.get(element);
- if (instance?.[0]) {
- try {
- instance[0].destroy();
- } catch (e) {
- console.warn('Error destroying tooltip:', e);
-
- const tippyRoot = document.querySelector(`[data-for-tooltip-id="${id}"]`);
- if (tippyRoot && tippyRoot.parentNode) {
- tippyRoot.parentNode.removeChild(tippyRoot);
- }
- }
- }
-
- this.activeTooltips.delete(element);
- element.removeAttribute('data-tooltip-trigger-id');
- }
-
- cleanup() {
- document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
- this.destroy(element);
- });
-
- document.querySelectorAll('[data-tippy-root]').forEach(element => {
- if (element.parentNode) {
- element.parentNode.removeChild(element);
- }
- });
- }
-
- getActiveTooltipInstances() {
- const result = [];
-
- document.querySelectorAll('[data-tooltip-trigger-id]').forEach(element => {
- const instance = this.activeTooltips.get(element);
- if (instance) {
- result.push([element, instance]);
- }
- });
-
- return result;
- }
-
- initializeMutationObserver() {
- if (this.mutationObserver) return;
-
- this.mutationObserver = new MutationObserver(mutations => {
- let needsCleanup = false;
-
- mutations.forEach(mutation => {
- if (mutation.removedNodes.length) {
- Array.from(mutation.removedNodes).forEach(node => {
- if (node.nodeType === 1) {
- if (node.hasAttribute && node.hasAttribute('data-tooltip-trigger-id')) {
- this.destroy(node);
- needsCleanup = true;
- }
-
- if (node.querySelectorAll) {
- node.querySelectorAll('[data-tooltip-trigger-id]').forEach(el => {
- this.destroy(el);
- needsCleanup = true;
- });
- }
- }
- });
- }
- });
-
- if (needsCleanup) {
- document.querySelectorAll('[data-tippy-root]').forEach(element => {
- const id = element.getAttribute('data-for-tooltip-id');
- if (id && !document.querySelector(`[data-tooltip-trigger-id="${id}"]`)) {
- if (element.parentNode) {
- element.parentNode.removeChild(element);
- }
- }
- });
- }
- });
-
- this.mutationObserver.observe(document.body, {
- childList: true,
- subtree: true
- });
- }
-
- setupStyles() {
- if (document.getElementById('tooltip-styles')) return;
-
- document.head.insertAdjacentHTML('beforeend', `
-
- `);
- }
-
- setupCleanupEvents() {
- this.boundCleanup = this.cleanup.bind(this);
- this.handleVisibilityChange = () => {
- if (document.hidden) {
- this.cleanup();
- }
- };
-
- window.addEventListener('beforeunload', this.boundCleanup);
- window.addEventListener('unload', this.boundCleanup);
- document.addEventListener('visibilitychange', this.handleVisibilityChange);
- }
-
- removeCleanupEvents() {
- window.removeEventListener('beforeunload', this.boundCleanup);
- window.removeEventListener('unload', this.boundCleanup);
- document.removeEventListener('visibilitychange', this.handleVisibilityChange);
- }
-
- initializeTooltips(selector = '[data-tooltip-target]') {
- document.querySelectorAll(selector).forEach(element => {
- const targetId = element.getAttribute('data-tooltip-target');
- const tooltipContent = document.getElementById(targetId);
-
- if (tooltipContent) {
- this.create(element, tooltipContent.innerHTML, {
- placement: element.getAttribute('data-tooltip-placement') || 'top'
- });
- }
- });
- }
-
- dispose() {
- this.cleanup();
-
- if (this.mutationObserver) {
- this.mutationObserver.disconnect();
- this.mutationObserver = null;
- }
-
- this.removeCleanupEvents();
-
- const styleElement = document.getElementById('tooltip-styles');
- if (styleElement && styleElement.parentNode) {
- styleElement.parentNode.removeChild(styleElement);
- }
-
- if (window.TooltipManager === this) {
- window.TooltipManager = null;
- }
- }
-}
-
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = TooltipManager;
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- TooltipManager.initialize();
-});
diff --git a/basicswap/static/js/dropdown.js b/basicswap/static/js/ui/dropdown.js
similarity index 91%
rename from basicswap/static/js/dropdown.js
rename to basicswap/static/js/ui/dropdown.js
index 6fdf12a..788382d 100644
--- a/basicswap/static/js/dropdown.js
+++ b/basicswap/static/js/ui/dropdown.js
@@ -1,15 +1,17 @@
(function(window) {
'use strict';
+ const dropdownInstances = [];
+
function positionElement(targetEl, triggerEl, placement = 'bottom', offsetDistance = 8) {
targetEl.style.visibility = 'hidden';
targetEl.style.display = 'block';
-
+
const triggerRect = triggerEl.getBoundingClientRect();
const targetRect = targetEl.getBoundingClientRect();
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
-
+
let top, left;
top = triggerRect.bottom + offsetDistance;
@@ -58,6 +60,9 @@
this._handleScroll = this._handleScroll.bind(this);
this._handleResize = this._handleResize.bind(this);
this._handleOutsideClick = this._handleOutsideClick.bind(this);
+
+ dropdownInstances.push(this);
+
this.init();
}
@@ -66,7 +71,8 @@
this._targetEl.style.margin = '0';
this._targetEl.style.display = 'none';
this._targetEl.style.position = 'fixed';
- this._targetEl.style.zIndex = '50';
+ this._targetEl.style.zIndex = '40';
+ this._targetEl.classList.add('dropdown-menu');
this._setupEventListeners();
this._initialized = true;
@@ -123,6 +129,12 @@
show() {
if (!this._visible) {
+ dropdownInstances.forEach(instance => {
+ if (instance !== this && instance._visible) {
+ instance.hide();
+ }
+ });
+
this._targetEl.style.display = 'block';
this._targetEl.style.visibility = 'hidden';
@@ -133,7 +145,7 @@
this._options.placement,
this._options.offset
);
-
+
this._visible = true;
this._options.onShow();
});
@@ -160,6 +172,12 @@
document.removeEventListener('click', this._handleOutsideClick);
window.removeEventListener('scroll', this._handleScroll, true);
window.removeEventListener('resize', this._handleResize);
+
+ const index = dropdownInstances.indexOf(this);
+ if (index > -1) {
+ dropdownInstances.splice(index, 1);
+ }
+
this._initialized = false;
}
}
@@ -168,7 +186,7 @@
document.querySelectorAll('[data-dropdown-toggle]').forEach(triggerEl => {
const targetId = triggerEl.getAttribute('data-dropdown-toggle');
const targetEl = document.getElementById(targetId);
-
+
if (targetEl) {
const placement = triggerEl.getAttribute('data-dropdown-placement');
new Dropdown(targetEl, triggerEl, {
@@ -184,6 +202,8 @@
initDropdowns();
}
+ Dropdown.instances = dropdownInstances;
+
window.Dropdown = Dropdown;
window.initDropdowns = initDropdowns;
diff --git a/basicswap/static/js/tabs.js b/basicswap/static/js/ui/tabs.js
similarity index 99%
rename from basicswap/static/js/tabs.js
rename to basicswap/static/js/ui/tabs.js
index a8e9c76..5ca6339 100644
--- a/basicswap/static/js/tabs.js
+++ b/basicswap/static/js/ui/tabs.js
@@ -36,7 +36,7 @@
show(tabId, force = false) {
const tab = this.getTab(tabId);
-
+
if ((tab !== this._activeTab) || force) {
this._items.forEach(t => {
if (t !== tab) {
diff --git a/basicswap/static/js/wallets.js b/basicswap/static/js/wallets.js
deleted file mode 100644
index 4f5f70c..0000000
--- a/basicswap/static/js/wallets.js
+++ /dev/null
@@ -1,654 +0,0 @@
-const Wallets = (function() {
- const CONFIG = {
- MAX_RETRIES: 5,
- BASE_DELAY: 500,
- CACHE_EXPIRATION: 5 * 60 * 1000,
- PRICE_UPDATE_INTERVAL: 5 * 60 * 1000,
- API_TIMEOUT: 30000,
- DEBOUNCE_DELAY: 300,
- CACHE_MIN_INTERVAL: 60 * 1000,
- DEFAULT_TTL: 300,
- PRICE_SOURCE: {
- PRIMARY: 'coingecko.com',
- FALLBACK: 'cryptocompare.com',
- ENABLED_SOURCES: ['coingecko.com', 'cryptocompare.com']
- }
- };
-
- const COIN_SYMBOLS = {
- 'Bitcoin': 'BTC',
- 'Particl': 'PART',
- 'Monero': 'XMR',
- 'Wownero': 'WOW',
- 'Litecoin': 'LTC',
- 'Dogecoin': 'DOGE',
- 'Firo': 'FIRO',
- 'Dash': 'DASH',
- 'PIVX': 'PIVX',
- 'Decred': 'DCR',
- 'Bitcoin Cash': 'BCH'
- };
-
- const COINGECKO_IDS = {
- 'BTC': 'btc',
- 'PART': 'part',
- 'XMR': 'xmr',
- 'WOW': 'wownero',
- 'LTC': 'ltc',
- 'DOGE': 'doge',
- 'FIRO': 'firo',
- 'DASH': 'dash',
- 'PIVX': 'pivx',
- 'DCR': 'dcr',
- 'BCH': 'bch'
- };
-
- const SHORT_NAMES = {
- 'Bitcoin': 'BTC',
- 'Particl': 'PART',
- 'Monero': 'XMR',
- 'Wownero': 'WOW',
- 'Litecoin': 'LTC',
- 'Litecoin MWEB': 'LTC MWEB',
- 'Firo': 'FIRO',
- 'Dash': 'DASH',
- 'PIVX': 'PIVX',
- 'Decred': 'DCR',
- 'Bitcoin Cash': 'BCH',
- 'Dogecoin': 'DOGE'
- };
-
- class Cache {
- constructor(expirationTime) {
- this.data = null;
- this.timestamp = null;
- this.expirationTime = expirationTime;
- }
-
- isValid() {
- return Boolean(
- this.data &&
- this.timestamp &&
- (Date.now() - this.timestamp < this.expirationTime)
- );
- }
-
- set(data) {
- this.data = data;
- this.timestamp = Date.now();
- }
-
- get() {
- if (this.isValid()) {
- return this.data;
- }
- return null;
- }
-
- clear() {
- this.data = null;
- this.timestamp = null;
- }
- }
-
- class ApiClient {
- constructor() {
- this.cache = new Cache(CONFIG.CACHE_EXPIRATION);
- this.lastFetchTime = 0;
- }
-
- async fetchPrices(forceUpdate = false) {
- const now = Date.now();
- const timeSinceLastFetch = now - this.lastFetchTime;
-
- if (!forceUpdate && timeSinceLastFetch < CONFIG.CACHE_MIN_INTERVAL) {
- const cachedData = this.cache.get();
- if (cachedData) {
- return cachedData;
- }
- }
-
- let lastError = null;
- for (let attempt = 0; attempt < CONFIG.MAX_RETRIES; attempt++) {
- try {
- const processedData = {};
- const currentSource = CONFIG.PRICE_SOURCE.PRIMARY;
-
- const shouldIncludeWow = currentSource === 'coingecko.com';
-
- const coinsToFetch = Object.values(COIN_SYMBOLS)
- .filter(symbol => shouldIncludeWow || symbol !== 'WOW')
- .map(symbol => COINGECKO_IDS[symbol] || symbol.toLowerCase())
- .join(',');
-
- const mainResponse = await fetch("/json/coinprices", {
- method: "POST",
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
- coins: coinsToFetch,
- source: currentSource,
- ttl: CONFIG.DEFAULT_TTL
- })
- });
-
- if (!mainResponse.ok) {
- throw new Error(`HTTP error: ${mainResponse.status}`);
- }
-
- const mainData = await mainResponse.json();
-
- if (mainData && mainData.rates) {
- Object.entries(mainData.rates).forEach(([coinId, price]) => {
- const symbol = Object.entries(COINGECKO_IDS).find(([sym, id]) => id.toLowerCase() === coinId.toLowerCase())?.[0];
- if (symbol) {
- const coinKey = Object.keys(COIN_SYMBOLS).find(key => COIN_SYMBOLS[key] === symbol);
- if (coinKey) {
- processedData[coinKey.toLowerCase().replace(' ', '-')] = {
- usd: price,
- btc: symbol === 'BTC' ? 1 : price / (mainData.rates.btc || 1)
- };
- }
- }
- });
- }
-
- if (!shouldIncludeWow && !processedData['wownero']) {
- try {
- const wowResponse = await fetch("/json/coinprices", {
- method: "POST",
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({
- coins: "wownero",
- source: "coingecko.com",
- ttl: CONFIG.DEFAULT_TTL
- })
- });
-
- if (wowResponse.ok) {
- const wowData = await wowResponse.json();
- if (wowData && wowData.rates && wowData.rates.wownero) {
- processedData['wownero'] = {
- usd: wowData.rates.wownero,
- btc: processedData.bitcoin ? wowData.rates.wownero / processedData.bitcoin.usd : 0
- };
- }
- }
- } catch (wowError) {
- console.error('Error fetching WOW price:', wowError);
- }
- }
-
- this.cache.set(processedData);
- this.lastFetchTime = now;
- return processedData;
- } catch (error) {
- lastError = error;
- console.error(`Price fetch attempt ${attempt + 1} failed:`, error);
-
- if (attempt === CONFIG.MAX_RETRIES - 1 &&
- CONFIG.PRICE_SOURCE.FALLBACK &&
- CONFIG.PRICE_SOURCE.FALLBACK !== CONFIG.PRICE_SOURCE.PRIMARY) {
- const temp = CONFIG.PRICE_SOURCE.PRIMARY;
- CONFIG.PRICE_SOURCE.PRIMARY = CONFIG.PRICE_SOURCE.FALLBACK;
- CONFIG.PRICE_SOURCE.FALLBACK = temp;
-
- console.warn(`Switching to fallback source: ${CONFIG.PRICE_SOURCE.PRIMARY}`);
- attempt = -1;
- continue;
- }
-
- if (attempt < CONFIG.MAX_RETRIES - 1) {
- const delay = Math.min(CONFIG.BASE_DELAY * Math.pow(2, attempt), 10000);
- await new Promise(resolve => setTimeout(resolve, delay));
- }
- }
- }
-
- const cachedData = this.cache.get();
- if (cachedData) {
- console.warn('Using cached data after fetch failures');
- return cachedData;
- }
-
- throw lastError || new Error('Failed to fetch prices');
- }
-
- setPriceSource(primarySource, fallbackSource = null) {
- if (!CONFIG.PRICE_SOURCE.ENABLED_SOURCES.includes(primarySource)) {
- throw new Error(`Invalid primary source: ${primarySource}`);
- }
-
- if (fallbackSource && !CONFIG.PRICE_SOURCE.ENABLED_SOURCES.includes(fallbackSource)) {
- throw new Error(`Invalid fallback source: ${fallbackSource}`);
- }
-
- CONFIG.PRICE_SOURCE.PRIMARY = primarySource;
- if (fallbackSource) {
- CONFIG.PRICE_SOURCE.FALLBACK = fallbackSource;
- }
- }
- }
-
- class UiManager {
- constructor() {
- this.api = new ApiClient();
- this.toggleInProgress = false;
- this.toggleDebounceTimer = null;
- this.priceUpdateInterval = null;
- this.lastUpdateTime = parseInt(localStorage.getItem(STATE_KEYS.LAST_UPDATE) || '0');
- this.isWalletsPage = document.querySelector('.wallet-list') !== null ||
- window.location.pathname.includes('/wallets');
- }
-
- getShortName(fullName) {
- return SHORT_NAMES[fullName] || fullName;
- }
-
- storeOriginalValues() {
- document.querySelectorAll('.coinname-value').forEach(el => {
- const coinName = el.getAttribute('data-coinname');
- const value = el.textContent?.trim() || '';
-
- if (coinName) {
- const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
- const coinId = COIN_SYMBOLS[coinName];
- const shortName = this.getShortName(coinName);
-
- if (coinId) {
- if (coinName === 'Particl') {
- const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
- const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
- const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
- localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
- } else if (coinName === 'Litecoin') {
- const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
- const balanceType = isMWEB ? 'mweb' : 'public';
- localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
- } else {
- localStorage.setItem(`${coinId.toLowerCase()}-amount`, amount.toString());
- }
-
- el.setAttribute('data-original-value', `${amount} ${shortName}`);
- }
- }
- });
-
- document.querySelectorAll('.usd-value').forEach(el => {
- const text = el.textContent?.trim() || '';
- if (text === 'Loading...') {
- el.textContent = '';
- }
- });
- }
-
- async updatePrices(forceUpdate = false) {
- try {
- const prices = await this.api.fetchPrices(forceUpdate);
- let newTotal = 0;
-
- const currentTime = Date.now();
- localStorage.setItem(STATE_KEYS.LAST_UPDATE, currentTime.toString());
- this.lastUpdateTime = currentTime;
-
- if (prices) {
- Object.entries(prices).forEach(([coinId, priceData]) => {
- if (priceData?.usd) {
- localStorage.setItem(`${coinId}-price`, priceData.usd.toString());
- }
- });
- }
-
- document.querySelectorAll('.coinname-value').forEach(el => {
- const coinName = el.getAttribute('data-coinname');
- const amountStr = el.getAttribute('data-original-value') || el.textContent?.trim() || '';
-
- if (!coinName) return;
-
- let amount = 0;
- if (amountStr) {
- const matches = amountStr.match(/([0-9]*[.])?[0-9]+/);
- if (matches && matches.length > 0) {
- amount = parseFloat(matches[0]);
- }
- }
-
- const coinId = coinName.toLowerCase().replace(' ', '-');
-
- if (!prices[coinId]) {
- return;
- }
-
- const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
- if (!price) return;
-
- const usdValue = (amount * price).toFixed(2);
-
- if (coinName === 'Particl') {
- const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
- const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
- const balanceType = isBlind ? 'blind' : isAnon ? 'anon' : 'public';
- localStorage.setItem(`particl-${balanceType}-last-value`, usdValue);
- localStorage.setItem(`particl-${balanceType}-amount`, amount.toString());
- } else if (coinName === 'Litecoin') {
- const isMWEB = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('MWEB');
- const balanceType = isMWEB ? 'mweb' : 'public';
- localStorage.setItem(`litecoin-${balanceType}-last-value`, usdValue);
- localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
- } else {
- localStorage.setItem(`${coinId}-last-value`, usdValue);
- localStorage.setItem(`${coinId}-amount`, amount.toString());
- }
-
- if (amount > 0) {
- newTotal += parseFloat(usdValue);
- }
-
- let usdEl = null;
-
- const flexContainer = el.closest('.flex');
- if (flexContainer) {
- const nextFlex = flexContainer.nextElementSibling;
- if (nextFlex) {
- const usdInNextFlex = nextFlex.querySelector('.usd-value');
- if (usdInNextFlex) {
- usdEl = usdInNextFlex;
- }
- }
- }
-
- if (!usdEl) {
- const parentCell = el.closest('td');
- if (parentCell) {
- const usdInSameCell = parentCell.querySelector('.usd-value');
- if (usdInSameCell) {
- usdEl = usdInSameCell;
- }
- }
- }
-
- if (!usdEl) {
- const sibling = el.nextElementSibling;
- if (sibling && sibling.classList.contains('usd-value')) {
- usdEl = sibling;
- }
- }
-
- if (!usdEl) {
- const parentElement = el.parentElement;
- if (parentElement) {
- const usdElNearby = parentElement.querySelector('.usd-value');
- if (usdElNearby) {
- usdEl = usdElNearby;
- }
- }
- }
-
- if (usdEl) {
- usdEl.textContent = `$${usdValue}`;
- usdEl.setAttribute('data-original-value', usdValue);
- }
- });
-
- document.querySelectorAll('.usd-value').forEach(el => {
- if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
- const parentCell = el.closest('td');
- if (!parentCell) return;
-
- const coinValueEl = parentCell.querySelector('.coinname-value');
- if (!coinValueEl) return;
-
- const coinName = coinValueEl.getAttribute('data-coinname');
- if (!coinName) return;
-
- const amountStr = coinValueEl.textContent?.trim() || '0';
- const amount = parseFloat(amountStr) || 0;
-
- const coinId = coinName.toLowerCase().replace(' ', '-');
- if (!prices[coinId]) return;
-
- const price = prices[coinId]?.usd || parseFloat(localStorage.getItem(`${coinId}-price`) || '0');
- if (!price) return;
-
- const usdValue = (amount * price).toFixed(8);
- el.textContent = `$${usdValue}`;
- el.setAttribute('data-original-value', usdValue);
- }
- });
-
- if (this.isWalletsPage) {
- this.updateTotalValues(newTotal, prices?.bitcoin?.usd);
- }
-
- localStorage.setItem(STATE_KEYS.PREVIOUS_TOTAL, localStorage.getItem(STATE_KEYS.CURRENT_TOTAL) || '0');
- localStorage.setItem(STATE_KEYS.CURRENT_TOTAL, newTotal.toString());
-
- return true;
- } catch (error) {
- console.error('Price update failed:', error);
- return false;
- }
- }
-
- updateTotalValues(totalUsd, btcPrice) {
- const totalUsdEl = document.getElementById('total-usd-value');
- if (totalUsdEl) {
- totalUsdEl.textContent = `$${totalUsd.toFixed(2)}`;
- totalUsdEl.setAttribute('data-original-value', totalUsd.toString());
- localStorage.setItem('total-usd', totalUsd.toString());
- }
-
- if (btcPrice) {
- const btcTotal = btcPrice ? totalUsd / btcPrice : 0;
- const totalBtcEl = document.getElementById('total-btc-value');
- if (totalBtcEl) {
- totalBtcEl.textContent = `~ ${btcTotal.toFixed(8)} BTC`;
- totalBtcEl.setAttribute('data-original-value', btcTotal.toString());
- }
- }
- }
-
- async toggleBalances() {
- if (this.toggleInProgress) return;
-
- try {
- this.toggleInProgress = true;
- const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
- const newVisibility = !balancesVisible;
-
- localStorage.setItem('balancesVisible', newVisibility.toString());
- this.updateVisibility(newVisibility);
-
- if (this.toggleDebounceTimer) {
- clearTimeout(this.toggleDebounceTimer);
- }
-
- this.toggleDebounceTimer = window.setTimeout(async () => {
- this.toggleInProgress = false;
- if (newVisibility) {
- await this.updatePrices(true);
- }
- }, CONFIG.DEBOUNCE_DELAY);
- } catch (error) {
- console.error('Failed to toggle balances:', error);
- this.toggleInProgress = false;
- }
- }
-
- updateVisibility(isVisible) {
- if (isVisible) {
- this.showBalances();
- } else {
- this.hideBalances();
- }
-
- const eyeIcon = document.querySelector("#hide-usd-amount-toggle svg");
- if (eyeIcon) {
- eyeIcon.innerHTML = isVisible ?
- '' :
- '';
- }
- }
-
- showBalances() {
- const usdText = document.getElementById('usd-text');
- if (usdText) {
- usdText.style.display = 'inline';
- }
-
- document.querySelectorAll('.coinname-value').forEach(el => {
- const originalValue = el.getAttribute('data-original-value');
- if (originalValue) {
- el.textContent = originalValue;
- }
- });
-
- document.querySelectorAll('.usd-value').forEach(el => {
- const storedValue = el.getAttribute('data-original-value');
- if (storedValue !== null && storedValue !== undefined) {
- if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
- el.textContent = `$${parseFloat(storedValue).toFixed(8)}`;
- } else {
- el.textContent = `$${parseFloat(storedValue).toFixed(2)}`;
- }
- } else {
- if (el.closest('tr')?.querySelector('td')?.textContent?.includes('Fee Estimate:')) {
- el.textContent = '$0.00000000';
- } else {
- el.textContent = '$0.00';
- }
- }
- });
-
- if (this.isWalletsPage) {
- ['total-usd-value', 'total-btc-value'].forEach(id => {
- const el = document.getElementById(id);
- const originalValue = el?.getAttribute('data-original-value');
- if (el && originalValue) {
- if (id === 'total-usd-value') {
- el.textContent = `$${parseFloat(originalValue).toFixed(2)}`;
- el.classList.add('font-extrabold');
- } else {
- el.textContent = `~ ${parseFloat(originalValue).toFixed(8)} BTC`;
- }
- }
- });
- }
- }
-
- hideBalances() {
- const usdText = document.getElementById('usd-text');
- if (usdText) {
- usdText.style.display = 'none';
- }
-
- document.querySelectorAll('.coinname-value').forEach(el => {
- el.textContent = '****';
- });
-
- document.querySelectorAll('.usd-value').forEach(el => {
- el.textContent = '****';
- });
-
- if (this.isWalletsPage) {
- ['total-usd-value', 'total-btc-value'].forEach(id => {
- const el = document.getElementById(id);
- if (el) {
- el.textContent = '****';
- }
- });
-
- const totalUsdEl = document.getElementById('total-usd-value');
- if (totalUsdEl) {
- totalUsdEl.classList.remove('font-extrabold');
- }
- }
- }
-
- async initialize() {
- document.querySelectorAll('.usd-value').forEach(el => {
- const text = el.textContent?.trim() || '';
- if (text === 'Loading...') {
- el.textContent = '';
- }
- });
-
- this.storeOriginalValues();
-
- if (localStorage.getItem('balancesVisible') === null) {
- localStorage.setItem('balancesVisible', 'true');
- }
-
- const hideBalancesToggle = document.getElementById('hide-usd-amount-toggle');
- if (hideBalancesToggle) {
- hideBalancesToggle.addEventListener('click', () => this.toggleBalances());
- }
-
- await this.loadBalanceVisibility();
-
- if (this.priceUpdateInterval) {
- clearInterval(this.priceUpdateInterval);
- }
-
- this.priceUpdateInterval = setInterval(() => {
- if (localStorage.getItem('balancesVisible') === 'true' && !this.toggleInProgress) {
- this.updatePrices(false);
- }
- }, CONFIG.PRICE_UPDATE_INTERVAL);
- }
-
- async loadBalanceVisibility() {
- const balancesVisible = localStorage.getItem('balancesVisible') === 'true';
- this.updateVisibility(balancesVisible);
-
- if (balancesVisible) {
- await this.updatePrices(true);
- }
- }
-
- cleanup() {
- if (this.priceUpdateInterval) {
- clearInterval(this.priceUpdateInterval);
- }
- }
- }
-
- const STATE_KEYS = {
- LAST_UPDATE: 'last-update-time',
- PREVIOUS_TOTAL: 'previous-total-usd',
- CURRENT_TOTAL: 'current-total-usd',
- BALANCES_VISIBLE: 'balancesVisible'
- };
-
- return {
- initialize: function() {
- const uiManager = new UiManager();
-
- window.cryptoPricingManager = uiManager;
-
- window.addEventListener('beforeunload', () => {
- uiManager.cleanup();
- });
-
- uiManager.initialize().catch(error => {
- console.error('Failed to initialize crypto pricing:', error);
- });
-
- return uiManager;
- },
-
- getUiManager: function() {
- return window.cryptoPricingManager;
- },
-
- setPriceSource: function(primarySource, fallbackSource = null) {
- const uiManager = this.getUiManager();
- if (uiManager && uiManager.api) {
- uiManager.api.setPriceSource(primarySource, fallbackSource);
- }
- }
- };
-})();
-
-document.addEventListener('DOMContentLoaded', function() {
- Wallets.initialize();
-});
diff --git a/basicswap/templates/active.html b/basicswap/templates/active.html
index 8630183..72fbad5 100644
--- a/basicswap/templates/active.html
+++ b/basicswap/templates/active.html
@@ -113,6 +113,6 @@
-
+
{% include 'footer.html' %}
diff --git a/basicswap/templates/bid.html b/basicswap/templates/bid.html
index d96d504..e916a60 100644
--- a/basicswap/templates/bid.html
+++ b/basicswap/templates/bid.html
@@ -557,6 +557,27 @@
+
+
+
+
+
+ Confirm Action
+ Are you sure?
+
+
+
+
+
+
+
+
@@ -564,9 +585,74 @@
{% include 'footer.html' %}
diff --git a/basicswap/templates/bids.html b/basicswap/templates/bids.html
index 1bf3d5d..e2ec345 100644
--- a/basicswap/templates/bids.html
+++ b/basicswap/templates/bids.html
@@ -363,6 +363,6 @@
-
+
{% include 'footer.html' %}
diff --git a/basicswap/templates/footer.html b/basicswap/templates/footer.html
index bdfd99e..0042412 100644
--- a/basicswap/templates/footer.html
+++ b/basicswap/templates/footer.html
@@ -43,68 +43,3 @@
-
-
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html
index 0e8b867..f57f5ab 100644
--- a/basicswap/templates/header.html
+++ b/basicswap/templates/header.html
@@ -9,153 +9,95 @@
swap_in_progress_green_svg, available_bids_svg, your_offers_svg, bids_received_svg,
bids_sent_svg, header_arrow_down_svg, love_svg %}
+
+
{% if refresh %}
{% endif %}
-
-
-
-
-
-
-
-
-
-
-
-
-
+ (BSX) BasicSwap - v{{ version }}
+
+
+
+
+
+
-
-
-
- (BSX) BasicSwap - v{{ version }}
-
-
+
-
-
-
-
-
-
-
+
+
+
- closeButtons.forEach(button => {
- button.addEventListener('click', function() {
- const targetId = this.getAttribute('data-dismiss-target');
- const targetElement = document.querySelector(targetId);
-
- if (targetElement) {
- targetElement.style.display = 'none';
- }
- });
- });
-});
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if current_page == 'wallets' or current_page == 'wallet' %}
+
+ {% endif %}
+
+
+
+
-
@@ -757,194 +699,3 @@ function getWebSocketConfig() {
-
-
- {% if ws_port %}
-
- {% endif %}
diff --git a/basicswap/templates/offer_confirm.html b/basicswap/templates/offer_confirm.html
index c9c941c..08e2055 100644
--- a/basicswap/templates/offer_confirm.html
+++ b/basicswap/templates/offer_confirm.html
@@ -1,5 +1,4 @@
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %}
-
diff --git a/basicswap/templates/offer_new_1.html b/basicswap/templates/offer_new_1.html
index 0165b0a..da9f42c 100644
--- a/basicswap/templates/offer_new_1.html
+++ b/basicswap/templates/offer_new_1.html
@@ -1,5 +1,4 @@
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg %}
-
-
@@ -413,225 +355,6 @@ if (document.readyState === 'loading') {
-
{% include 'footer.html' %}
diff --git a/basicswap/templates/offer_new_2.html b/basicswap/templates/offer_new_2.html
index 73169d3..b92a8a8 100644
--- a/basicswap/templates/offer_new_2.html
+++ b/basicswap/templates/offer_new_2.html
@@ -1,5 +1,4 @@
{% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, input_down_arrow_offer_svg, select_network_svg, select_address_svg, select_swap_type_svg, select_bid_amount_svg, select_rate_svg, step_one_svg, step_two_svg, step_three_svg, select_setup_svg, input_time_svg, select_target_svg , select_auto_strategy_svg %}
-
diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html
index 80b00c9..eb04efb 100644
--- a/basicswap/templates/offers.html
+++ b/basicswap/templates/offers.html
@@ -193,9 +193,9 @@
-
-{% endif %}
+{% endif %}
+
@@ -401,4 +401,5 @@
+
{% include 'footer.html' %}
diff --git a/basicswap/templates/unlock.html b/basicswap/templates/unlock.html
index 3feeb3c..918b282 100644
--- a/basicswap/templates/unlock.html
+++ b/basicswap/templates/unlock.html
@@ -9,19 +9,49 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if current_page == 'wallets' or current_page == 'wallet' %}
+
+ {% endif %}
+
+
+
(BSX) BasicSwap - v{{ version }}
@@ -107,7 +137,6 @@
diff --git a/basicswap/templates/wallet.html b/basicswap/templates/wallet.html
index 175edf7..c4d3860 100644
--- a/basicswap/templates/wallet.html
+++ b/basicswap/templates/wallet.html
@@ -1183,7 +1183,6 @@ document.addEventListener('DOMContentLoaded', function() {
});
-
{% include 'footer.html' %}
|