From 748dd388cbf0fc1a97f5d4ffc535bb028fd34c09 Mon Sep 17 00:00:00 2001 From: Gerlof van Ek Date: Thu, 10 Apr 2025 21:18:03 +0200 Subject: [PATCH] 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> --- basicswap/js_server.py | 95 +- basicswap/static/js/bids_available.js | 20 +- basicswap/static/js/modules/api-manager.js | 17 +- basicswap/static/js/modules/coin-manager.js | 230 +++ basicswap/static/js/modules/config-manager.js | 148 +- basicswap/static/js/modules/price-manager.js | 233 +++ basicswap/static/js/modules/wallet-manager.js | 153 +- .../static/js/modules/websocket-manager.js | 2 + basicswap/static/js/offers.js | 1386 ++++++++--------- basicswap/static/js/pricechart.js | 570 +++---- basicswap/static/js/swaps_in_progress.js | 46 +- basicswap/templates/footer.html | 2 +- basicswap/templates/header.html | 35 +- basicswap/templates/offers.html | 8 +- basicswap/templates/unlock.html | 67 +- 15 files changed, 1611 insertions(+), 1401 deletions(-) create mode 100644 basicswap/static/js/modules/coin-manager.js create mode 100644 basicswap/static/js/modules/price-manager.js diff --git a/basicswap/js_server.py b/basicswap/js_server.py index bc47292..af470f6 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -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: swap_client = self.server.swap_client 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 = [] - 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: + for bid_id, (bid, offer) in list(swap_client.swaps_in_progress.items()): 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 = { - "bid_id": bid_id_hex, - "offer_id": bid[3].hex(), - "created_at": bid[0], - "bid_state": bid_state, - "tx_state_a": tx_state_a if tx_state_a else "None", - "tx_state_b": tx_state_b if tx_state_b else "None", - "coin_from": swap_client.ci(bid[9]).coin_name(), + "bid_id": bid_id.hex(), + "offer_id": offer.offer_id.hex(), + "created_at": bid.created_at, + "expire_at": bid.expire_at, + "bid_state": strBidState(bid.state), + "tx_state_a": None, + "tx_state_b": None, + "coin_from": swap_client.ci(offer.coin_from).coin_name(), "coin_to": swap_client.ci(offer.coin_to).coin_name(), - "amount_from": swap_client.ci(bid[9]).format_amount(bid[4]), - "amount_to": swap_client.ci(offer.coin_to).format_amount( - (bid[4] * bid[10]) // swap_client.ci(bid[9]).COIN() + "amount_from": swap_client.ci(offer.coin_from).format_amount( + bid.amount ), - "addr_from": bid[11], - "status": { - "main": bid_state, - "initial_tx": tx_state_a if tx_state_a else "None", - "payment_tx": tx_state_b if tx_state_b else "None", - }, + "amount_to": swap_client.ci(offer.coin_to).format_amount( + bid.amount_to + ), + "addr_from": bid.bid_addr if bid.was_received else offer.addr_from, } + + 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) - processed_bid_ids.add(bid_id_hex) + except Exception: - continue + pass 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 53d5251..4cd1e03 100644 --- a/basicswap/static/js/bids_available.js +++ b/basicswap/static/js/bids_available.js @@ -1,20 +1,4 @@ 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 = { dentities: new Map(), @@ -288,8 +272,8 @@ const createBidTableRow = async (bid) => { const rate = toAmount > 0 ? toAmount / fromAmount : 0; const inverseRate = fromAmount > 0 ? fromAmount / toAmount : 0; - const fromSymbol = COIN_NAME_TO_SYMBOL[bid.coin_from] || bid.coin_from; - const toSymbol = COIN_NAME_TO_SYMBOL[bid.coin_to] || bid.coin_to; + const fromSymbol = window.CoinManager.getSymbol(bid.coin_from) || bid.coin_from; + const toSymbol = window.CoinManager.getSymbol(bid.coin_to) || bid.coin_to; const timeColor = getTimeStrokeColor(bid.expire_at); const uniqueId = `${bid.bid_id}_${bid.created_at}`; diff --git a/basicswap/static/js/modules/api-manager.js b/basicswap/static/js/modules/api-manager.js index e166593..e38440c 100644 --- a/basicswap/static/js/modules/api-manager.js +++ b/basicswap/static/js/modules/api-manager.js @@ -201,12 +201,23 @@ const ApiManager = (function() { }, fetchCoinPrices: async function(coins, source = "coingecko.com", ttl = 300) { - if (!Array.isArray(coins)) { - coins = [coins]; + if (!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', {}, { - coins: Array.isArray(coins) ? coins.join(',') : coins, + coins: coinsParam, source: source, ttl: ttl }); diff --git a/basicswap/static/js/modules/coin-manager.js b/basicswap/static/js/modules/coin-manager.js new file mode 100644 index 0000000..b4ee830 --- /dev/null +++ b/basicswap/static/js/modules/coin-manager.js @@ -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'); diff --git a/basicswap/static/js/modules/config-manager.js b/basicswap/static/js/modules/config-manager.js index 0b70751..d4df53e 100644 --- a/basicswap/static/js/modules/config-manager.js +++ b/basicswap/static/js/modules/config-manager.js @@ -17,10 +17,8 @@ const ConfigManager = (function() { cacheDuration: 10 * 60 * 1000, requestTimeout: 60000, wsPort: selectedWsPort, - cacheConfig: { defaultTTL: 10 * 60 * 1000, - ttlSettings: { prices: 5 * 60 * 1000, chart: 5 * 60 * 1000, @@ -29,17 +27,13 @@ const ConfigManager = (function() { 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', @@ -47,7 +41,6 @@ const ConfigManager = (function() { cryptoCompareHourly: 'https://min-api.cryptocompare.com/data/v2/histohour', volumeEndpoint: 'https://api.coingecko.com/api/v3/simple/price' }, - rateLimits: { coingecko: { requestsPerMinute: 50, @@ -58,86 +51,23 @@ const ConfigManager = (function() { 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: 'NMC', name: 'namecoin', 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', - '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' - } + get coins() { + return window.CoinManager ? window.CoinManager.getAllCoins() : [ + { symbol: 'BTC', name: 'bitcoin', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'XMR', name: 'monero', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'PART', name: 'particl', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'BCH', name: 'bitcoincash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'PIVX', name: 'pivx', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'FIRO', name: 'firo', displayName: 'Firo', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'DASH', name: 'dash', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'LTC', name: 'litecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'DOGE', name: 'dogecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'DCR', name: 'decred', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'NMC', name: 'namecoin', usesCryptoCompare: true, usesCoinGecko: true, historicalDays: 30 }, + { symbol: 'WOW', name: 'wownero', usesCryptoCompare: false, usesCoinGecko: true, historicalDays: 30 } + ]; }, - chartConfig: { colors: { default: { @@ -158,28 +88,22 @@ const ConfigManager = (function() { 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(); @@ -188,16 +112,16 @@ const ConfigManager = (function() { coinGecko: apiKeys.coinGecko || '' }; } - return { cryptoCompare: '', coinGecko: '' }; }, - getCoinBackendId: function(coinName) { if (!coinName) return null; - + if (window.CoinManager) { + return window.CoinManager.getPriceKey(coinName); + } const nameMap = { 'bitcoin-cash': 'bitcoincash', 'bitcoin cash': 'bitcoincash', @@ -205,70 +129,57 @@ const ConfigManager = (function() { '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; - + if (window.CoinManager) { + return window.CoinManager.coinMatches(offerCoin, filterCoin); + } offerCoin = offerCoin.toLowerCase(); filterCoin = filterCoin.toLowerCase(); - if (offerCoin === filterCoin) return true; - if ((offerCoin === 'firo' || offerCoin === 'zcoin') && (filterCoin === 'firo' || filterCoin === 'zcoin')) { return true; } - if ((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'); @@ -290,7 +201,6 @@ const ConfigManager = (function() { return '0'; } }, - formatDate: function(timestamp, resolution) { const date = new Date(timestamp); const options = { @@ -300,7 +210,6 @@ const ConfigManager = (function() { }; return date.toLocaleString('en-US', { ...options[resolution], timeZone: 'UTC' }); }, - debounce: function(func, delay) { let timeoutId; return function(...args) { @@ -308,17 +217,14 @@ const ConfigManager = (function() { 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`; @@ -333,10 +239,8 @@ const ConfigManager = (function() { } 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); @@ -349,7 +253,6 @@ const ConfigManager = (function() { .replace(/"/g, """) .replace(/'/g, "'"); }, - formatPrice: function(coin, price) { if (typeof price !== 'number' || isNaN(price)) { console.warn(`Invalid price for ${coin}:`, price); @@ -363,7 +266,6 @@ const ConfigManager = (function() { if (price < 100000) return price.toFixed(1); return price.toFixed(0); }, - getEmptyPriceData: function() { return { 'bitcoin': { usd: null, btc: null }, @@ -381,12 +283,13 @@ const ConfigManager = (function() { 'firo': { usd: null, btc: null } }; }, - getCoinSymbol: function(fullName) { - return publicAPI.coinMappings?.nameToSymbol[fullName] || fullName; + if (window.CoinManager) { + return window.CoinManager.getSymbol(fullName) || fullName; + } + return fullName; } }; - return publicAPI; })(); @@ -415,5 +318,4 @@ 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/price-manager.js b/basicswap/static/js/modules/price-manager.js new file mode 100644 index 0000000..f01739e --- /dev/null +++ b/basicswap/static/js/modules/price-manager.js @@ -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'); diff --git a/basicswap/static/js/modules/wallet-manager.js b/basicswap/static/js/modules/wallet-manager.js index 6b4b775..93512e5 100644 --- a/basicswap/static/js/modules/wallet-manager.js +++ b/basicswap/static/js/modules/wallet-manager.js @@ -23,54 +23,6 @@ const WalletManager = (function() { 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 = { lastFetchTime: 0, toggleInProgress: false, @@ -83,7 +35,23 @@ const WalletManager = (function() { }; 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) { @@ -105,16 +73,33 @@ const WalletManager = (function() { 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 coinsToFetch = []; + const processedCoins = new Set(); + + 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", { method: "POST", headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ - coins: coinsToFetch, + coins: fetchCoinsString, source: currentSource, ttl: config.defaultTTL }) @@ -127,46 +112,27 @@ const WalletManager = (function() { 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) - }; - } + document.querySelectorAll('.coinname-value').forEach(el => { + const coinName = el.getAttribute('data-coinname'); + if (!coinName) return; + + const adjustedName = coinName === 'Zcoin' ? 'Firo' : + 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, + 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); state.lastFetchTime = now; return processedData; @@ -202,7 +168,6 @@ const WalletManager = (function() { 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'); @@ -210,10 +175,10 @@ const WalletManager = (function() { if (coinName) { 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); - if (coinId) { + if (coinSymbol) { if (coinName === 'Particl') { const isBlind = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Blind'); const isAnon = el.closest('.flex')?.querySelector('h4')?.textContent?.includes('Anon'); @@ -224,7 +189,7 @@ const WalletManager = (function() { const balanceType = isMWEB ? 'mweb' : 'public'; localStorage.setItem(`litecoin-${balanceType}-amount`, amount.toString()); } else { - localStorage.setItem(`${coinId.toLowerCase()}-amount`, amount.toString()); + localStorage.setItem(`${coinSymbol.toLowerCase()}-amount`, amount.toString()); } el.setAttribute('data-original-value', `${amount} ${shortName}`); diff --git a/basicswap/static/js/modules/websocket-manager.js b/basicswap/static/js/modules/websocket-manager.js index e823821..d85996a 100644 --- a/basicswap/static/js/modules/websocket-manager.js +++ b/basicswap/static/js/modules/websocket-manager.js @@ -168,6 +168,8 @@ const WebSocketManager = (function() { } state.isConnecting = false; + state.messageHandlers = {}; + if (ws) { ws.onopen = null; diff --git a/basicswap/static/js/offers.js b/basicswap/static/js/offers.js index 3476c87..0993312 100644 --- a/basicswap/static/js/offers.js +++ b/basicswap/static/js/offers.js @@ -6,6 +6,7 @@ let originalJsonData = []; let currentSortColumn = 0; let currentSortDirection = 'desc'; let filterTimeout = null; +let isPaginationInProgress = false; const isSentOffers = window.offersTableConfig.isSentOffers; const CACHE_DURATION = window.config.cacheConfig.defaultTTL; @@ -22,23 +23,6 @@ const lastRefreshTimeSpan = document.getElementById('lastRefreshTime'); const newEntriesCountSpan = document.getElementById('newEntriesCount'); window.tableRateModule = { - coinNameToSymbol: { - 'Bitcoin': 'BTC', - 'Particl': 'PART', - 'Particl Blind': 'PART', - 'Particl Anon': 'PART', - 'Monero': 'XMR', - 'Wownero': 'WOW', - 'Litecoin': 'LTC', - 'Firo': 'FIRO', - 'Dash': 'DASH', - 'PIVX': 'PIVX', - 'Decred': 'DCR', - 'Namecoin': 'NMC', - 'Zano': 'ZANO', - 'Bitcoin Cash': 'BCH', - 'Dogecoin': 'DOGE' - }, cache: {}, processedOffers: new Set(), @@ -102,8 +86,10 @@ window.tableRateModule = { }, getFallbackValue(coinSymbol) { - const value = localStorage.getItem(`fallback_${coinSymbol}_usd`); - return value ? parseFloat(value) : null; + if (!coinSymbol) return null; + const normalizedSymbol = coinSymbol.toLowerCase() === 'part' ? 'particl' : coinSymbol.toLowerCase(); + const cachedValue = this.getCachedValue(`fallback_${normalizedSymbol}_usd`); + return cachedValue; }, initializeTable() { @@ -191,143 +177,237 @@ function saveFilterSettings() { })); } +function coinMatches(offerCoin, filterCoin) { + if (!offerCoin || !filterCoin || filterCoin === 'any') return true; + + 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; +} + function filterAndSortData() { - const formData = new FormData(filterForm); - const filters = Object.fromEntries(formData); + const formData = new FormData(filterForm); + const filters = Object.fromEntries(formData); - saveFilterSettings(); + saveFilterSettings(); - if (filters.coin_to !== 'any') { - filters.coin_to = window.config.coinMappings.idToName[filters.coin_to] || filters.coin_to; - } - if (filters.coin_from !== 'any') { - filters.coin_from = window.config.coinMappings.idToName[filters.coin_from] || filters.coin_from; - } + let filteredData = [...originalJsonData]; - let filteredData = [...originalJsonData]; + const sentFromFilter = filters.sent_from || 'any'; + filteredData = filteredData.filter(offer => { + const isMatch = sentFromFilter === 'public' ? offer.is_public : + sentFromFilter === 'private' ? !offer.is_public : + true; + return isMatch; + }); - const sentFromFilter = filters.sent_from || 'any'; - filteredData = filteredData.filter(offer => { - if (sentFromFilter === 'public') return offer.is_public; - if (sentFromFilter === 'private') return !offer.is_public; - return true; - }); + filteredData = filteredData.filter(offer => { + if (!isSentOffers && isOfferExpired(offer)) { + return false; + } - filteredData = filteredData.filter(offer => { - if (!isSentOffers && isOfferExpired(offer)) return false; + if (filters.coin_to && filters.coin_to !== 'any') { + const coinToSelect = document.getElementById('coin_to'); + const selectedOption = coinToSelect?.querySelector(`option[value="${filters.coin_to}"]`); + const coinName = selectedOption?.textContent.trim(); + + if (coinName && !coinMatches(offer.coin_to, coinName)) { + return false; + } + } - const coinFrom = (offer.coin_from || '').toLowerCase(); - const coinTo = (offer.coin_to || '').toLowerCase(); + if (filters.coin_from && filters.coin_from !== 'any') { + const coinFromSelect = document.getElementById('coin_from'); + const selectedOption = coinFromSelect?.querySelector(`option[value="${filters.coin_from}"]`); + const coinName = selectedOption?.textContent.trim(); - if (filters.coin_to !== 'any' && !coinMatches(coinTo, filters.coin_to)) return false; - if (filters.coin_from !== 'any' && !coinMatches(coinFrom, filters.coin_from)) return false; + if (coinName && !coinMatches(offer.coin_from, coinName)) { + return false; + } + } - if (isSentOffers && filters.status && filters.status !== 'any') { - const isExpired = offer.expire_at <= Math.floor(Date.now() / 1000); - const isRevoked = Boolean(offer.is_revoked); + if (isSentOffers && filters.status && filters.status !== 'any') { + const isExpired = offer.expire_at <= Math.floor(Date.now() / 1000); + const isRevoked = Boolean(offer.is_revoked); - switch (filters.status) { - case 'active': return !isExpired && !isRevoked; - case 'expired': return isExpired && !isRevoked; - case 'revoked': return isRevoked; - } - } - return true; - }); + let statusMatch = false; + switch (filters.status) { + case 'active': + statusMatch = !isExpired && !isRevoked; + break; + case 'expired': + statusMatch = isExpired && !isRevoked; + break; + case 'revoked': + statusMatch = isRevoked; + break; + } - if (currentSortColumn !== null) { - const priceCache = new Map(); - const getPrice = coin => { - if (priceCache.has(coin)) return priceCache.get(coin); - const symbol = coin === 'Firo' || coin === 'Zcoin' ? 'zcoin' : - coin === 'Bitcoin Cash' ? 'bitcoin-cash' : - coin.includes('Particl') ? 'particl' : - coin.toLowerCase(); - const price = latestPrices[symbol]?.usd || 0; - priceCache.set(coin, price); - return price; - }; + if (!statusMatch) { + return false; + } + } - const calculateValue = offer => { - const fromUSD = parseFloat(offer.amount_from) * getPrice(offer.coin_from); - const toUSD = parseFloat(offer.amount_to) * getPrice(offer.coin_to); - return (isSentOffers || offer.is_own_offer) ? - ((toUSD / fromUSD) - 1) * 100 : - ((fromUSD / toUSD) - 1) * 100; - }; + return true; + }); - const sortValues = new Map(); - if (currentSortColumn === 5 || currentSortColumn === 6) { - filteredData.forEach(offer => { - sortValues.set(offer.offer_id, calculateValue(offer)); - }); - } + if (currentSortColumn === 7) { + const offersWithPercentages = []; + + for (const offer of filteredData) { + const fromAmount = parseFloat(offer.amount_from) || 0; + const toAmount = parseFloat(offer.amount_to) || 0; + const coinFrom = offer.coin_from; + const coinTo = offer.coin_to; - filteredData.sort((a, b) => { - let comparison; - switch(currentSortColumn) { - case 0: - comparison = a.created_at - b.created_at; - break; - case 5: - case 6: - comparison = sortValues.get(a.offer_id) - sortValues.get(b.offer_id); - break; - case 7: - comparison = a.offer_id.localeCompare(b.offer_id); - break; - default: - comparison = 0; - } - return currentSortDirection === 'desc' ? -comparison : comparison; - }); - } + const fromSymbol = getPriceKey(coinFrom); + const toSymbol = getPriceKey(coinTo); - return filteredData; + const fromPriceUSD = latestPrices && fromSymbol ? latestPrices[fromSymbol]?.usd : null; + const toPriceUSD = latestPrices && toSymbol ? latestPrices[toSymbol]?.usd : null; + + let percentDiff = null; + + if (fromPriceUSD && toPriceUSD && !isNaN(fromPriceUSD) && !isNaN(toPriceUSD)) { + const fromValueUSD = fromAmount * fromPriceUSD; + const toValueUSD = toAmount * toPriceUSD; + + if (fromValueUSD && toValueUSD) { + if (offer.is_own_offer || isSentOffers) { + percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100; + } else { + percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100; + } + } + } + + offersWithPercentages.push({ + offer: offer, + percentDiff: percentDiff + }); + } + + const validOffers = offersWithPercentages.filter(item => item.percentDiff !== null); + const nullOffers = offersWithPercentages.filter(item => item.percentDiff === null); + + validOffers.sort((a, b) => { + return currentSortDirection === 'asc' ? a.percentDiff - b.percentDiff : b.percentDiff - a.percentDiff; + }); + + filteredData = [...validOffers.map(item => item.offer), ...nullOffers.map(item => item.offer)]; + } else if (currentSortColumn !== null && currentSortDirection !== null) { + filteredData.sort((a, b) => { + let aValue, bValue; + + switch(currentSortColumn) { + case 0: + aValue = a.created_at; + bValue = b.created_at; + break; + case 1: + aValue = a.addr_from || ''; + bValue = b.addr_from || ''; + break; + case 2: + aValue = parseFloat(a.amount_to) || 0; + bValue = parseFloat(b.amount_to) || 0; + break; + case 3: + aValue = a.coin_from + a.coin_to; + bValue = b.coin_from + b.coin_to; + break; + case 4: + aValue = parseFloat(a.amount_from) || 0; + bValue = parseFloat(b.amount_from) || 0; + break; + case 5: + aValue = parseFloat(a.rate) || 0; + bValue = parseFloat(b.rate) || 0; + break; + case 6: + const aSymbol = getPriceKey(a.coin_to); + const bSymbol = getPriceKey(b.coin_to); + const aRate = parseFloat(a.rate) || 0; + const bRate = parseFloat(b.rate) || 0; + const aPriceUSD = latestPrices && aSymbol ? latestPrices[aSymbol]?.usd : null; + const bPriceUSD = latestPrices && bSymbol ? latestPrices[bSymbol]?.usd : null; + + aValue = aPriceUSD && !isNaN(aPriceUSD) ? aRate * aPriceUSD : 0; + bValue = bPriceUSD && !isNaN(bPriceUSD) ? bRate * bPriceUSD : 0; + break; + default: + aValue = a.created_at; + bValue = b.created_at; + } + + if (typeof aValue === 'number' && typeof bValue === 'number') { + return currentSortDirection === 'asc' ? aValue - bValue : bValue - aValue; + } + + if (typeof aValue === 'string' && typeof bValue === 'string') { + return currentSortDirection === 'asc' + ? aValue.localeCompare(bValue) + : bValue.localeCompare(aValue); + } + + return currentSortDirection === 'asc' ? (aValue > bValue ? 1 : -1) : (bValue > aValue ? 1 : -1); + }); + } + + return filteredData; } async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) { return new Promise((resolve) => { - if (!latestPrices) { - console.error('Latest prices not available. Unable to calculate profit/loss.'); + if (!fromCoin || !toCoin) { resolve(null); return; } - const getPriceKey = (coin) => { - const lowerCoin = coin.toLowerCase(); - const symbolToName = { - 'btc': 'bitcoin', - 'xmr': 'monero', - 'part': 'particl', - 'bch': 'bitcoin-cash', - 'pivx': 'pivx', - 'firo': 'firo', - 'dash': 'dash', - 'ltc': 'litecoin', - 'doge': 'dogecoin', - 'dcr': 'decred', - 'nmc': 'namecoin', - 'wow': 'wownero' - }; + const getPriceForCoin = (coin) => { + if (!coin) return null; - if (lowerCoin === 'zcoin') return 'firo'; - if (lowerCoin === 'bitcoin cash') return 'bitcoin-cash'; - if (lowerCoin === 'particl anon' || lowerCoin === 'particl blind') return 'particl'; + let normalizedCoin = coin.toLowerCase(); - return symbolToName[lowerCoin] || lowerCoin; + if (window.CoinManager) { + normalizedCoin = window.CoinManager.getPriceKey(coin) || normalizedCoin; + } else { + if (normalizedCoin === 'zcoin') normalizedCoin = 'firo'; + if (normalizedCoin === 'bitcoincash' || normalizedCoin === 'bitcoin cash') + normalizedCoin = 'bitcoin-cash'; + if (normalizedCoin.includes('particl')) normalizedCoin = 'particl'; + } + let price = null; + if (latestPrices && latestPrices[normalizedCoin]) { + price = latestPrices[normalizedCoin].usd; + } + return price; }; - const fromSymbol = getPriceKey(fromCoin); - const toSymbol = getPriceKey(toCoin); - - 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); - } + const fromPriceUSD = getPriceForCoin(fromCoin); + const toPriceUSD = getPriceForCoin(toCoin); if (!fromPriceUSD || !toPriceUSD || isNaN(fromPriceUSD) || isNaN(toPriceUSD)) { resolve(null); @@ -363,129 +443,14 @@ function 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(); - } - - const PRICES_CACHE_KEY = 'prices_coingecko'; - const minRequestInterval = 60000; - const currentTime = Date.now(); - - if (!window.isManualRefresh) { - const lastRequestTime = window.lastPriceRequest || 0; - if (currentTime - lastRequestTime < minRequestInterval) { - const cachedData = CacheManager.get(PRICES_CACHE_KEY); - if (cachedData) { - return cachedData.value; - } - } - } - window.lastPriceRequest = currentTime; - - if (!window.isManualRefresh) { - const cachedData = CacheManager.get(PRICES_CACHE_KEY); - if (cachedData && cachedData.remainingTime > 60000) { - latestPrices = cachedData.value; - Object.entries(cachedData.value).forEach(([coin, prices]) => { - if (prices.usd) { - tableRateModule.setFallbackValue(coin, prices.usd); - } - }); - return cachedData.value; - } - } - try { - const existingCache = CacheManager.get(PRICES_CACHE_KEY); - const fallbackData = existingCache ? existingCache.value : null; - const coinIds = [ - 'bitcoin', 'particl', 'monero', 'litecoin', - 'dogecoin', 'firo', 'dash', 'pivx', - 'decred', 'namecoin', '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) - }; - }); - } - - try { - const wowResponse = await Api.fetchCoinPrices("wownero"); - - 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); - } - - 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)); - } - } - } - - if (fallbackData) { - return fallbackData; - } - - const fallbackPrices = {}; - Object.keys(getEmptyPriceData()).forEach(coin => { - const fallbackValue = tableRateModule.getFallbackValue(coin); - if (fallbackValue !== null) { - fallbackPrices[coin] = { usd: fallbackValue, btc: null }; - } - }); - - if (Object.keys(fallbackPrices).length > 0) { - return fallbackPrices; - } - - return getEmptyPriceData(); - } catch (error) { - console.error('Unexpected error in fetchLatestPrices:', error); - NetworkManager.handleNetworkError(error); - return getEmptyPriceData(); - } finally { + const prices = await window.PriceManager.getPrices(window.isManualRefresh); window.isManualRefresh = false; + return prices; + } catch (error) { + console.error('Error fetching prices:', error); + return getEmptyPriceData(); } } @@ -652,20 +617,26 @@ function updatePaginationInfo() { const validOffers = getValidOffers(); const totalItems = validOffers.length; const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage)); + const previousPage = currentPage; - currentPage = Math.max(1, Math.min(currentPage, totalPages)); + if (previousPage !== currentPage) { + debugPaginationChange('updatePaginationInfo', previousPage, currentPage); + } - currentPageSpan.textContent = currentPage; - totalPagesSpan.textContent = totalPages; + if (currentPageSpan) currentPageSpan.textContent = totalPages > 0 ? currentPage : 0; + if (totalPagesSpan) totalPagesSpan.textContent = totalPages; const showPrev = currentPage > 1; - const showNext = currentPage < totalPages && totalItems > 0; + const showNext = currentPage < totalPages; - prevPageButton.style.display = showPrev ? 'inline-flex' : 'none'; - nextPageButton.style.display = showNext ? 'inline-flex' : 'none'; + if (prevPageButton) { + prevPageButton.style.display = showPrev ? 'inline-flex' : 'none'; + prevPageButton.disabled = !showPrev; + } - if (lastRefreshTime) { - lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString(); + if (nextPageButton) { + nextPageButton.style.display = showNext ? 'inline-flex' : 'none'; + nextPageButton.disabled = !showNext; } if (newEntriesCountSpan) { @@ -721,181 +692,6 @@ function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffe } }) .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) { - 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'; }); @@ -944,36 +740,39 @@ function updateClearFiltersButton() { } function cleanupRow(row) { - if (!row) return; + if (!row) return; - const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]'); - tooltipTriggers.forEach(trigger => { - if (window.TooltipManager) { - window.TooltipManager.destroy(trigger); - } - }); - - CleanupManager.removeListenersByElement(row); - - row.removeAttribute('data-offer-id'); - - while (row.firstChild) { - const child = row.firstChild; - row.removeChild(child); + const tooltipTriggers = row.querySelectorAll('[data-tooltip-trigger-id]'); + tooltipTriggers.forEach(trigger => { + if (window.TooltipManager) { + window.TooltipManager.destroy(trigger); } + }); + + CleanupManager.removeListenersByElement(row); + + while (row.attributes && row.attributes.length > 0) { + row.removeAttribute(row.attributes[0].name); + } + + while (row.firstChild) { + const child = row.firstChild; + row.removeChild(child); + } } function cleanupTable() { - if (!offersBody) return; + if (!offersBody) return; - const existingRows = offersBody.querySelectorAll('tr'); - existingRows.forEach(row => cleanupRow(row)); + const existingRows = Array.from(offersBody.querySelectorAll('tr')); - offersBody.innerHTML = ''; + existingRows.forEach(row => cleanupRow(row)); - if (window.TooltipManager) { - window.TooltipManager.cleanup(); - } + offersBody.innerHTML = ''; + + if (window.TooltipManager) { + window.TooltipManager.cleanup(); + } } function handleNoOffersScenario() { @@ -1012,8 +811,14 @@ function handleNoOffersScenario() { } } -async function updateOffersTable() { +async function updateOffersTable(options = {}) { try { + + if (isPaginationInProgress && !options.fromPaginationClick) { + console.log('Skipping table update during pagination operation'); + return; + } + if (window.TooltipManager) { window.TooltipManager.cleanup(); } @@ -1024,6 +829,16 @@ async function updateOffersTable() { return; } + const totalPages = Math.ceil(validOffers.length / itemsPerPage); + + if (!options.fromPaginationClick) { + const oldPage = currentPage; + currentPage = Math.max(1, Math.min(currentPage, totalPages)); + if (oldPage !== currentPage) { + debugPaginationChange('updateOffersTable auto-adjust', oldPage, currentPage); + } + } + const startIndex = (currentPage - 1) * itemsPerPage; const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length); const itemsToDisplay = validOffers.slice(startIndex, endIndex); @@ -1061,7 +876,7 @@ async function updateOffersTable() { requestAnimationFrame(() => { updateRowTimes(); - updatePaginationControls(Math.ceil(validOffers.length / itemsPerPage)); + updatePaginationInfo(); updateProfitLossDisplays(); if (tableRateModule?.initializeTable) { tableRateModule.initializeTable(); @@ -1164,13 +979,30 @@ function createTableRow(offer, identity = null) { is_public: isPublic } = offer; - 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); + let coinFromSymbol, coinToSymbol; + + if (window.CoinManager) { + coinFromSymbol = window.CoinManager.getSymbol(coinFrom) || coinFrom.toLowerCase(); + coinToSymbol = window.CoinManager.getSymbol(coinTo) || coinTo.toLowerCase(); + } else { + coinFromSymbol = coinFrom.toLowerCase(); + coinToSymbol = coinTo.toLowerCase(); + } + + let coinFromDisplay, coinToDisplay; + + if (window.CoinManager) { + coinFromDisplay = window.CoinManager.getDisplayName(coinFrom) || coinFrom; + coinToDisplay = window.CoinManager.getDisplayName(coinTo) || coinTo; + } else { + coinFromDisplay = coinFrom; + coinToDisplay = coinTo; + if (coinFromDisplay.toLowerCase() === 'zcoin') coinFromDisplay = 'Firo'; + if (coinToDisplay.toLowerCase() === 'zcoin') coinToDisplay = 'Firo'; + } + const postedTime = formatTime(createdAt, true); const expiresIn = formatTime(expireAt); - const currentTime = Math.floor(Date.now() / 1000); const isActuallyExpired = currentTime > expireAt; const fromAmount = parseFloat(amountFrom) || 0; @@ -1354,31 +1186,6 @@ function createRateColumn(offer, coinFrom, coinTo) { const rate = parseFloat(offer.rate) || 0; const inverseRate = rate ? (1 / rate) : 0; - const getPriceKey = (coin) => { - const lowerCoin = coin.toLowerCase(); - - const symbolToName = { - 'btc': 'bitcoin', - 'xmr': 'monero', - 'part': 'particl', - 'bch': 'bitcoin-cash', - 'pivx': 'pivx', - 'firo': 'firo', - 'dash': 'dash', - 'ltc': 'litecoin', - 'doge': 'dogecoin', - 'dcr': 'decred', - 'nmc': 'namecoin', - '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 && latestPrices[toSymbolKey] ? latestPrices[toSymbolKey].usd : null; @@ -1646,42 +1453,58 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou toAmount = parseFloat(toAmount) || 0; const getPriceKey = (coin) => { + if (!coin) return null; + const lowerCoin = coin.toLowerCase(); - const symbolToName = { - 'btc': 'bitcoin', - 'xmr': 'monero', - 'part': 'particl', - 'bch': 'bitcoin-cash', - 'pivx': 'pivx', - 'firo': 'firo', - 'dash': 'dash', - 'ltc': 'litecoin', - 'doge': 'dogecoin', - 'dcr': 'decred', - 'nmc': 'namecoin', - '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 (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') { + + if (latestPrices && latestPrices['bitcoin-cash']) { + return 'bitcoin-cash'; + } else if (latestPrices && latestPrices['bch']) { + return 'bch'; + } + return 'bitcoin-cash'; + } + + if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) { + return 'part'; + } + + if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) { + const symbol = window.config.coinMappings.nameToSymbol[coin]; + if (symbol) { + if (symbol.toUpperCase() === 'BCH') { + if (latestPrices && latestPrices['bitcoin-cash']) { + return 'bitcoin-cash'; + } else if (latestPrices && latestPrices['bch']) { + return 'bch'; + } + return 'bitcoin-cash'; + } + + if (symbol.toUpperCase() === 'PART') { + return 'part'; + } + + return symbol.toLowerCase(); + } + } + return lowerCoin; }; - if (latestPrices && latestPrices['firo'] && !latestPrices['zcoin']) { - latestPrices['zcoin'] = JSON.parse(JSON.stringify(latestPrices['firo'])); - } - const fromSymbol = getPriceKey(coinFrom); const toSymbol = getPriceKey(coinTo); - let fromPriceUSD = latestPrices && latestPrices[fromSymbol] ? latestPrices[fromSymbol].usd : null; - let toPriceUSD = latestPrices && latestPrices[toSymbol] ? latestPrices[toSymbol].usd : null; + let fromPriceUSD = latestPrices && fromSymbol ? latestPrices[fromSymbol]?.usd : null; + let toPriceUSD = latestPrices && toSymbol ? latestPrices[toSymbol]?.usd : null; - if (!fromPriceUSD || !toPriceUSD) { + if (!fromPriceUSD && window.tableRateModule) { fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol); + } + if (!toPriceUSD && window.tableRateModule) { toPriceUSD = tableRateModule.getFallbackValue(toSymbol); } @@ -1749,42 +1572,57 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) { const inverseRate = rate ? (1 / rate) : 0; const getPriceKey = (coin) => { + if (!coin) return null; + const lowerCoin = coin.toLowerCase(); - const symbolToName = { - 'btc': 'bitcoin', - 'xmr': 'monero', - 'part': 'particl', - 'bch': 'bitcoin-cash', - 'pivx': 'pivx', - 'firo': 'firo', - 'dash': 'dash', - 'ltc': 'litecoin', - 'doge': 'dogecoin', - 'dcr': 'decred', - 'nmc': 'namecoin', - '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 (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') { + if (latestPrices && latestPrices['bitcoin-cash']) { + return 'bitcoin-cash'; + } else if (latestPrices && latestPrices['bch']) { + return 'bch'; + } + + return 'bitcoin-cash'; + } + + if (lowerCoin === 'part' || lowerCoin === 'particl' || lowerCoin.includes('particl')) { + return 'part'; + } + + if (window.config && window.config.coinMappings && window.config.coinMappings.nameToSymbol) { + const symbol = window.config.coinMappings.nameToSymbol[coin]; + if (symbol) { + if (symbol.toUpperCase() === 'BCH') { + + if (latestPrices && latestPrices['bitcoin-cash']) { + return 'bitcoin-cash'; + } else if (latestPrices && latestPrices['bch']) { + return 'bch'; + } + return 'bitcoin-cash'; + } + if (symbol.toUpperCase() === 'PART') { + return 'part'; + } + return symbol.toLowerCase(); + } + } + return lowerCoin; }; - if (latestPrices && latestPrices['firo'] && !latestPrices['zcoin']) { - latestPrices['zcoin'] = JSON.parse(JSON.stringify(latestPrices['firo'])); - } - const fromSymbol = getPriceKey(coinFrom); const toSymbol = getPriceKey(coinTo); - let fromPriceUSD = latestPrices && latestPrices[fromSymbol] ? latestPrices[fromSymbol].usd : null; - let toPriceUSD = latestPrices && latestPrices[toSymbol] ? latestPrices[toSymbol].usd : null; + let fromPriceUSD = latestPrices && fromSymbol ? latestPrices[fromSymbol]?.usd : null; + let toPriceUSD = latestPrices && toSymbol ? latestPrices[toSymbol]?.usd : null; - if (!fromPriceUSD || !toPriceUSD) { + if (!fromPriceUSD && window.tableRateModule) { fromPriceUSD = tableRateModule.getFallbackValue(fromSymbol); + } + if (!toPriceUSD && window.tableRateModule) { toPriceUSD = tableRateModule.getFallbackValue(toSymbol); } @@ -1807,7 +1645,7 @@ function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) { const percentDiff = marketRate ? ((rate - marketRate) / marketRate) * 100 : 0; const formattedPercentDiff = percentDiff.toFixed(2); const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" : - (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); + (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff); const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at"; const action = treatAsSentOffer ? "selling" : "buying"; @@ -1853,9 +1691,9 @@ function applyFilters() { try { filterTimeout = setTimeout(() => { + currentPage = 1; jsonData = filterAndSortData(); updateOffersTable(); - updatePaginationInfo(); updateClearFiltersButton(); filterTimeout = null; }, 250); @@ -1909,6 +1747,9 @@ function formatTimeLeft(timestamp) { } function getDisplayName(coinName) { + if (window.CoinManager) { + return window.CoinManager.getDisplayName(coinName) || coinName; + } if (coinName.toLowerCase() === 'zcoin') { return 'Firo'; } @@ -1929,6 +1770,9 @@ function getCoinSymbolLowercase(coin) { } function coinMatches(offerCoin, filterCoin) { + if (window.CoinManager) { + return window.CoinManager.coinMatches(offerCoin, filterCoin); + } return window.config.coinMatches(offerCoin, filterCoin); } @@ -1957,7 +1801,38 @@ function escapeHtml(unsafe) { return window.config.utils.escapeHtml(unsafe); } +function getPriceKey(coin) { + + if (window.CoinManager) { + return window.CoinManager.getPriceKey(coin); + } + + if (!coin) return null; + + const lowerCoin = coin.toLowerCase(); + + if (lowerCoin === 'zcoin') { + return 'firo'; + } + + if (lowerCoin === 'bitcoin cash' || lowerCoin === 'bitcoincash' || lowerCoin === 'bch') { + return 'bitcoin-cash'; + } + + if (lowerCoin === 'part' || lowerCoin === 'particl' || + lowerCoin.includes('particl')) { + return 'particl'; + } + + return lowerCoin; +} + function getCoinSymbol(fullName) { + + if (window.CoinManager) { + return window.CoinManager.getSymbol(fullName) || fullName; + } + return window.config.coinMappings.nameToSymbol[fullName] || fullName; } @@ -1977,6 +1852,8 @@ function initializeTableEvents() { const coinToSelect = document.getElementById('coin_to'); const coinFromSelect = document.getElementById('coin_from'); + const statusSelect = document.getElementById('status'); + const sentFromSelect = document.getElementById('sent_from'); if (coinToSelect) { CleanupManager.addListener(coinToSelect, 'change', () => { @@ -1992,6 +1869,18 @@ function initializeTableEvents() { }); } + if (statusSelect) { + CleanupManager.addListener(statusSelect, 'change', () => { + applyFilters(); + }); + } + + if (sentFromSelect) { + CleanupManager.addListener(sentFromSelect, 'change', () => { + applyFilters(); + }); + } + const clearFiltersBtn = document.getElementById('clearFilters'); if (clearFiltersBtn) { CleanupManager.addListener(clearFiltersBtn, 'click', () => { @@ -2103,46 +1992,70 @@ function initializeTableEvents() { }); } - document.querySelectorAll('th[data-sortable="true"]').forEach(header => { - CleanupManager.addListener(header, 'click', async () => { - const columnIndex = parseInt(header.getAttribute('data-column-index')); - handleTableSort(columnIndex, header); - }); - }); - const prevPageButton = document.getElementById('prevPage'); const nextPageButton = document.getElementById('nextPage'); if (prevPageButton) { - CleanupManager.addListener(prevPageButton, 'click', () => { - if (currentPage > 1) { - currentPage--; - updateOffersTable(); + CleanupManager.addListener(prevPageButton, 'click', async () => { + if (currentPage > 1 && !isPaginationInProgress) { + try { + isPaginationInProgress = true; + const oldPage = currentPage; + currentPage--; + await updateOffersTable({ fromPaginationClick: true }); + updatePaginationInfo(); + } finally { + setTimeout(() => { + isPaginationInProgress = false; + }, 100); + } } }); } if (nextPageButton) { - CleanupManager.addListener(nextPageButton, 'click', () => { - const totalPages = Math.ceil(jsonData.length / itemsPerPage); - if (currentPage < totalPages) { - currentPage++; - updateOffersTable(); + CleanupManager.addListener(nextPageButton, 'click', async () => { + const validOffers = getValidOffers(); + const totalPages = Math.ceil(validOffers.length / itemsPerPage); + if (currentPage < totalPages && !isPaginationInProgress) { + try { + isPaginationInProgress = true; + const oldPage = currentPage; + currentPage++; + await updateOffersTable({ fromPaginationClick: true }); + updatePaginationInfo(); + } finally { + setTimeout(() => { + isPaginationInProgress = false; + }, 100); + } } }); } + + document.querySelectorAll('th[data-sortable="true"]').forEach(header => { + CleanupManager.addListener(header, 'click', () => { + const columnIndex = parseInt(header.getAttribute('data-column-index')); + handleTableSort(columnIndex, header); + }); + }); } function handleTableSort(columnIndex, header) { if (currentSortColumn === columnIndex) { - currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; + if (currentSortDirection === null) { + currentSortDirection = 'desc'; + } else if (currentSortDirection === 'desc') { + currentSortDirection = 'asc'; + } else if (currentSortDirection === 'asc') { + currentSortColumn = null; + currentSortDirection = null; + } } else { currentSortColumn = columnIndex; currentSortDirection = 'desc'; } - saveFilterSettings(); - document.querySelectorAll('th[data-sortable="true"]').forEach(th => { const columnSpan = th.querySelector('span:not(.sort-icon)'); const icon = th.querySelector('.sort-icon'); @@ -2156,7 +2069,18 @@ function handleTableSort(columnIndex, header) { if (icon) { icon.classList.remove('text-gray-600', 'dark:text-gray-400'); icon.classList.add('text-blue-500', 'dark:text-blue-500'); - icon.textContent = currentSortDirection === 'asc' ? '↑' : '↓'; + + if (currentSortDirection === 'desc') { + icon.textContent = '↓'; + } else if (currentSortDirection === 'asc') { + icon.textContent = '↑'; + } else { + icon.textContent = '↓'; + columnSpan.classList.remove('text-blue-500', 'dark:text-blue-500'); + columnSpan.classList.add('text-gray-600', 'dark:text-gray-300'); + icon.classList.remove('text-blue-500', 'dark:text-blue-500'); + icon.classList.add('text-gray-600', 'dark:text-gray-400'); + } } } else { if (columnSpan) { @@ -2171,6 +2095,8 @@ function handleTableSort(columnIndex, header) { } }); + saveFilterSettings(); + if (window.sortTimeout) { clearTimeout(window.sortTimeout); } @@ -2230,164 +2156,226 @@ function updateSortIndicators() { } } -document.addEventListener('DOMContentLoaded', async () => { - if (window.NetworkManager && !window.networkManagerInitialized) { - NetworkManager.initialize({ - connectionTestEndpoint: '/json', - connectionTestTimeout: 3000, - reconnectDelay: 5000, - maxReconnectAttempts: 5 - }); - window.networkManagerInitialized = true; - } +document.addEventListener('DOMContentLoaded', async function() { + try { + if (typeof window.PriceManager === 'undefined') { + console.error('PriceManager module not loaded'); + ui.displayErrorMessage('Price data unavailable. Some features may not work correctly.'); + } - NetworkManager.addHandler('offline', () => { - ui.displayErrorMessage("Network connection lost. Will automatically retry when connection is restored."); - updateConnectionStatus('disconnected'); - }); + if (initializeTableRateModule()) { + tableRateModule.init(); + } - NetworkManager.addHandler('reconnected', () => { - ui.hideErrorMessage(); - updateConnectionStatus('connected'); - fetchOffers(); - }); + await initializeTableAndData(); - 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(); + if (window.PriceManager) { + window.PriceManager.addEventListener('priceUpdate', function(prices) { + latestPrices = prices; updateProfitLossDisplays(); }); - } catch (error) { - console.error('[Debug] Error processing WebSocket message:', error); - NetworkManager.handleNetworkError(error); - } - }); - - await tableLoadPromise; - - CleanupManager.setInterval(() => { - CacheManager.cleanup(); - }, 300000); - - CleanupManager.setInterval(updateRowTimes, 900000); - - 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(); - } - }); - } - } - }); - - CleanupManager.addListener(window, 'beforeunload', () => { - cleanup(); - }); -}); - -async function cleanup() { - console.log('Starting cleanup process'); - - try { - - if (filterTimeout) { - clearTimeout(filterTimeout); - filterTimeout = null; } if (window.WebSocketManager) { - WebSocketManager.disconnect(); - WebSocketManager.dispose(); + WebSocketManager.addMessageHandler('message', async (data) => { + if (data.event === 'new_offer' || data.event === 'offer_revoked') { + console.log('WebSocket event received:', data.event); + try { + + const previousPrices = latestPrices; + + const offersResponse = await fetch(isSentOffers ? '/json/sentoffers' : '/json/offers'); + if (!offersResponse.ok) { + throw new Error(`HTTP error! status: ${offersResponse.status}`); + } + + const newData = await offersResponse.json(); + const processedNewData = Array.isArray(newData) ? newData : Object.values(newData); + jsonData = formatInitialData(processedNewData); + originalJsonData = [...jsonData]; + + let priceData; + if (window.PriceManager) { + priceData = await window.PriceManager.getPrices(true); + } else { + priceData = await fetchLatestPrices(); + } + + if (priceData) { + latestPrices = priceData; + CacheManager.set('prices_coingecko', priceData, 'prices'); + } else if (previousPrices) { + console.log('Using previous price data after failed refresh'); + latestPrices = previousPrices; + } + + await updateOffersTable(); + + updateProfitLossDisplays(); + + document.querySelectorAll('.usd-value').forEach(usdValue => { + const coinName = usdValue.getAttribute('data-coin'); + if (coinName) { + const priceKey = getPriceKey(coinName); + const price = latestPrices[priceKey]?.usd; + if (price !== undefined && price !== null) { + const amount = parseFloat(usdValue.getAttribute('data-amount') || '0'); + if (!isNaN(amount) && amount > 0) { + const usdValue = amount * price; + usdValue.textContent = tableRateModule.formatUSD(usdValue); + } + } + } + }); + + updatePaginationInfo(); + + console.log('WebSocket-triggered refresh completed successfully'); + } catch (error) { + console.error('Error during WebSocket-triggered refresh:', error); + NetworkManager.handleNetworkError(error); + } + } + }); + + if (!WebSocketManager.isConnected()) { + WebSocketManager.connect(); + } } - if (window.TooltipManager) { - window.TooltipManager.cleanup(); - window.TooltipManager.dispose(); - } - - cleanupTable(); - - 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; + document.querySelectorAll('th[data-sortable="true"]').forEach(header => { + CleanupManager.addListener(header, 'click', () => { + const columnIndex = parseInt(header.getAttribute('data-column-index'), 10); + if (columnIndex !== null && !isNaN(columnIndex)) { + handleTableSort(columnIndex, header); + } + }); }); - if (window.tableRateModule) { - window.tableRateModule.cache = {}; - window.tableRateModule.processedOffers.clear(); + if (window.NetworkManager) { + NetworkManager.addHandler('online', () => { + updateConnectionStatus('connected'); + if (document.getElementById('error-overlay').classList.contains('hidden')) { + fetchOffers(); + } + }); + + NetworkManager.addHandler('offline', () => { + updateConnectionStatus('disconnected'); + }); } - currentPage = 1; - currentSortColumn = 0; - currentSortDirection = 'desc'; - - if (window.MemoryManager) { - MemoryManager.forceCleanup(); + if (window.config.autoRefreshEnabled) { + startAutoRefresh(); + } + + const filterForm = document.getElementById('filterForm'); + if (filterForm) { + filterForm.querySelectorAll('select').forEach(select => { + CleanupManager.addListener(select, 'change', () => { + applyFilters(); + updateCoinFilterImages(); + updateClearFiltersButton(); + }); + }); + } + + const clearFiltersBtn = document.getElementById('clearFilters'); + if (clearFiltersBtn) { + CleanupManager.addListener(clearFiltersBtn, 'click', clearFilters); + } + + const rowTimeInterval = setInterval(updateRowTimes, 30000); + if (CleanupManager.registerResource) { + CleanupManager.registerResource('rowTimeInterval', rowTimeInterval, () => { + clearInterval(rowTimeInterval); + }); + } else if (CleanupManager.addResource) { + CleanupManager.addResource('rowTimeInterval', rowTimeInterval, () => { + clearInterval(rowTimeInterval); + }); + } else { + + window._cleanupIntervals = window._cleanupIntervals || []; + window._cleanupIntervals.push(rowTimeInterval); } - console.log('Offers table cleanup completed'); } catch (error) { - console.error('Error during offers cleanup:', error); - - try { - CleanupManager.clearAll(); - cleanupTable(); - } catch (e) { - console.error('Failsafe cleanup failed:', e); - } + console.error('Error during initialization:', error); + ui.displayErrorMessage('Error initializing application. Please refresh the page.'); } -} +}); +function cleanup() { + console.log('Starting offers.js cleanup process'); + + try { + if (window.filterTimeout) { + clearTimeout(window.filterTimeout); + window.filterTimeout = null; + } + + if (window.sortTimeout) { + clearTimeout(window.sortTimeout); + window.sortTimeout = null; + } + + if (window.refreshInterval) { + clearInterval(window.refreshInterval); + window.refreshInterval = null; + } + + if (window.countdownInterval) { + clearInterval(window.countdownInterval); + window.countdownInterval = null; + } + + if (window._cleanupIntervals && Array.isArray(window._cleanupIntervals)) { + window._cleanupIntervals.forEach(interval => { + clearInterval(interval); + }); + window._cleanupIntervals = []; + } + + if (window.PriceManager) { + if (typeof window.PriceManager.removeEventListener === 'function') { + window.PriceManager.removeEventListener('priceUpdate'); + } + } + + if (window.TooltipManager) { + if (typeof window.TooltipManager.cleanup === 'function') { + window.TooltipManager.cleanup(); + } + } + + const filterForm = document.getElementById('filterForm'); + if (filterForm) { + CleanupManager.removeListenersByElement(filterForm); + + filterForm.querySelectorAll('select').forEach(select => { + CleanupManager.removeListenersByElement(select); + }); + } + + const paginationButtons = document.querySelectorAll('#prevPage, #nextPage'); + paginationButtons.forEach(button => { + CleanupManager.removeListenersByElement(button); + }); + + document.querySelectorAll('th[data-sortable="true"]').forEach(header => { + CleanupManager.removeListenersByElement(header); + }); + + cleanupTable(); + + jsonData = null; + originalJsonData = null; + latestPrices = null; + + console.log('Offers.js cleanup completed'); + } catch (error) { + console.error('Error during cleanup:', error); + } +} window.cleanup = cleanup; diff --git a/basicswap/static/js/pricechart.js b/basicswap/static/js/pricechart.js index b8b818a..3a0556b 100644 --- a/basicswap/static/js/pricechart.js +++ b/basicswap/static/js/pricechart.js @@ -118,122 +118,28 @@ const api = { }, fetchCoinGeckoDataXHR: async () => { - const cacheKey = 'coinGeckoOneLiner'; - const cachedData = CacheManager.get(cacheKey); - - if (cachedData) { - return cachedData.value; - } - 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 priceData = await window.PriceManager.getPrices(); const transformedData = {}; + 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] = { - 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 + current_price: coinData.usd, + price_btc: coinData.btc || (priceData.bitcoin ? coinData.usd / priceData.bitcoin.usd : 0), + displayName: coin.displayName || coin.symbol }; } }); - 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); - 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; + console.error('Error in fetchCoinGeckoDataXHR:', error); + return {}; } }, @@ -617,6 +523,7 @@ const chartModule = { currentCoin: 'BTC', loadStartTime: 0, chartRefs: new WeakMap(), + pendingAnimationFrame: null, verticalLinePlugin: { id: 'verticalLine', @@ -648,34 +555,25 @@ 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); - }); - } + if (chartModule.chart) { + try { + const chartInstance = chartModule.chart; + const canvas = document.getElementById('coin-chart'); - chartModule.chart.destroy(); - chartModule.chart = null; + chartModule.chart = null; - if (canvas) { - chartModule.chartRefs.delete(canvas); - } - } catch (e) { - try { - if (chartModule.chart) { - if (chartModule.chart.destroy && typeof chartModule.chart.destroy === 'function') { - chartModule.chart.destroy(); - } - chartModule.chart = null; - } - } catch (finalError) {} + if (chartInstance && chartInstance.destroy && typeof chartInstance.destroy === 'function') { + chartInstance.destroy(); } + + if (canvas) { + chartModule.chartRefs.delete(canvas); + } + } catch (e) { + console.error('Error destroying chart:', e); } - }, + } + }, initChart: function() { this.destroyChart(); @@ -1088,11 +986,18 @@ const chartModule = { }, cleanup: function() { - this.destroyChart(); - this.currentCoin = null; - this.loadStartTime = 0; - this.chartRefs = new WeakMap(); + if (this.pendingAnimationFrame) { + cancelAnimationFrame(this.pendingAnimationFrame); + this.pendingAnimationFrame = null; } + + if (!document.hidden) { + this.currentCoin = null; + } + + this.loadStartTime = 0; + this.chartRefs = new WeakMap(); +} }; Chart.register(chartModule.verticalLinePlugin); @@ -1152,14 +1057,16 @@ const app = { nextRefreshTime: null, lastRefreshedTime: null, isRefreshing: false, - isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'false', + isAutoRefreshEnabled: localStorage.getItem('autoRefreshEnabled') !== 'true', + updateNextRefreshTimeRAF: null, + refreshTexts: { label: 'Auto-refresh in', disabled: 'Auto-refresh: disabled', justRefreshed: 'Just refreshed', }, cacheTTL: window.config.cacheConfig.ttlSettings.prices, - minimumRefreshInterval: 60 * 1000, + minimumRefreshInterval: 300 * 1000, init: function() { window.addEventListener('load', app.onLoad); @@ -1329,7 +1236,6 @@ const app = { const headers = document.querySelectorAll('th'); headers.forEach((header, index) => { - CleanupManager.addListener(header, 'click', () => app.sortTable(index, header.classList.contains('disabled'))); }); const closeErrorButton = document.getElementById('close-error'); @@ -1354,6 +1260,52 @@ const app = { app.scheduleNextRefresh(); } }, + + 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() { if (app.autoRefreshInterval) { @@ -1376,7 +1328,7 @@ const app = { } }); - let nextRefreshTime; + let nextRefreshTime = now + app.minimumRefreshInterval; if (earliestExpiration !== Infinity) { nextRefreshTime = Math.max(earliestExpiration, now + app.minimumRefreshInterval); } else { @@ -1395,208 +1347,182 @@ const app = { app.updateNextRefreshTime(); }, - refreshAllData: async function() { - if (app.isRefreshing) { - console.log('Refresh already in progress, skipping...'); - return; +refreshAllData: async function() { + console.log('Price refresh started at', new Date().toLocaleTimeString()); + + if (app.isRefreshing) { + 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; + const timeSinceLastRequest = Date.now() - lastGeckoRequest; + 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`); + + 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; + } + + console.log('Starting refresh of all data...'); + app.isRefreshing = true; + app.updateNextRefreshTime(); + ui.showLoader(); + chartModule.showChartLoader(); + + try { + ui.hideErrorMessage(); + CacheManager.clear(); + + const btcUpdateSuccess = await app.updateBTCPrice(); + if (!btcUpdateSuccess) { + console.warn('BTC price update failed, continuing with cached or default value'); } - if (!NetworkManager.isOnline()) { - ui.displayErrorMessage("Network connection unavailable. Please check your connection."); - return; + await new Promise(resolve => setTimeout(resolve, 1000)); + + const allCoinData = await api.fetchCoinGeckoDataXHR(); + if (allCoinData.error) { + throw new Error(`CoinGecko API Error: ${allCoinData.error}`); } - const lastGeckoRequest = rateLimiter.lastRequestTime['coingecko'] || 0; - const timeSinceLastRequest = Date.now() - lastGeckoRequest; - 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`); - - 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; - } - - console.log('Starting refresh of all data...'); - app.isRefreshing = true; - ui.showLoader(); - chartModule.showChartLoader(); - + let volumeData = {}; try { - ui.hideErrorMessage(); - CacheManager.clear(); + volumeData = await api.fetchVolumeDataXHR(); + } catch (volumeError) {} - const btcUpdateSuccess = await app.updateBTCPrice(); - if (!btcUpdateSuccess) { - console.warn('BTC price update failed, continuing with cached or default value'); - } + const failedCoins = []; - await new Promise(resolve => setTimeout(resolve, 1000)); + for (const coin of window.config.coins) { + const symbol = coin.symbol.toLowerCase(); + const coinData = allCoinData[symbol]; - const allCoinData = await api.fetchCoinGeckoDataXHR(); - if (allCoinData.error) { - throw new Error(`CoinGecko API Error: ${allCoinData.error}`); - } - - let volumeData = {}; try { - volumeData = await api.fetchVolumeDataXHR(); - } catch (volumeError) {} - - const failedCoins = []; - - for (const coin of window.config.coins) { - const symbol = coin.symbol.toLowerCase(); - const coinData = allCoinData[symbol]; - - try { - if (!coinData) { - throw new Error(`No data received`); - } - - 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; - } - } else { - try { - 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); + if (!coinData) { + throw new Error(`No data received`); } - } - await new Promise(resolve => setTimeout(resolve, 1000)); + coinData.displayName = coin.displayName || coin.symbol; - if (chartModule.currentCoin) { - try { - await chartModule.updateChart(chartModule.currentCoin, true); - } catch (chartError) { - console.error('Chart update failed:', chartError); - } - } - - app.lastRefreshedTime = new Date(); - localStorage.setItem('lastRefreshedTime', app.lastRefreshedTime.getTime().toString()); - ui.updateLastRefreshedTime(); - - if (failedCoins.length > 0) { - const failureMessage = failedCoins.length === 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(); + 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; } - }, 1000); + } else { + try { + 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'); + + console.log(`Updated price for ${coin.symbol}: $${coinData.current_price}`); + + } catch (coinError) { + console.warn(`Failed to update ${coin.symbol}: ${coinError.message}`); + failedCoins.push(coin.symbol); } + } - console.log(`Refresh completed. Failed coins: ${failedCoins.length}`); + await new Promise(resolve => setTimeout(resolve, 1000)); - } catch (error) { - console.error('Critical error during refresh:', error); - NetworkManager.handleNetworkError(error); + if (chartModule.currentCoin) { + try { + await chartModule.updateChart(chartModule.currentCoin, true); + } catch (chartError) { + console.error('Chart update failed:', chartError); + } + } - let countdown = 10; - ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); + 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(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); + ui.displayErrorMessage(`${failureMessage} (${countdown}s)`); } else { clearInterval(countdownInterval); ui.hideErrorMessage(); } }, 1000); - - } finally { - ui.hideLoader(); - chartModule.hideChartLoader(); - app.isRefreshing = false; - - if (app.isAutoRefreshEnabled) { - app.scheduleNextRefresh(); - } } - }, + console.log(`Price refresh completed at ${new Date().toLocaleTimeString()}. Updated ${window.config.coins.length - failedCoins.length}/${window.config.coins.length} coins.`); - 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.nextRefreshTime) { - if (app.updateNextRefreshTimeRAF) { - cancelAnimationFrame(app.updateNextRefreshTimeRAF); - } + } catch (error) { + console.error('Critical error during refresh:', error); + NetworkManager.handleNetworkError(error); - const updateDisplay = () => { - const timeUntilRefresh = Math.max(0, Math.ceil((app.nextRefreshTime - Date.now()) / 1000)); + let countdown = 10; + ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); - 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(); + const countdownInterval = setInterval(() => { + countdown--; + if (countdown > 0) { + ui.displayErrorMessage(`Refresh failed: ${error.message}. Please try again later. (${countdown}s)`); } else { - labelElement.textContent = ''; - valueElement.textContent = app.refreshTexts.disabled; + clearInterval(countdownInterval); + ui.hideErrorMessage(); } + }, 1000); + + console.error(`Price refresh failed at ${new Date().toLocaleTimeString()}: ${error.message}`); + + } finally { + ui.hideLoader(); + chartModule.hideChartLoader(); + app.isRefreshing = false; + app.updateNextRefreshTime(); + + if (app.isAutoRefreshEnabled) { + app.scheduleNextRefresh(); } - }, + + console.log(`Refresh process finished at ${new Date().toLocaleTimeString()}, next refresh scheduled: ${app.isAutoRefreshEnabled ? 'yes' : 'no'}`); + } +}, updateAutoRefreshButton: function() { const button = document.getElementById('toggle-auto-refresh'); @@ -1649,23 +1575,33 @@ const app = { updateBTCPrice: async function() { try { - if (!NetworkManager.isOnline()) { - throw new Error('Network is offline'); + const priceData = await window.PriceManager.getPrices(); + + 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 (response && response.rates && response.rates.bitcoin) { - app.btcPriceUSD = response.rates.bitcoin; + if (app.btcPriceUSD > 0) { + console.log('Using previously cached BTC price:', app.btcPriceUSD); return true; } - console.warn('Unexpected BTC price data structure:', response); + console.warn('Could not find BTC price in current data'); return false; - } catch (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; } }, @@ -1815,13 +1751,13 @@ document.addEventListener('DOMContentLoaded', () => { CleanupManager.setInterval(() => { CacheManager.cleanup(); - }, 300000); // Every 5 minutes + }, 300000); CleanupManager.setInterval(() => { if (chartModule && chartModule.currentCoin && NetworkManager.isOnline()) { chartModule.updateChart(chartModule.currentCoin); } - }, 900000); // Every 15 minutes + }, 900000); CleanupManager.addListener(document, 'visibilitychange', () => { if (!document.hidden) { diff --git a/basicswap/static/js/swaps_in_progress.js b/basicswap/static/js/swaps_in_progress.js index 8aeee3d..158051d 100644 --- a/basicswap/static/js/swaps_in_progress.js +++ b/basicswap/static/js/swaps_in_progress.js @@ -1,20 +1,4 @@ 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 = { identities: new Map(), @@ -309,8 +293,8 @@ const createSwapTableRow = async (swap) => { const identity = await IdentityManager.getIdentityData(swap.addr_from); const uniqueId = `${swap.bid_id}_${swap.created_at}`; - const fromSymbol = COIN_NAME_TO_SYMBOL[swap.coin_from] || swap.coin_from; - const toSymbol = COIN_NAME_TO_SYMBOL[swap.coin_to] || swap.coin_to; + const fromSymbol = window.CoinManager.getSymbol(swap.coin_from) || swap.coin_from; + const toSymbol = window.CoinManager.getSymbol(swap.coin_to) || swap.coin_to; const timeColor = getTimeStrokeColor(swap.expire_at); const fromAmount = parseFloat(swap.amount_from) || 0; const toAmount = parseFloat(swap.amount_to) || 0; @@ -630,31 +614,7 @@ async function updateSwapsTable(options = {}) { } 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); + return true; } const setupEventListeners = () => { diff --git a/basicswap/templates/footer.html b/basicswap/templates/footer.html index 0042412..3ab89eb 100644 --- a/basicswap/templates/footer.html +++ b/basicswap/templates/footer.html @@ -25,7 +25,7 @@

© 2025~ (BSX) BasicSwap

BSX: v{{ version }}

-

GUI: v3.2.0

+

GUI: v3.2.1

Made with

{{ love_svg | safe }}
diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index f57f5ab..8cfc632 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -12,31 +12,25 @@ - {% if refresh %} {% endif %} (BSX) BasicSwap - v{{ version }} - - - - + > + - - - - - + + - - - - + @@ -93,9 +80,9 @@ {% if current_page == 'wallets' or current_page == 'wallet' %} {% endif %} + - - + diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html index 7cb93b1..a5227ae 100644 --- a/basicswap/templates/offers.html +++ b/basicswap/templates/offers.html @@ -343,16 +343,16 @@ {% endif %} - +
Rate - +
- +
Market +/- - +
diff --git a/basicswap/templates/unlock.html b/basicswap/templates/unlock.html index 918b282..40270bc 100644 --- a/basicswap/templates/unlock.html +++ b/basicswap/templates/unlock.html @@ -1,36 +1,68 @@ {% from 'style.html' import circular_info_messages_svg, green_cross_close_svg, red_cross_close_svg, circular_error_messages_svg %} - - - {% if refresh %} - - {% endif %} - - - + + + {% if refresh %} + + {% endif %} + (BSX) BasicSwap - v{{ version }} + + > + + + + + - - - + + - - - - + @@ -38,8 +70,11 @@ {% if current_page == 'wallets' or current_page == 'wallet' %} {% endif %} + - + + +