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 = ` -
© 2025~ (BSX) BasicSwap
BSX: v{{ version }}
-GUI: v3.2.0
+GUI: v3.2.1
Made with
{{ love_svg | safe }}