Extra refactor + Various bug/fixes. (#293)

* Refactor + Various Fixes.

* WS / LINT

* Show also failed status.

* Fix sorting market +/-

* Simplified swaps in progress

* Black

* Update basicswap/static/js/modules/coin-manager.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Update basicswap/static/js/modules/coin-manager.js

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>

* Fixes + GUI: v3.2.1

* Fixes + AutoRefreshEnabled true as default.

* Fix small memory issue since new features added,

---------

Co-authored-by: nahuhh <50635951+nahuhh@users.noreply.github.com>
This commit is contained in:
Gerlof van Ek
2025-04-10 21:18:03 +02:00
committed by GitHub
parent f15f073b12
commit 748dd388cb
15 changed files with 1611 additions and 1401 deletions

View File

@@ -984,76 +984,53 @@ def js_readurl(self, url_split, post_string, is_json) -> bytes:
def js_active(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 = self.server.swap_client
swap_client.checkSystemStatus() swap_client.checkSystemStatus()
filters = {
"sort_by": "created_at",
"sort_dir": "desc",
"with_available_or_active": True,
"with_extra_info": True,
}
EXCLUDED_STATES = [
"Failed, refunded",
"Failed, swiped",
"Failed",
"Error",
"Expired",
"Timed-out",
"Abandoned",
"Completed",
]
all_bids = [] all_bids = []
processed_bid_ids = set()
try: try:
received_bids = swap_client.listBids(filters=filters) for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()):
sent_bids = swap_client.listBids(sent=True, filters=filters)
for bid in received_bids + sent_bids:
try: try:
bid_id_hex = bid[2].hex()
if bid_id_hex in processed_bid_ids:
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 = { swap_data = {
"bid_id": bid_id_hex, "bid_id": bid_id.hex(),
"offer_id": bid[3].hex(), "offer_id": offer.offer_id.hex(),
"created_at": bid[0], "created_at": bid.created_at,
"bid_state": bid_state, "expire_at": bid.expire_at,
"tx_state_a": tx_state_a if tx_state_a else "None", "bid_state": strBidState(bid.state),
"tx_state_b": tx_state_b if tx_state_b else "None", "tx_state_a": None,
"coin_from": swap_client.ci(bid[9]).coin_name(), "tx_state_b": None,
"coin_from": swap_client.ci(offer.coin_from).coin_name(),
"coin_to": swap_client.ci(offer.coin_to).coin_name(), "coin_to": swap_client.ci(offer.coin_to).coin_name(),
"amount_from": swap_client.ci(bid[9]).format_amount(bid[4]), "amount_from": swap_client.ci(offer.coin_from).format_amount(
"amount_to": swap_client.ci(offer.coin_to).format_amount( bid.amount
(bid[4] * bid[10]) // swap_client.ci(bid[9]).COIN()
), ),
"addr_from": bid[11], "amount_to": swap_client.ci(offer.coin_to).format_amount(
"status": { bid.amount_to
"main": bid_state, ),
"initial_tx": tx_state_a if tx_state_a else "None", "addr_from": bid.bid_addr if bid.was_received else offer.addr_from,
"payment_tx": tx_state_b if tx_state_b else "None",
},
} }
if offer.swap_type == SwapTypes.XMR_SWAP:
swap_data["tx_state_a"] = (
strTxState(bid.xmr_a_lock_tx.state)
if bid.xmr_a_lock_tx
else None
)
swap_data["tx_state_b"] = (
strTxState(bid.xmr_b_lock_tx.state)
if bid.xmr_b_lock_tx
else None
)
else:
swap_data["tx_state_a"] = bid.getITxState()
swap_data["tx_state_b"] = bid.getPTxState()
if hasattr(bid, "rate"):
swap_data["rate"] = bid.rate
all_bids.append(swap_data) all_bids.append(swap_data)
processed_bid_ids.add(bid_id_hex)
except Exception: except Exception:
continue pass
except Exception: except Exception:
return bytes(json.dumps([]), "UTF-8") return bytes(json.dumps([]), "UTF-8")
return bytes(json.dumps(all_bids), "UTF-8") return bytes(json.dumps(all_bids), "UTF-8")

View File

@@ -1,20 +1,4 @@
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
'Litecoin': 'LTC',
'Monero': 'XMR',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'PIVX': 'PIVX',
'Firo': 'FIRO',
'Dash': 'DASH',
'Decred': 'DCR',
'Namecoin': 'NMC',
'Wownero': 'WOW',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
const state = { const state = {
dentities: new Map(), dentities: new Map(),
@@ -288,8 +272,8 @@ const createBidTableRow = async (bid) => {
const rate = toAmount > 0 ? toAmount / fromAmount : 0; const rate = toAmount > 0 ? toAmount / fromAmount : 0;
const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0; const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0;
const fromSymbol = COIN_NAME_TO_SYMBOL[bid.coin_from] || bid.coin_from; const fromSymbol = window.CoinManager.getSymbol(bid.coin_from) || bid.coin_from;
const toSymbol = COIN_NAME_TO_SYMBOL[bid.coin_to] || bid.coin_to; const toSymbol = window.CoinManager.getSymbol(bid.coin_to) || bid.coin_to;
const timeColor = getTimeStrokeColor(bid.expire_at); const timeColor = getTimeStrokeColor(bid.expire_at);
const uniqueId = `${bid.bid_id}_${bid.created_at}`; const uniqueId = `${bid.bid_id}_${bid.created_at}`;

View File

@@ -201,12 +201,23 @@ const ApiManager = (function() {
}, },
fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) { fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) {
if (!Array.isArray(coins)) { if (!coins) {
coins = [coins]; throw new Error('No coins specified for price lookup');
}
let coinsParam;
if (Array.isArray(coins)) {
coinsParam = coins.filter(c => c && c.trim() !== '').join(',');
} else if (typeof coins === 'object' && coins.coins) {
coinsParam = coins.coins;
} else {
coinsParam = coins;
}
if (!coinsParam || coinsParam.trim() === '') {
throw new Error('No valid coins to fetch prices for');
} }
return this.makeRequest('/json/coinprices', 'POST', {}, { return this.makeRequest('/json/coinprices', 'POST', {}, {
coins: Array.isArray(coins) ? coins.join(',') : coins, coins: coinsParam,
source: source, source: source,
ttl: ttl ttl: ttl
}); });

View File

@@ -0,0 +1,230 @@
const CoinManager = (function() {
const coinRegistry = [
{
symbol: 'BTC',
name: 'bitcoin',
displayName: 'Bitcoin',
aliases: ['btc', 'bitcoin'],
coingeckoId: 'bitcoin',
cryptocompareId: 'BTC',
usesCryptoCompare: false,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin.png'
},
{
symbol: 'XMR',
name: 'monero',
displayName: 'Monero',
aliases: ['xmr', 'monero'],
coingeckoId: 'monero',
cryptocompareId: 'XMR',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Monero.png'
},
{
symbol: 'PART',
name: 'particl',
displayName: 'Particl',
aliases: ['part', 'particl', 'particl anon', 'particl blind'],
variants: ['Particl', 'Particl Blind', 'Particl Anon'],
coingeckoId: 'particl',
cryptocompareId: 'PART',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Particl.png'
},
{
symbol: 'BCH',
name: 'bitcoin-cash',
displayName: 'Bitcoin Cash',
aliases: ['bch', 'bitcoincash', 'bitcoin cash'],
coingeckoId: 'bitcoin-cash',
cryptocompareId: 'BCH',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Bitcoin-Cash.png'
},
{
symbol: 'PIVX',
name: 'pivx',
displayName: 'PIVX',
aliases: ['pivx'],
coingeckoId: 'pivx',
cryptocompareId: 'PIVX',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'PIVX.png'
},
{
symbol: 'FIRO',
name: 'firo',
displayName: 'Firo',
aliases: ['firo', 'zcoin'],
coingeckoId: 'firo',
cryptocompareId: 'FIRO',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Firo.png'
},
{
symbol: 'DASH',
name: 'dash',
displayName: 'Dash',
aliases: ['dash'],
coingeckoId: 'dash',
cryptocompareId: 'DASH',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Dash.png'
},
{
symbol: 'LTC',
name: 'litecoin',
displayName: 'Litecoin',
aliases: ['ltc', 'litecoin'],
variants: ['Litecoin', 'Litecoin MWEB'],
coingeckoId: 'litecoin',
cryptocompareId: 'LTC',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Litecoin.png'
},
{
symbol: 'DOGE',
name: 'dogecoin',
displayName: 'Dogecoin',
aliases: ['doge', 'dogecoin'],
coingeckoId: 'dogecoin',
cryptocompareId: 'DOGE',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Dogecoin.png'
},
{
symbol: 'DCR',
name: 'decred',
displayName: 'Decred',
aliases: ['dcr', 'decred'],
coingeckoId: 'decred',
cryptocompareId: 'DCR',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Decred.png'
},
{
symbol: 'NMC',
name: 'namecoin',
displayName: 'Namecoin',
aliases: ['nmc', 'namecoin'],
coingeckoId: 'namecoin',
cryptocompareId: 'NMC',
usesCryptoCompare: true,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Namecoin.png'
},
{
symbol: 'WOW',
name: 'wownero',
displayName: 'Wownero',
aliases: ['wow', 'wownero'],
coingeckoId: 'wownero',
cryptocompareId: 'WOW',
usesCryptoCompare: false,
usesCoinGecko: true,
historicalDays: 30,
icon: 'Wownero.png'
}
];
const symbolToInfo = {};
const nameToInfo = {};
const displayNameToInfo = {};
const coinAliasesMap = {};
function buildLookupMaps() {
coinRegistry.forEach(coin => {
symbolToInfo[coin.symbol.toLowerCase()] = coin;
nameToInfo[coin.name.toLowerCase()] = coin;
displayNameToInfo[coin.displayName.toLowerCase()] = coin;
if (coin.aliases && Array.isArray(coin.aliases)) {
coin.aliases.forEach(alias => {
coinAliasesMap[alias.toLowerCase()] = coin;
});
}
coinAliasesMap[coin.symbol.toLowerCase()] = coin;
coinAliasesMap[coin.name.toLowerCase()] = coin;
coinAliasesMap[coin.displayName.toLowerCase()] = coin;
if (coin.variants && Array.isArray(coin.variants)) {
coin.variants.forEach(variant => {
coinAliasesMap[variant.toLowerCase()] = coin;
});
}
});
}
buildLookupMaps();
function getCoinByAnyIdentifier(identifier) {
if (!identifier) return null;
const normalizedId = identifier.toString().toLowerCase().trim();
const coin = coinAliasesMap[normalizedId];
if (coin) return coin;
if (normalizedId.includes('bitcoin') && normalizedId.includes('cash') ||
normalizedId === 'bch') {
return symbolToInfo['bch'];
}
if (normalizedId === 'zcoin' || normalizedId.includes('firo')) {
return symbolToInfo['firo'];
}
if (normalizedId.includes('particl')) {
return symbolToInfo['part'];
}
return null;
}
return {
getAllCoins: function() {
return [...coinRegistry];
},
getCoinByAnyIdentifier: getCoinByAnyIdentifier,
getSymbol: function(identifier) {
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.symbol : null;
},
getDisplayName: function(identifier) {
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.displayName : null;
},
getCoingeckoId: function(identifier) {
const coin = getCoinByAnyIdentifier(identifier);
return coin ? coin.coingeckoId : null;
},
coinMatches: function(coinId1, coinId2) {
if (!coinId1 || !coinId2) return false;
const coin1 = getCoinByAnyIdentifier(coinId1);
const coin2 = getCoinByAnyIdentifier(coinId2);
if (!coin1 || !coin2) return false;
return coin1.symbol === coin2.symbol;
},
getPriceKey: function(coinIdentifier) {
if (!coinIdentifier) return null;
const coin = getCoinByAnyIdentifier(coinIdentifier);
if (!coin) return coinIdentifier.toLowerCase();
return coin.coingeckoId;
}
};
})();
window.CoinManager = CoinManager;
console.log('CoinManager initialized');

View File

@@ -17,10 +17,8 @@ const ConfigManager = (function() {
cacheDuration: 10 * 60 * 1000, cacheDuration: 10 * 60 * 1000,
requestTimeout: 60000, requestTimeout: 60000,
wsPort: selectedWsPort, wsPort: selectedWsPort,
cacheConfig: { cacheConfig: {
defaultTTL: 10 * 60 * 1000, defaultTTL: 10 * 60 * 1000,
ttlSettings: { ttlSettings: {
prices: 5 * 60 * 1000, prices: 5 * 60 * 1000,
chart: 5 * 60 * 1000, chart: 5 * 60 * 1000,
@@ -29,17 +27,13 @@ const ConfigManager = (function() {
offers: 2 * 60 * 1000, offers: 2 * 60 * 1000,
identity: 15 * 60 * 1000 identity: 15 * 60 * 1000
}, },
storage: { storage: {
maxSizeBytes: 10 * 1024 * 1024, maxSizeBytes: 10 * 1024 * 1024,
maxItems: 200 maxItems: 200
}, },
fallbackTTL: 24 * 60 * 60 * 1000 fallbackTTL: 24 * 60 * 60 * 1000
}, },
itemsPerPage: 50, itemsPerPage: 50,
apiEndpoints: { apiEndpoints: {
cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull', cryptoCompare: 'https://min-api.cryptocompare.com/data/pricemultifull',
coinGecko: 'https://api.coingecko.com/api/v3', coinGecko: 'https://api.coingecko.com/api/v3',
@@ -47,7 +41,6 @@ const ConfigManager = (function() {
cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour', cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour',
volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price' volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price'
}, },
rateLimits: { rateLimits: {
coingecko: { coingecko: {
requestsPerMinute: 50, requestsPerMinute: 50,
@@ -58,10 +51,9 @@ const ConfigManager = (function() {
minInterval: 2000 minInterval: 2000
} }
}, },
retryDelays: [5000, 15000, 30000], retryDelays: [5000, 15000, 30000],
get coins() {
coins: [ return window.CoinManager ? window.CoinManager.getAllCoins() : [
{ symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }, { symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'XMR', name: 'monero', 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: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
@@ -74,70 +66,8 @@ const ConfigManager = (function() {
{ symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, { symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, { symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 },
{ symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, 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',
'Namecoin': 'NMC',
'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',
'Namecoin': 'Namecoin',
'Wownero': 'Wownero',
'Bitcoin Cash': 'Bitcoin Cash',
'Dogecoin': 'Dogecoin'
},
idToName: {
1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred', 5: 'namecoin',
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',
'namecoin': 'namecoin',
'wownero': 'wownero'
}
},
chartConfig: { chartConfig: {
colors: { colors: {
default: { default: {
@@ -158,28 +88,22 @@ const ConfigManager = (function() {
const publicAPI = { const publicAPI = {
...defaultConfig, ...defaultConfig,
initialize: function(options = {}) { initialize: function(options = {}) {
if (state.isInitialized) { if (state.isInitialized) {
console.warn('[ConfigManager] Already initialized'); console.warn('[ConfigManager] Already initialized');
return this; return this;
} }
if (options) { if (options) {
Object.assign(this, options); Object.assign(this, options);
} }
if (window.CleanupManager) { if (window.CleanupManager) {
window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose()); window.CleanupManager.registerResource('configManager', this, (mgr) => mgr.dispose());
} }
this.utils = utils; this.utils = utils;
state.isInitialized = true; state.isInitialized = true;
console.log('ConfigManager initialized'); console.log('ConfigManager initialized');
return this; return this;
}, },
getAPIKeys: function() { getAPIKeys: function() {
if (typeof window.getAPIKeys === 'function') { if (typeof window.getAPIKeys === 'function') {
const apiKeys = window.getAPIKeys(); const apiKeys = window.getAPIKeys();
@@ -188,16 +112,16 @@ const ConfigManager = (function() {
coinGecko: apiKeys.coinGecko || '' coinGecko: apiKeys.coinGecko || ''
}; };
} }
return { return {
cryptoCompare: '', cryptoCompare: '',
coinGecko: '' coinGecko: ''
}; };
}, },
getCoinBackendId: function(coinName) { getCoinBackendId: function(coinName) {
if (!coinName) return null; if (!coinName) return null;
if (window.CoinManager) {
return window.CoinManager.getPriceKey(coinName);
}
const nameMap = { const nameMap = {
'bitcoin-cash': 'bitcoincash', 'bitcoin-cash': 'bitcoincash',
'bitcoin cash': 'bitcoincash', 'bitcoin cash': 'bitcoincash',
@@ -205,70 +129,57 @@ const ConfigManager = (function() {
'zcoin': 'firo', 'zcoin': 'firo',
'bitcoincash': 'bitcoin-cash' 'bitcoincash': 'bitcoin-cash'
}; };
const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : ''; const lowerCoinName = typeof coinName === 'string' ? coinName.toLowerCase() : '';
return nameMap[lowerCoinName] || lowerCoinName; return nameMap[lowerCoinName] || lowerCoinName;
}, },
coinMatches: function(offerCoin, filterCoin) { coinMatches: function(offerCoin, filterCoin) {
if (!offerCoin || !filterCoin) return false; if (!offerCoin || !filterCoin) return false;
if (window.CoinManager) {
return window.CoinManager.coinMatches(offerCoin, filterCoin);
}
offerCoin = offerCoin.toLowerCase(); offerCoin = offerCoin.toLowerCase();
filterCoin = filterCoin.toLowerCase(); filterCoin = filterCoin.toLowerCase();
if (offerCoin === filterCoin) return true; if (offerCoin === filterCoin) return true;
if ((offerCoin === 'firo' || offerCoin === 'zcoin') && if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
(filterCoin === 'firo' || filterCoin === 'zcoin')) { (filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true; return true;
} }
if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') || if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
(offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) { (offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
return true; return true;
} }
const particlVariants = ['particl', 'particl anon', 'particl blind']; const particlVariants = ['particl', 'particl anon', 'particl blind'];
if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) { if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
return true; return true;
} }
if (particlVariants.includes(filterCoin)) { if (particlVariants.includes(filterCoin)) {
return offerCoin === filterCoin; return offerCoin === filterCoin;
} }
return false; return false;
}, },
update: function(path, value) { update: function(path, value) {
const parts = path.split('.'); const parts = path.split('.');
let current = this; let current = this;
for (let i = 0; i < parts.length - 1; i++) { for (let i = 0; i < parts.length - 1; i++) {
if (!current[parts[i]]) { if (!current[parts[i]]) {
current[parts[i]] = {}; current[parts[i]] = {};
} }
current = current[parts[i]]; current = current[parts[i]];
} }
current[parts[parts.length - 1]] = value; current[parts[parts.length - 1]] = value;
return this; return this;
}, },
get: function(path, defaultValue = null) { get: function(path, defaultValue = null) {
const parts = path.split('.'); const parts = path.split('.');
let current = this; let current = this;
for (let i = 0; i < parts.length; i++) { for (let i = 0; i < parts.length; i++) {
if (current === undefined || current === null) { if (current === undefined || current === null) {
return defaultValue; return defaultValue;
} }
current = current[parts[i]]; current = current[parts[i]];
} }
return current !== undefined ? current : defaultValue; return current !== undefined ? current : defaultValue;
}, },
dispose: function() { dispose: function() {
state.isInitialized = false; state.isInitialized = false;
console.log('ConfigManager disposed'); console.log('ConfigManager disposed');
@@ -290,7 +201,6 @@ const ConfigManager = (function() {
return '0'; return '0';
} }
}, },
formatDate: function(timestamp, resolution) { formatDate: function(timestamp, resolution) {
const date = new Date(timestamp); const date = new Date(timestamp);
const options = { const options = {
@@ -300,7 +210,6 @@ const ConfigManager = (function() {
}; };
return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' }); return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' });
}, },
debounce: function(func, delay) { debounce: function(func, delay) {
let timeoutId; let timeoutId;
return function(...args) { return function(...args) {
@@ -308,17 +217,14 @@ const ConfigManager = (function() {
timeoutId = setTimeout(() => func(...args), delay); timeoutId = setTimeout(() => func(...args), delay);
}; };
}, },
formatTimeLeft: function(timestamp) { formatTimeLeft: function(timestamp) {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
if (timestamp <= now) return "Expired"; if (timestamp <= now) return "Expired";
return this.formatTime(timestamp); return this.formatTime(timestamp);
}, },
formatTime: function(timestamp, addAgoSuffix = false) { formatTime: function(timestamp, addAgoSuffix = false) {
const now = Math.floor(Date.now() / 1000); const now = Math.floor(Date.now() / 1000);
const diff = Math.abs(now - timestamp); const diff = Math.abs(now - timestamp);
let timeString; let timeString;
if (diff < 60) { if (diff < 60) {
timeString = `${diff} seconds`; timeString = `${diff} seconds`;
@@ -333,10 +239,8 @@ const ConfigManager = (function() {
} else { } else {
timeString = `${Math.floor(diff / 31536000)} years`; timeString = `${Math.floor(diff / 31536000)} years`;
} }
return addAgoSuffix ? `${timeString} ago` : timeString; return addAgoSuffix ? `${timeString} ago` : timeString;
}, },
escapeHtml: function(unsafe) { escapeHtml: function(unsafe) {
if (typeof unsafe !== 'string') { if (typeof unsafe !== 'string') {
console.warn('escapeHtml received a non-string value:', unsafe); console.warn('escapeHtml received a non-string value:', unsafe);
@@ -349,7 +253,6 @@ const ConfigManager = (function() {
.replace(/"/g, "&quot;") .replace(/"/g, "&quot;")
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
}, },
formatPrice: function(coin, price) { formatPrice: function(coin, price) {
if (typeof price !== 'number' || isNaN(price)) { if (typeof price !== 'number' || isNaN(price)) {
console.warn(`Invalid price for ${coin}:`, price); console.warn(`Invalid price for ${coin}:`, price);
@@ -363,7 +266,6 @@ const ConfigManager = (function() {
if (price < 100000) return price.toFixed(1); if (price < 100000) return price.toFixed(1);
return price.toFixed(0); return price.toFixed(0);
}, },
getEmptyPriceData: function() { getEmptyPriceData: function() {
return { return {
'bitcoin': { usd: null, btc: null }, 'bitcoin': { usd: null, btc: null },
@@ -381,12 +283,13 @@ const ConfigManager = (function() {
'firo': { usd: null, btc: null } 'firo': { usd: null, btc: null }
}; };
}, },
getCoinSymbol: function(fullName) { getCoinSymbol: function(fullName) {
return publicAPI.coinMappings?.nameToSymbol[fullName] || fullName; if (window.CoinManager) {
return window.CoinManager.getSymbol(fullName) || fullName;
}
return fullName;
} }
}; };
return publicAPI; return publicAPI;
})(); })();
@@ -415,5 +318,4 @@ if (typeof module !== 'undefined') {
module.exports = ConfigManager; module.exports = ConfigManager;
} }
//console.log('ConfigManager initialized with properties:', Object.keys(ConfigManager));
console.log('ConfigManager initialized'); console.log('ConfigManager initialized');

View File

@@ -0,0 +1,233 @@
const PriceManager = (function() {
const PRICES_CACHE_KEY = 'prices_unified';
let fetchPromise = null;
let lastFetchTime = 0;
const MIN_FETCH_INTERVAL = 60000;
let isInitialized = false;
const eventListeners = {
'priceUpdate': [],
'error': []
};
return {
addEventListener: function(event, callback) {
if (eventListeners[event]) {
eventListeners[event].push(callback);
}
},
removeEventListener: function(event, callback) {
if (eventListeners[event]) {
eventListeners[event] = eventListeners[event].filter(cb => cb !== callback);
}
},
triggerEvent: function(event, data) {
if (eventListeners[event]) {
eventListeners[event].forEach(callback => callback(data));
}
},
initialize: function() {
if (isInitialized) {
console.warn('PriceManager: Already initialized');
return this;
}
if (window.CleanupManager) {
window.CleanupManager.registerResource('priceManager', this, (mgr) => {
Object.keys(eventListeners).forEach(event => {
eventListeners[event] = [];
});
});
}
setTimeout(() => this.getPrices(), 1500);
isInitialized = true;
return this;
},
getPrices: async function(forceRefresh = false) {
if (!forceRefresh) {
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
return cachedData.value;
}
}
if (fetchPromise && Date.now() - lastFetchTime < MIN_FETCH_INTERVAL) {
return fetchPromise;
}
console.log('PriceManager: Fetching latest prices.');
lastFetchTime = Date.now();
fetchPromise = this.fetchPrices()
.then(prices => {
this.triggerEvent('priceUpdate', prices);
return prices;
})
.catch(error => {
this.triggerEvent('error', error);
throw error;
})
.finally(() => {
fetchPromise = null;
});
return fetchPromise;
},
fetchPrices: async function() {
try {
if (!NetworkManager.isOnline()) {
throw new Error('Network is offline');
}
const coinSymbols = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: (window.config.coins
? window.config.coins.map(c => c.symbol).filter(symbol => symbol && symbol.trim() !== '')
: ['BTC', 'XMR', 'PART', 'BCH', 'PIVX', 'FIRO', 'DASH', 'LTC', 'DOGE', 'DCR', 'NMC', 'WOW']);
console.log('PriceManager: lookupFiatRates ' + coinSymbols.join(', '));
if (!coinSymbols.length) {
throw new Error('No valid coins configured');
}
let apiResponse;
try {
apiResponse = await Api.fetchCoinPrices(
coinSymbols,
"coingecko.com",
300
);
if (!apiResponse) {
throw new Error('Empty response received from API');
}
if (apiResponse.error) {
throw new Error(`API error: ${apiResponse.error}`);
}
if (!apiResponse.rates) {
throw new Error('No rates found in API response');
}
if (typeof apiResponse.rates !== 'object' || Object.keys(apiResponse.rates).length === 0) {
throw new Error('Empty rates object in API response');
}
} catch (apiError) {
console.error('API call error:', apiError);
throw new Error(`API error: ${apiError.message}`);
}
const processedData = {};
Object.entries(apiResponse.rates).forEach(([coinId, price]) => {
let normalizedCoinId;
if (window.CoinManager) {
const coin = window.CoinManager.getCoinByAnyIdentifier(coinId);
if (coin) {
normalizedCoinId = window.CoinManager.getPriceKey(coin.name);
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
}
} else {
normalizedCoinId = coinId === 'bitcoincash' ? 'bitcoin-cash' : coinId.toLowerCase();
}
if (coinId.toLowerCase() === 'zcoin') {
normalizedCoinId = 'firo';
}
processedData[normalizedCoinId] = {
usd: price,
btc: normalizedCoinId === 'bitcoin' ? 1 : price / (apiResponse.rates.bitcoin || 1)
};
});
CacheManager.set(PRICES_CACHE_KEY, processedData, 'prices');
Object.entries(processedData).forEach(([coin, prices]) => {
if (prices.usd) {
if (window.tableRateModule) {
window.tableRateModule.setFallbackValue(coin, prices.usd);
}
}
});
return processedData;
} catch (error) {
console.error('Error fetching prices:', error);
NetworkManager.handleNetworkError(error);
const cachedData = CacheManager.get(PRICES_CACHE_KEY);
if (cachedData) {
console.log('Using cached price data');
return cachedData.value;
}
try {
const existingCache = localStorage.getItem(PRICES_CACHE_KEY);
if (existingCache) {
console.log('Using localStorage cached price data');
return JSON.parse(existingCache).value;
}
} catch (e) {
console.warn('Failed to parse existing cache:', e);
}
const emptyData = {};
const coinNames = window.CoinManager
? window.CoinManager.getAllCoins().map(c => c.name.toLowerCase())
: ['bitcoin', 'bitcoin-cash', 'dash', 'dogecoin', 'decred', 'namecoin', 'litecoin', 'particl', 'pivx', 'monero', 'wownero', 'firo'];
coinNames.forEach(coin => {
emptyData[coin] = { usd: null, btc: null };
});
return emptyData;
}
},
getCoinPrice: function(coinSymbol) {
if (!coinSymbol) return null;
const prices = this.getPrices();
if (!prices) return null;
let normalizedSymbol;
if (window.CoinManager) {
normalizedSymbol = window.CoinManager.getPriceKey(coinSymbol);
} else {
normalizedSymbol = coinSymbol.toLowerCase();
}
return prices[normalizedSymbol] || null;
},
formatPrice: function(coin, price) {
if (window.config && window.config.utils && window.config.utils.formatPrice) {
return window.config.utils.formatPrice(coin, price);
}
if (typeof price !== 'number' || isNaN(price)) return 'N/A';
if (price < 0.01) return price.toFixed(8);
if (price < 1) return price.toFixed(4);
if (price < 1000) return price.toFixed(2);
return price.toFixed(0);
}
};
})();
window.PriceManager = PriceManager;
document.addEventListener('DOMContentLoaded', function() {
if (!window.priceManagerInitialized) {
window.PriceManager = PriceManager.initialize();
window.priceManagerInitialized = true;
}
});
console.log('PriceManager initialized');

View File

@@ -23,54 +23,6 @@ const WalletManager = (function() {
balancesVisible: 'balancesVisible' 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',
'Namecoin': 'NMC',
'Bitcoin Cash': 'BCH'
},
coingeckoIds: {
'BTC': 'btc',
'PART': 'part',
'XMR': 'xmr',
'WOW': 'wownero',
'LTC': 'ltc',
'DOGE': 'doge',
'FIRO': 'firo',
'DASH': 'dash',
'PIVX': 'pivx',
'DCR': 'dcr',
'NMC': 'nmc',
'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',
'Namecoin': 'NMC',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
}
};
const state = { const state = {
lastFetchTime: 0, lastFetchTime: 0,
toggleInProgress: false, toggleInProgress: false,
@@ -83,7 +35,23 @@ const WalletManager = (function() {
}; };
function getShortName(fullName) { function getShortName(fullName) {
return coinData.shortNames[fullName] || fullName; return window.CoinManager.getSymbol(fullName) || fullName;
}
function getCoingeckoId(coinName) {
if (!window.CoinManager) {
console.warn('[WalletManager] CoinManager not available');
return coinName;
}
const coin = window.CoinManager.getCoinByAnyIdentifier(coinName);
if (!coin) {
console.warn(`[WalletManager] No coin found for: ${coinName}`);
return coinName;
}
return coin.symbol;
} }
async function fetchPrices(forceUpdate = false) { async function fetchPrices(forceUpdate = false) {
@@ -105,16 +73,33 @@ const WalletManager = (function() {
const shouldIncludeWow = currentSource === 'coingecko.com'; const shouldIncludeWow = currentSource === 'coingecko.com';
const coinsToFetch = Object.values(coinData.symbols) const coinsToFetch = [];
.filter(symbol => shouldIncludeWow || symbol !== 'WOW') const processedCoins = new Set();
.map(symbol => coinData.coingeckoIds[symbol] || symbol.toLowerCase())
.join(','); document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname');
if (!coinName || processedCoins.has(coinName)) return;
const adjustedName = coinName === 'Zcoin' ? 'Firo' :
coinName.includes('Particl') ? 'Particl' :
coinName;
const coinId = getCoingeckoId(adjustedName);
if (coinId && (shouldIncludeWow || coinId !== 'WOW')) {
coinsToFetch.push(coinId);
processedCoins.add(coinName);
}
});
const fetchCoinsString = coinsToFetch.join(',');
const mainResponse = await fetch("/json/coinprices", { const mainResponse = await fetch("/json/coinprices", {
method: "POST", method: "POST",
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ body: JSON.stringify({
coins: coinsToFetch, coins: fetchCoinsString,
source: currentSource, source: currentSource,
ttl: config.defaultTTL ttl: config.defaultTTL
}) })
@@ -127,46 +112,27 @@ const WalletManager = (function() {
const mainData = await mainResponse.json(); const mainData = await mainResponse.json();
if (mainData && mainData.rates) { if (mainData && mainData.rates) {
Object.entries(mainData.rates).forEach(([coinId, price]) => { document.querySelectorAll('.coinname-value').forEach(el => {
const symbol = Object.entries(coinData.coingeckoIds).find(([sym, id]) => id.toLowerCase() === coinId.toLowerCase())?.[0]; const coinName = el.getAttribute('data-coinname');
if (symbol) { if (!coinName) return;
const coinKey = Object.keys(coinData.symbols).find(key => coinData.symbols[key] === symbol);
if (coinKey) { const adjustedName = coinName === 'Zcoin' ? 'Firo' :
processedData[coinKey.toLowerCase().replace(' ', '-')] = { coinName.includes('Particl') ? 'Particl' :
coinName;
const coinId = getCoingeckoId(adjustedName);
const price = mainData.rates[coinId];
if (price) {
const coinKey = coinName.toLowerCase().replace(' ', '-');
processedData[coinKey] = {
usd: price, usd: price,
btc: symbol === 'BTC' ? 1 : price / (mainData.rates.btc || 1) btc: coinId === '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); CacheManager.set(state.cacheKey, processedData, config.cacheExpiration);
state.lastFetchTime = now; state.lastFetchTime = now;
return processedData; return processedData;
@@ -202,7 +168,6 @@ const WalletManager = (function() {
throw lastError || new Error('Failed to fetch prices'); throw lastError || new Error('Failed to fetch prices');
} }
// UI Management functions
function storeOriginalValues() { function storeOriginalValues() {
document.querySelectorAll('.coinname-value').forEach(el => { document.querySelectorAll('.coinname-value').forEach(el => {
const coinName = el.getAttribute('data-coinname'); const coinName = el.getAttribute('data-coinname');
@@ -210,10 +175,10 @@ const WalletManager = (function() {
if (coinName) { if (coinName) {
const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0; const amount = value ? parseFloat(value.replace(/[^0-9.-]+/g, '')) : 0;
const coinId = coinData.symbols[coinName]; const coinSymbol = window.CoinManager.getSymbol(coinName);
const shortName = getShortName(coinName); const shortName = getShortName(coinName);
if (coinId) { if (coinSymbol) {
if (coinName === 'Particl') { if (coinName === 'Particl') {
const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind'); const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind');
const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon'); const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon');
@@ -224,7 +189,7 @@ const WalletManager = (function() {
const balanceType = isMWEB ? 'mweb' : 'public'; const balanceType = isMWEB ? 'mweb' : 'public';
localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString()); localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString());
} else { } else {
localStorage.setItem(`${coinId.toLowerCase()}-amount`, amount.toString()); localStorage.setItem(`${coinSymbol.toLowerCase()}-amount`, amount.toString());
} }
el.setAttribute('data-original-value', `${amount} ${shortName}`); el.setAttribute('data-original-value', `${amount} ${shortName}`);

View File

@@ -168,6 +168,8 @@ const WebSocketManager = (function() {
} }
state.isConnecting = false; state.isConnecting = false;
state.messageHandlers = {};
if (ws) { if (ws) {
ws.onopen = null; ws.onopen = null;

File diff suppressed because it is too large Load Diff

View File

@@ -118,122 +118,28 @@ const api = {
}, },
fetchCoinGeckoDataXHR: async () => { fetchCoinGeckoDataXHR: async () => {
const cacheKey = 'coinGeckoOneLiner';
const cachedData = CacheManager.get(cacheKey);
if (cachedData) {
return cachedData.value;
}
try { try {
if (!NetworkManager.isOnline()) { const priceData = await window.PriceManager.getPrices();
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 = {}; const transformedData = {};
window.config.coins.forEach(coin => { window.config.coins.forEach(coin => {
const coinName = coin.name;
const coinRate = apiResponse.rates[coinName];
if (coinRate) {
const symbol = coin.symbol.toLowerCase(); const symbol = coin.symbol.toLowerCase();
const coinData = priceData[symbol] || priceData[coin.name.toLowerCase()];
if (coinData && coinData.usd) {
transformedData[symbol] = { transformedData[symbol] = {
current_price: coinRate, current_price: coinData.usd,
price_btc: coinName === 'bitcoin' ? 1 : coinRate / (apiResponse.rates.bitcoin || 1), price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0),
total_volume: fallbackData && fallbackData[symbol] ? fallbackData[symbol].total_volume : null, displayName: coin.displayName || coin.symbol
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; return transformedData;
} catch (error) { } catch (error) {
console.error('Error fetching coin data:', error); console.error('Error in fetchCoinGeckoDataXHR:', error);
return {};
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);
if (existingCache) {
const parsed = JSON.parse(existingCache);
if (parsed.value) {
console.log('Using expired cache as last resort');
return parsed.value;
}
}
} catch (e) {
console.warn('Failed to parse expired cache:', e);
}
throw error;
} }
}, },
@@ -617,6 +523,7 @@ const chartModule = {
currentCoin: 'BTC', currentCoin: 'BTC',
loadStartTime: 0, loadStartTime: 0,
chartRefs: new WeakMap(), chartRefs: new WeakMap(),
pendingAnimationFrame: null,
verticalLinePlugin: { verticalLinePlugin: {
id: 'verticalLine', id: 'verticalLine',
@@ -650,29 +557,20 @@ const chartModule = {
destroyChart: function() { destroyChart: function() {
if (chartModule.chart) { if (chartModule.chart) {
try { try {
const chartInstance = chartModule.chart;
const canvas = document.getElementById('coin-chart'); 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; chartModule.chart = null;
if (chartInstance && chartInstance.destroy && typeof chartInstance.destroy === 'function') {
chartInstance.destroy();
}
if (canvas) { if (canvas) {
chartModule.chartRefs.delete(canvas); chartModule.chartRefs.delete(canvas);
} }
} catch (e) { } catch (e) {
try { console.error('Error destroying chart:', e);
if (chartModule.chart) {
if (chartModule.chart.destroy && typeof chartModule.chart.destroy === 'function') {
chartModule.chart.destroy();
}
chartModule.chart = null;
}
} catch (finalError) {}
} }
} }
}, },
@@ -1088,11 +986,18 @@ const chartModule = {
}, },
cleanup: function() { cleanup: function() {
this.destroyChart(); if (this.pendingAnimationFrame) {
cancelAnimationFrame(this.pendingAnimationFrame);
this.pendingAnimationFrame = null;
}
if (!document.hidden) {
this.currentCoin = null; this.currentCoin = null;
}
this.loadStartTime = 0; this.loadStartTime = 0;
this.chartRefs = new WeakMap(); this.chartRefs = new WeakMap();
} }
}; };
Chart.register(chartModule.verticalLinePlugin); Chart.register(chartModule.verticalLinePlugin);
@@ -1152,14 +1057,16 @@ const app = {
nextRefreshTime: null, nextRefreshTime: null,
lastRefreshedTime: null, lastRefreshedTime: null,
isRefreshing: false, isRefreshing: false,
isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'false', isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'true',
updateNextRefreshTimeRAF: null,
refreshTexts: { refreshTexts: {
label: 'Auto-refresh in', label: 'Auto-refresh in',
disabled: 'Auto-refresh: disabled', disabled: 'Auto-refresh: disabled',
justRefreshed: 'Just refreshed', justRefreshed: 'Just refreshed',
}, },
cacheTTL: window.config.cacheConfig.ttlSettings.prices, cacheTTL: window.config.cacheConfig.ttlSettings.prices,
minimumRefreshInterval: 60 * 1000, minimumRefreshInterval: 300 * 1000,
init: function() { init: function() {
window.addEventListener('load', app.onLoad); window.addEventListener('load', app.onLoad);
@@ -1329,7 +1236,6 @@ const app = {
const headers = document.querySelectorAll('th'); const headers = document.querySelectorAll('th');
headers.forEach((header, index) => { headers.forEach((header, index) => {
CleanupManager.addListener(header, 'click', () => app.sortTable(index, header.classList.contains('disabled')));
}); });
const closeErrorButton = document.getElementById('close-error'); const closeErrorButton = document.getElementById('close-error');
@@ -1355,6 +1261,52 @@ const app = {
} }
}, },
updateNextRefreshTime: function() {
const nextRefreshSpan = document.getElementById('next-refresh-time');
const labelElement = document.getElementById('next-refresh-label');
const valueElement = document.getElementById('next-refresh-value');
if (nextRefreshSpan && labelElement && valueElement) {
if (app.isRefreshing) {
labelElement.textContent = '';
valueElement.textContent = 'Refreshing...';
valueElement.classList.add('text-blue-500');
return;
} else {
valueElement.classList.remove('text-blue-500');
}
if (app.nextRefreshTime) {
if (app.updateNextRefreshTimeRAF) {
cancelAnimationFrame(app.updateNextRefreshTimeRAF);
app.updateNextRefreshTimeRAF = null;
}
const updateDisplay = () => {
const timeUntilRefresh = Math.max(0, Math.ceil((app.nextRefreshTime - Date.now()) / 1000));
if (timeUntilRefresh === 0) {
labelElement.textContent = '';
valueElement.textContent = app.refreshTexts.justRefreshed;
} else {
const minutes = Math.floor(timeUntilRefresh / 60);
const seconds = timeUntilRefresh % 60;
labelElement.textContent = `${app.refreshTexts.label}: `;
valueElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
if (timeUntilRefresh > 0) {
app.updateNextRefreshTimeRAF = requestAnimationFrame(updateDisplay);
}
};
updateDisplay();
} else {
labelElement.textContent = '';
valueElement.textContent = app.refreshTexts.disabled;
}
}
},
scheduleNextRefresh: function() { scheduleNextRefresh: function() {
if (app.autoRefreshInterval) { if (app.autoRefreshInterval) {
clearTimeout(app.autoRefreshInterval); clearTimeout(app.autoRefreshInterval);
@@ -1376,7 +1328,7 @@ const app = {
} }
}); });
let nextRefreshTime; let nextRefreshTime = now + app.minimumRefreshInterval;
if (earliestExpiration !== Infinity) { if (earliestExpiration !== Infinity) {
nextRefreshTime = Math.max(earliestExpiration, now + app.minimumRefreshInterval); nextRefreshTime = Math.max(earliestExpiration, now + app.minimumRefreshInterval);
} else { } else {
@@ -1395,7 +1347,9 @@ const app = {
app.updateNextRefreshTime(); app.updateNextRefreshTime();
}, },
refreshAllData: async function() { refreshAllData: async function() {
console.log('Price refresh started at', new Date().toLocaleTimeString());
if (app.isRefreshing) { if (app.isRefreshing) {
console.log('Refresh already in progress, skipping...'); console.log('Refresh already in progress, skipping...');
return; return;
@@ -1430,6 +1384,7 @@ const app = {
console.log('Starting refresh of all data...'); console.log('Starting refresh of all data...');
app.isRefreshing = true; app.isRefreshing = true;
app.updateNextRefreshTime();
ui.showLoader(); ui.showLoader();
chartModule.showChartLoader(); chartModule.showChartLoader();
@@ -1494,6 +1449,8 @@ const app = {
const cacheKey = `coinData_${coin.symbol}`; const cacheKey = `coinData_${coin.symbol}`;
CacheManager.set(cacheKey, coinData, 'prices'); CacheManager.set(cacheKey, coinData, 'prices');
console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`);
} catch (coinError) { } catch (coinError) {
console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`); console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`);
failedCoins.push(coin.symbol); failedCoins.push(coin.symbol);
@@ -1532,8 +1489,7 @@ const app = {
} }
}, 1000); }, 1000);
} }
console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`);
console.log(`Refresh completed. Failed coins: ${failedCoins.length}`);
} catch (error) { } catch (error) {
console.error('Critical error during refresh:', error); console.error('Critical error during refresh:', error);
@@ -1552,51 +1508,21 @@ const app = {
} }
}, 1000); }, 1000);
console.error(`Price refresh failed at ${new Date().toLocaleTimeString()}: ${error.message}`);
} finally { } finally {
ui.hideLoader(); ui.hideLoader();
chartModule.hideChartLoader(); chartModule.hideChartLoader();
app.isRefreshing = false; app.isRefreshing = false;
app.updateNextRefreshTime();
if (app.isAutoRefreshEnabled) { if (app.isAutoRefreshEnabled) {
app.scheduleNextRefresh(); app.scheduleNextRefresh();
} }
}
},
updateNextRefreshTime: function() { console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`);
const nextRefreshSpan = document.getElementById('next-refresh-time');
const labelElement = document.getElementById('next-refresh-label');
const valueElement = document.getElementById('next-refresh-value');
if (nextRefreshSpan && labelElement && valueElement) {
if (app.nextRefreshTime) {
if (app.updateNextRefreshTimeRAF) {
cancelAnimationFrame(app.updateNextRefreshTimeRAF);
} }
},
const updateDisplay = () => {
const timeUntilRefresh = Math.max(0, Math.ceil((app.nextRefreshTime - Date.now()) / 1000));
if (timeUntilRefresh === 0) {
labelElement.textContent = '';
valueElement.textContent = app.refreshTexts.justRefreshed;
} else {
const minutes = Math.floor(timeUntilRefresh / 60);
const seconds = timeUntilRefresh % 60;
labelElement.textContent = `${app.refreshTexts.label}: `;
valueElement.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
if (timeUntilRefresh > 0) {
app.updateNextRefreshTimeRAF = requestAnimationFrame(updateDisplay);
}
};
updateDisplay();
} else {
labelElement.textContent = '';
valueElement.textContent = app.refreshTexts.disabled;
}
}
},
updateAutoRefreshButton: function() { updateAutoRefreshButton: function() {
const button = document.getElementById('toggle-auto-refresh'); const button = document.getElementById('toggle-auto-refresh');
@@ -1649,23 +1575,33 @@ const app = {
updateBTCPrice: async function() { updateBTCPrice: async function() {
try { try {
if (!NetworkManager.isOnline()) { const priceData = await window.PriceManager.getPrices();
throw new Error('Network is offline');
if (priceData) {
if (priceData.bitcoin && priceData.bitcoin.usd) {
app.btcPriceUSD = priceData.bitcoin.usd;
return true;
} else if (priceData.btc && priceData.btc.usd) {
app.btcPriceUSD = priceData.btc.usd;
return true;
}
} }
const response = await Api.fetchCoinPrices("bitcoin"); if (app.btcPriceUSD > 0) {
console.log('Using previously cached BTC price:', app.btcPriceUSD);
if (response && response.rates && response.rates.bitcoin) {
app.btcPriceUSD = response.rates.bitcoin;
return true; return true;
} }
console.warn('Unexpected BTC price data structure:', response); console.warn('Could not find BTC price in current data');
return false; return false;
} catch (error) { } catch (error) {
console.error('Error fetching BTC price:', error); console.error('Error fetching BTC price:', error);
NetworkManager.handleNetworkError(error);
if (app.btcPriceUSD > 0) {
console.log('Using previously cached BTC price after error:', app.btcPriceUSD);
return true;
}
return false; return false;
} }
}, },
@@ -1815,13 +1751,13 @@ document.addEventListener('DOMContentLoaded', () => {
CleanupManager.setInterval(() => { CleanupManager.setInterval(() => {
CacheManager.cleanup(); CacheManager.cleanup();
}, 300000); // Every 5 minutes }, 300000);
CleanupManager.setInterval(() => { CleanupManager.setInterval(() => {
if (chartModule && chartModule.currentCoin && NetworkManager.isOnline()) { if (chartModule && chartModule.currentCoin && NetworkManager.isOnline()) {
chartModule.updateChart(chartModule.currentCoin); chartModule.updateChart(chartModule.currentCoin);
} }
}, 900000); // Every 15 minutes }, 900000);
CleanupManager.addListener(document, 'visibilitychange', () => { CleanupManager.addListener(document, 'visibilitychange', () => {
if (!document.hidden) { if (!document.hidden) {

View File

@@ -1,20 +1,4 @@
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
const COIN_NAME_TO_SYMBOL = {
'Bitcoin': 'BTC',
'Litecoin': 'LTC',
'Monero': 'XMR',
'Particl': 'PART',
'Particl Blind': 'PART',
'Particl Anon': 'PART',
'PIVX': 'PIVX',
'Firo': 'FIRO',
'Dash': 'DASH',
'Decred': 'DCR',
'Namecoin': 'NMC',
'Wownero': 'WOW',
'Bitcoin Cash': 'BCH',
'Dogecoin': 'DOGE'
};
const state = { const state = {
identities: new Map(), identities: new Map(),
@@ -309,8 +293,8 @@ const createSwapTableRow = async (swap) => {
const identity = await IdentityManager.getIdentityData(swap.addr_from); const identity = await IdentityManager.getIdentityData(swap.addr_from);
const uniqueId = `${swap.bid_id}_${swap.created_at}`; const uniqueId = `${swap.bid_id}_${swap.created_at}`;
const fromSymbol = COIN_NAME_TO_SYMBOL[swap.coin_from] || swap.coin_from; const fromSymbol = window.CoinManager.getSymbol(swap.coin_from) || swap.coin_from;
const toSymbol = COIN_NAME_TO_SYMBOL[swap.coin_to] || swap.coin_to; const toSymbol = window.CoinManager.getSymbol(swap.coin_to) || swap.coin_to;
const timeColor = getTimeStrokeColor(swap.expire_at); const timeColor = getTimeStrokeColor(swap.expire_at);
const fromAmount = parseFloat(swap.amount_from) || 0; const fromAmount = parseFloat(swap.amount_from) || 0;
const toAmount = parseFloat(swap.amount_to) || 0; const toAmount = parseFloat(swap.amount_to) || 0;
@@ -630,31 +614,7 @@ async function updateSwapsTable(options = {}) {
} }
function isActiveSwap(swap) { function isActiveSwap(swap) {
const activeStates = [ return true;
'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 = () => { const setupEventListeners = () => {

View File

@@ -25,7 +25,7 @@
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2025~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-gray-90 dark:text-white font-medium">© 2025~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.2.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-coolGray-400 font-medium">GUI: v3.2.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p> <p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }} {{ love_svg | safe }}
</div> </div>

View File

@@ -12,23 +12,18 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<!-- Meta Tags -->
<meta charset="UTF-8"> <meta charset="UTF-8">
{% if refresh %} {% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}"> <meta http-equiv="refresh" content="{{ refresh }}">
{% endif %} {% endif %}
<title>(BSX) BasicSwap - v{{ version }}</title> <title>(BSX) BasicSwap - v{{ version }}</title>
<!-- Favicon -->
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png"> <link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<!-- CSS Stylesheets -->>
<!-- Stylesheets -->
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet"> <link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet"> <link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<!-- Custom styles -->
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet"> <link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script> <script>
// API Keys Configuration
function getAPIKeys() { function getAPIKeys() {
return { return {
cryptoCompare: "{{ chart_api_key|safe }}", cryptoCompare: "{{ chart_api_key|safe }}",
@@ -36,7 +31,6 @@
}; };
} }
// WebSocket Configuration
(function() { (function() {
Object.defineProperty(window, 'ws_port', { Object.defineProperty(window, 'ws_port', {
value: "{{ ws_port|safe }}", value: "{{ ws_port|safe }}",
@@ -44,16 +38,14 @@
configurable: false, configurable: false,
enumerable: true enumerable: true
}); });
window.getWebSocketConfig = window.getWebSocketConfig || function() { window.getWebSocketConfig = window.getWebSocketConfig || function() {
return { return {
port: window.ws_port || '11701', port: window.ws_port || '11700',
fallbackPort: '11700' fallbackPort: '11700'
}; };
}; };
})(); })();
// Dark Mode Initialization
(function() { (function() {
const isDarkMode = localStorage.getItem('color-theme') === 'dark' || const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches); (!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
@@ -64,28 +56,23 @@
document.documentElement.classList.toggle('dark', isDarkMode); document.documentElement.classList.toggle('dark', isDarkMode);
})(); })();
</script> </script>
<!-- Third-party Libraries --> <!-- Third-party Libraries -->
<script src="/static/js/libs/chart.js"></script> <script src="/static/js/libs/chart.js"></script>
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script> <script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/static/js/libs/popper.js"></script> <script src="/static/js/libs/popper.js"></script>
<script src="/static/js/libs/tippy.js"></script> <script src="/static/js/libs/tippy.js"></script>
<!-- UI Components --> <!-- UI Components -->
<script src="/static/js/ui/tabs.js"></script> <script src="/static/js/ui/tabs.js"></script>
<script src="/static/js/ui/dropdown.js"></script> <script src="/static/js/ui/dropdown.js"></script>
<!-- Core functionality -->
<!-- Core Application Modules --> <script src="/static/js/modules/coin-manager.js"></script>
<script src="/static/js/modules/config-manager.js"></script> <script src="/static/js/modules/config-manager.js"></script>
<script src="/static/js/modules/cache-manager.js"></script> <script src="/static/js/modules/cache-manager.js"></script>
<script src="/static/js/modules/cleanup-manager.js"></script> <script src="/static/js/modules/cleanup-manager.js"></script>
<!-- Connection & Communication Modules -->
<script src="/static/js/modules/websocket-manager.js"></script> <script src="/static/js/modules/websocket-manager.js"></script>
<script src="/static/js/modules/network-manager.js"></script> <script src="/static/js/modules/network-manager.js"></script>
<script src="/static/js/modules/api-manager.js"></script> <script src="/static/js/modules/api-manager.js"></script>
<script src="/static/js/modules/price-manager.js"></script>
<!-- UI & Interaction Modules -->
<script src="/static/js/modules/tooltips-manager.js"></script> <script src="/static/js/modules/tooltips-manager.js"></script>
<script src="/static/js/modules/notification-manager.js"></script> <script src="/static/js/modules/notification-manager.js"></script>
<script src="/static/js/modules/identity-manager.js"></script> <script src="/static/js/modules/identity-manager.js"></script>
@@ -93,9 +80,9 @@
{% if current_page == 'wallets' or current_page == 'wallet' %} {% if current_page == 'wallets' or current_page == 'wallet' %}
<script src="/static/js/modules/wallet-manager.js"></script> <script src="/static/js/modules/wallet-manager.js"></script>
{% endif %} {% endif %}
<!-- Memory management -->
<script src="/static/js/modules/memory-manager.js"></script> <script src="/static/js/modules/memory-manager.js"></script>
<!-- Main application script -->
<!-- Global Script -->
<script src="/static/js/global.js"></script> <script src="/static/js/global.js"></script>
</head> </head>
<body class="dark:bg-gray-700"> <body class="dark:bg-gray-700">

View File

@@ -343,16 +343,16 @@
{% endif %} {% endif %}
</div> </div>
</th> </th>
<th class="p-0" data-sortable="true" data-column-index="5"> <th class="p-0" data-sortable="true" data-column-index="6">
<div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right flex items-center justify-end"> <div class="py-3 px-4 bg-coolGray-200 dark:bg-gray-600 text-right flex items-center justify-end">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Rate</span> <span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Rate</span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-5"></span> <span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6"></span>
</div> </div>
</th> </th>
<th class="p-0" data-sortable="true" data-column-index="6"> <th class="p-0" data-sortable="true" data-column-index="7">
<div class="py-3 bg-coolGray-200 dark:bg-gray-600 text-center flex items-center justify-center"> <div class="py-3 bg-coolGray-200 dark:bg-gray-600 text-center flex items-center justify-center">
<span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Market +/-</span> <span class="text-sm text-gray-600 dark:text-gray-300 font-semibold">Market +/-</span>
<span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-6"></span> <span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-7"></span>
</div> </div>
</th> </th>
<th class="p-0"> <th class="p-0">

View File

@@ -1,36 +1,68 @@
{% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %} {% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
{% if refresh %} {% if refresh %}
<meta http-equiv="refresh" content="{{ refresh }}"> <meta http-equiv="refresh" content="{{ refresh }}">
{% endif %} {% endif %}
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet" /> <title>(BSX) BasicSwap - v{{ version }}</title>
<link rel="icon" sizes="32x32" type="image/png" href="/static/images/favicon/favicon-32.png">
<!-- CSS Stylesheets -->>
<link type="text/css" media="all" href="/static/css/libs/flowbite.min.css" rel="stylesheet">
<link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet"> <link type="text/css" media="all" href="/static/css/libs/tailwind.min.css" rel="stylesheet">
<!-- Custom styles -->
<link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet"> <link type="text/css" media="all" href="/static/css/style.css" rel="stylesheet">
<script>
function getAPIKeys() {
return {
cryptoCompare: "{{ chart_api_key|safe }}",
coinGecko: "{{ coingecko_api_key|safe }}"
};
}
(function() {
Object.defineProperty(window, 'ws_port', {
value: "{{ ws_port|safe }}",
writable: false,
configurable: false,
enumerable: true
});
window.getWebSocketConfig = window.getWebSocketConfig || function() {
return {
port: window.ws_port || '11700',
fallbackPort: '11700'
};
};
})();
(function() {
const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||
(!localStorage.getItem('color-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (!localStorage.getItem('color-theme')) {
localStorage.setItem('color-theme', 'dark');
}
document.documentElement.classList.toggle('dark', isDarkMode);
})();
</script>
<!-- Third-party Libraries --> <!-- Third-party Libraries -->
<script src="/static/js/libs/chart.js"></script> <script src="/static/js/libs/chart.js"></script>
<script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script> <script src="/static/js/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/static/js/libs/popper.js"></script> <script src="/static/js/libs/popper.js"></script>
<script src="/static/js/libs/tippy.js"></script> <script src="/static/js/libs/tippy.js"></script>
<!-- UI Components --> <!-- UI Components -->
<script src="/static/js/ui/tabs.js"></script> <script src="/static/js/ui/tabs.js"></script>
<script src="/static/js/ui/dropdown.js"></script> <script src="/static/js/ui/dropdown.js"></script>
<!-- Core functionality -->
<!-- Core Application Modules --> <script src="/static/js/modules/coin-manager.js"></script>
<script src="/static/js/modules/config-manager.js"></script> <script src="/static/js/modules/config-manager.js"></script>
<script src="/static/js/modules/cache-manager.js"></script> <script src="/static/js/modules/cache-manager.js"></script>
<script src="/static/js/modules/cleanup-manager.js"></script> <script src="/static/js/modules/cleanup-manager.js"></script>
<!-- Connection & Communication Modules -->
<script src="/static/js/modules/websocket-manager.js"></script> <script src="/static/js/modules/websocket-manager.js"></script>
<script src="/static/js/modules/network-manager.js"></script> <script src="/static/js/modules/network-manager.js"></script>
<script src="/static/js/modules/api-manager.js"></script> <script src="/static/js/modules/api-manager.js"></script>
<script src="/static/js/modules/price-manager.js"></script>
<!-- UI & Interaction Modules -->
<script src="/static/js/modules/tooltips-manager.js"></script> <script src="/static/js/modules/tooltips-manager.js"></script>
<script src="/static/js/modules/notification-manager.js"></script> <script src="/static/js/modules/notification-manager.js"></script>
<script src="/static/js/modules/identity-manager.js"></script> <script src="/static/js/modules/identity-manager.js"></script>
@@ -38,8 +70,11 @@
{% if current_page == 'wallets' or current_page == 'wallet' %} {% if current_page == 'wallets' or current_page == 'wallet' %}
<script src="/static/js/modules/wallet-manager.js"></script> <script src="/static/js/modules/wallet-manager.js"></script>
{% endif %} {% endif %}
<!-- Memory management -->
<script src="/static/js/modules/memory-manager.js"></script> <script src="/static/js/modules/memory-manager.js"></script>
<!-- Main application script -->
<script src="/static/js/global.js"></script>
</head>
<script> <script>
(function() { (function() {
const isDarkMode = localStorage.getItem('color-theme') === 'dark' || const isDarkMode = localStorage.getItem('color-theme') === 'dark' ||