From 97bb615176c306105e7bc7f80665a6cc71fc1590 Mon Sep 17 00:00:00 2001 From: Gerlof van Ek Date: Sat, 22 Feb 2025 16:55:12 +0100 Subject: [PATCH] New bids pages + various fixes. (#266) * New bids pages + various fixes. * LINT * Fix styling. --- basicswap/js_server.py | 25 +- basicswap/static/images/coins/Wownero.png | Bin 8589 -> 9215 bytes basicswap/static/js/bids_available.js | 899 +++++++++++ basicswap/static/js/bids_sentreceived.js | 1426 +++++++++++++++++ .../static/js/{offerstable.js => offers.js} | 0 basicswap/templates/bids.html | 742 ++++----- basicswap/templates/bids_available.html | 300 ++-- basicswap/templates/header.html | 2 +- basicswap/templates/offers.html | 51 +- basicswap/templates/unlock.html | 1 - basicswap/ui/page_bids.py | 19 +- basicswap/ui/util.py | 26 +- 12 files changed, 2851 insertions(+), 640 deletions(-) create mode 100644 basicswap/static/js/bids_available.js create mode 100644 basicswap/static/js/bids_sentreceived.js rename basicswap/static/js/{offerstable.js => offers.js} (100%) diff --git a/basicswap/js_server.py b/basicswap/js_server.py index 7ffd1e9..b95c662 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -15,6 +15,7 @@ from .util import ( ) from .basicswap_util import ( strBidState, + strTxState, SwapTypes, NotificationTypes as NT, ) @@ -320,18 +321,36 @@ def formatBids(swap_client, bids, filters) -> bytes: with_extra_info = filters.get("with_extra_info", False) rv = [] for b in bids: + ci_from = swap_client.ci(b[9]) + offer = swap_client.getOffer(b[3]) + ci_to = swap_client.ci(offer.coin_to) if offer else None + + amount_to = None + if ci_to: + amount_to = ci_to.format_amount( + (b[4] * b[10]) // ci_from.COIN() + ) + bid_data = { "bid_id": b[2].hex(), "offer_id": b[3].hex(), "created_at": b[0], "expire_at": b[1], - "coin_from": b[9], - "amount_from": swap_client.ci(b[9]).format_amount(b[4]), + "coin_from": ci_from.coin_name(), + "coin_to": ci_to.coin_name() if ci_to else "Unknown", + "amount_from": ci_from.format_amount(b[4]), + "amount_to": amount_to, "bid_rate": swap_client.ci(b[14]).format_amount(b[10]), "bid_state": strBidState(b[5]), + "addr_from": b[11], + "addr_to": offer.addr_to if offer else None } + if with_extra_info: - bid_data["addr_from"] = b[11] + bid_data.update({ + "tx_state_a": strTxState(b[7]), + "tx_state_b": strTxState(b[8]) + }) rv.append(bid_data) return bytes(json.dumps(rv), "UTF-8") diff --git a/basicswap/static/images/coins/Wownero.png b/basicswap/static/images/coins/Wownero.png index 504d708e39f319f7aeaa79559251c4c67897a92c..ea72870b09981d6d1c6950c49aa9d1de67b40242 100644 GIT binary patch delta 8386 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 diff --git a/basicswap/static/js/bids_available.js b/basicswap/static/js/bids_available.js new file mode 100644 index 0000000..921918e --- /dev/null +++ b/basicswap/static/js/bids_available.js @@ -0,0 +1,899 @@ +// Constants and State +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', + 'Wownero': 'WOW', + 'Bitcoin Cash': 'BCH', + 'Dogecoin': 'DOGE' +}; + +// Global state +const state = { + dentities: new Map(), + currentPage: 1, + wsConnected: false, + jsonData: [], + isLoading: false, + isRefreshing: false, + refreshPromise: null +}; + +// DOM +const elements = { + bidsBody: document.getElementById('bids-body'), + prevPageButton: document.getElementById('prevPage'), + nextPageButton: document.getElementById('nextPage'), + currentPageSpan: document.getElementById('currentPage'), + paginationControls: document.getElementById('pagination-controls'), + availableBidsCount: document.getElementById('availableBidsCount'), + refreshBidsButton: document.getElementById('refreshBids'), + statusDot: document.getElementById('status-dot'), + statusText: document.getElementById('status-text') +}; + +// Identity Manager +const IdentityManager = { + cache: new Map(), + pendingRequests: new Map(), + retryDelay: 2000, + maxRetries: 3, + cacheTimeout: 5 * 60 * 1000, // 5 minutes + + async getIdentityData(address) { + if (!address) { + return { address: '' }; + } + + const cachedData = this.getCachedIdentity(address); + if (cachedData) { + return { ...cachedData, address }; + } + + if (this.pendingRequests.has(address)) { + const pendingData = await this.pendingRequests.get(address); + return { ...pendingData, address }; + } + + const request = this.fetchWithRetry(address); + this.pendingRequests.set(address, request); + + try { + const data = await request; + this.cache.set(address, { + data, + timestamp: Date.now() + }); + return { ...data, address }; + } catch (error) { + console.warn(`Error fetching identity for ${address}:`, error); + return { address }; + } finally { + this.pendingRequests.delete(address); + } + }, + + getCachedIdentity(address) { + const cached = this.cache.get(address); + if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) { + return cached.data; + } + if (cached) { + this.cache.delete(address); + } + return null; + }, + + async fetchWithRetry(address, attempt = 1) { + try { + const response = await fetch(`/json/identities/${address}`, { + signal: AbortSignal.timeout(5000) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return { + ...data, + address, + num_sent_bids_successful: safeParseInt(data.num_sent_bids_successful), + num_recv_bids_successful: safeParseInt(data.num_recv_bids_successful), + num_sent_bids_failed: safeParseInt(data.num_sent_bids_failed), + num_recv_bids_failed: safeParseInt(data.num_recv_bids_failed), + num_sent_bids_rejected: safeParseInt(data.num_sent_bids_rejected), + num_recv_bids_rejected: safeParseInt(data.num_recv_bids_rejected), + label: data.label || '', + note: data.note || '', + automation_override: safeParseInt(data.automation_override) + }; + } catch (error) { + if (attempt >= this.maxRetries) { + console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`); + return { + address, + num_sent_bids_successful: 0, + num_recv_bids_successful: 0, + num_sent_bids_failed: 0, + num_recv_bids_failed: 0, + num_sent_bids_rejected: 0, + num_recv_bids_rejected: 0, + label: '', + note: '', + automation_override: 0 + }; + } + + await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt)); + return this.fetchWithRetry(address, attempt + 1); + } + }, + + clearCache() { + this.cache.clear(); + this.pendingRequests.clear(); + }, + + removeFromCache(address) { + this.cache.delete(address); + this.pendingRequests.delete(address); + }, + + cleanup() { + const now = Date.now(); + for (const [address, cached] of this.cache.entries()) { + if (now - cached.timestamp >= this.cacheTimeout) { + this.cache.delete(address); + } + } + } +}; + +// Util +const formatTimeAgo = (timestamp) => { + const now = Math.floor(Date.now() / 1000); + const diff = now - timestamp; + + if (diff < 60) return `${diff} seconds ago`; + if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`; + return `${Math.floor(diff / 86400)} days ago`; +}; + +const formatTime = (timestamp) => { + const now = Math.floor(Date.now() / 1000); + const diff = timestamp - now; + + if (diff <= 0) return "Expired"; + if (diff < 60) return `${diff} seconds`; + if (diff < 3600) return `${Math.floor(diff / 60)} minutes`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hours`; + return `${Math.floor(diff / 86400)} days`; +}; + +const formatAddress = (address, displayLength = 15) => { + if (!address) return ''; + if (address.length <= displayLength) return address; + return `${address.slice(0, displayLength)}...`; +}; + +const getTimeStrokeColor = (expireTime) => { + const now = Math.floor(Date.now() / 1000); + const timeLeft = expireTime - now; + + if (timeLeft <= 300) return '#9CA3AF'; // 5 minutes or less + if (timeLeft <= 1800) return '#3B82F6'; // 30 minutes or less + return '#10B981'; // More than 30 minutes +}; + +const createTimeTooltip = (bid) => { + const postedTime = formatTimeAgo(bid.created_at); + const expiresIn = formatTime(bid.expire_at); + return ` +
+ +
Posted: ${postedTime}
+
Expires in: ${expiresIn}
+
+
+
+

Time Indicator Colors:

+

+ + + + + + + Green: More than 30 minutes left +

+

+ + + + + + + Blue: Between 5 and 30 minutes left +

+

+ + + + + + + Grey: Less than 5 minutes left or expired +

+
+ `; +}; + +const safeParseInt = (value) => { + const parsed = parseInt(value); + return isNaN(parsed) ? 0 : parsed; +}; + +const processIdentityStats = (identity) => { + if (!identity) return null; + + const stats = { + sentSuccessful: safeParseInt(identity.num_sent_bids_successful), + recvSuccessful: safeParseInt(identity.num_recv_bids_successful), + sentFailed: safeParseInt(identity.num_sent_bids_failed), + recvFailed: safeParseInt(identity.num_recv_bids_failed), + sentRejected: safeParseInt(identity.num_sent_bids_rejected), + recvRejected: safeParseInt(identity.num_recv_bids_rejected) + }; + + stats.totalSuccessful = stats.sentSuccessful + stats.recvSuccessful; + stats.totalFailed = stats.sentFailed + stats.recvFailed; + stats.totalRejected = stats.sentRejected + stats.recvRejected; + stats.totalBids = stats.totalSuccessful + stats.totalFailed + stats.totalRejected; + + stats.successRate = stats.totalBids > 0 + ? ((stats.totalSuccessful / stats.totalBids) * 100).toFixed(1) + : '0.0'; + + return stats; +}; + +const createIdentityTooltip = (identity) => { + if (!identity) return ''; + + const stats = processIdentityStats(identity); + if (!stats) return ''; + + const getSuccessRateColor = (rate) => { + const numRate = parseFloat(rate); + if (numRate >= 80) return 'text-green-600'; + if (numRate >= 60) return 'text-yellow-600'; + return 'text-red-600'; + }; + + return ` +
+ ${identity.label ? ` +
+
Label:
+
${identity.label}
+
+ ` : ''} + +
+
Bid From Address:
+
+ ${identity.address || ''} +
+
+ + ${identity.note ? ` +
+
Note:
+
${identity.note}
+
+ ` : ''} + +
+
Swap History:
+
+
+
+ ${stats.successRate}% +
+
Success Rate
+
+
+
${stats.totalBids}
+
Total Trades
+
+
+
+
+
+ ${stats.totalSuccessful} +
+
Successful
+
+
+
+ ${stats.totalRejected} +
+
Rejected
+
+
+
+ ${stats.totalFailed} +
+
Failed
+
+
+
+
+ `; +}; + +// WebSocket Manager +const WebSocketManager = { + ws: null, + processingQueue: false, + reconnectTimeout: null, + maxReconnectAttempts: 5, + reconnectAttempts: 0, + reconnectDelay: 5000, + + initialize() { + this.connect(); + this.startHealthCheck(); + }, + + connect() { + if (this.ws?.readyState === WebSocket.OPEN) return; + + try { + const wsPort = window.ws_port || '11700'; + this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); + this.setupEventHandlers(); + } catch (error) { + console.error('WebSocket connection error:', error); + this.handleReconnect(); + } + }, + + setupEventHandlers() { + this.ws.onopen = () => { + state.wsConnected = true; + this.reconnectAttempts = 0; + updateConnectionStatus('connected'); + console.log('🟢 WebSocket connection established'); + updateBidsTable({ resetPage: true, refreshData: true }); + }; + + this.ws.onmessage = () => { + if (!this.processingQueue) { + this.processingQueue = true; + setTimeout(async () => { + try { + if (!state.isRefreshing) { + await updateBidsTable({ resetPage: false, refreshData: true }); + } + } finally { + this.processingQueue = false; + } + }, 200); + } + }; + + this.ws.onclose = () => { + state.wsConnected = false; + updateConnectionStatus('disconnected'); + this.handleReconnect(); + }; + + this.ws.onerror = () => { + updateConnectionStatus('error'); + }; + }, + + startHealthCheck() { + setInterval(() => { + if (this.ws?.readyState !== WebSocket.OPEN) { + this.handleReconnect(); + } + }, 30000); + }, + + handleReconnect() { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + } + + this.reconnectAttempts++; + if (this.reconnectAttempts <= this.maxReconnectAttempts) { + const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1); + this.reconnectTimeout = setTimeout(() => this.connect(), delay); + } else { + updateConnectionStatus('error'); + setTimeout(() => { + this.reconnectAttempts = 0; + this.connect(); + }, 60000); + } + } +}; + +// UI +const updateConnectionStatus = (status) => { + const { statusDot, statusText } = elements; + if (!statusDot || !statusText) return; + + const statusConfig = { + connected: { + dotClass: 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2', + textClass: 'text-sm text-green-500', + message: 'Connected' + }, + disconnected: { + dotClass: 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2', + textClass: 'text-sm text-red-500', + message: 'Disconnected - Reconnecting...' + }, + error: { + dotClass: 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2', + textClass: 'text-sm text-yellow-500', + message: 'Connection Error' + }, + default: { + dotClass: 'w-2.5 h-2.5 rounded-full bg-gray-500 mr-2', + textClass: 'text-sm text-gray-500', + message: 'Connecting...' + } + }; + + const config = statusConfig[status] || statusConfig.default; + statusDot.className = config.dotClass; + statusText.className = config.textClass; + statusText.textContent = config.message; +}; + +const updateLoadingState = (isLoading) => { + state.isLoading = isLoading; + if (elements.refreshBidsButton) { + elements.refreshBidsButton.disabled = isLoading; + elements.refreshBidsButton.classList.toggle('opacity-75', isLoading); + elements.refreshBidsButton.classList.toggle('cursor-wait', isLoading); + + const refreshIcon = elements.refreshBidsButton.querySelector('svg'); + const refreshText = elements.refreshBidsButton.querySelector('#refreshText'); + + if (refreshIcon) { + // Add CSS transition for smoother animation + refreshIcon.style.transition = 'transform 0.3s ease'; + refreshIcon.classList.toggle('animate-spin', isLoading); + } + + if (refreshText) { + refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh'; + } + } +}; + +const createBidTableRow = async (bid) => { + if (!bid || !bid.bid_id) { + console.error('Invalid bid data:', bid); + return ''; + } + + const identity = await IdentityManager.getIdentityData(bid.addr_from); + const fromAmount = parseFloat(bid.amount_from) || 0; + const toAmount = parseFloat(bid.amount_to) || 0; + 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 timeColor = getTimeStrokeColor(bid.expire_at); + const uniqueId = `${bid.bid_id}_${bid.created_at}`; + + return ` + + +
+ + + + +
+
+ + + + + + +
+ +
+ + + + + + + + + +
+
+
+
${fromAmount.toFixed(8)}
+
${bid.coin_from}
+
+
+
+ + + + +
+
+ + ${bid.coin_from} + + + + + + ${bid.coin_to} + +
+
+ + + + +
+
+
${toAmount.toFixed(8)}
+
${bid.coin_to}
+
+
+ + + + +
+
+ + ${rate.toFixed(8)} ${toSymbol}/${fromSymbol} + + + ${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol} + +
+
+ + + + + + Accept + + + + + + + + + + + + + `; +}; + +const getDisplayText = (identity, address) => { + if (identity?.label) { + return identity.label; + } + return formatAddress(address); +}; + +const createDetailsColumn = (bid, identity, uniqueId) => ` + + + +`; + +async function updateBidsTable(options = {}) { + const { resetPage = false, refreshData = true } = options; + + if (state.refreshPromise) { + await state.refreshPromise; + return; + } + + try { + updateLoadingState(true); + + if (refreshData) { + state.refreshPromise = (async () => { + try { + const response = await fetch('/json/bids', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + sort_by: "created_at", + sort_dir: "desc", + with_available_or_active: true + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const allBids = await response.json(); + if (!Array.isArray(allBids)) { + throw new Error('Invalid response format'); + } + + state.jsonData = allBids.filter(bid => bid.bid_state === "Received"); + state.originalJsonData = [...state.jsonData]; + } finally { + state.refreshPromise = null; + } + })(); + + await state.refreshPromise; + } + + if (elements.availableBidsCount) { + elements.availableBidsCount.textContent = state.jsonData.length; + } + + const totalPages = Math.ceil(state.jsonData.length / PAGE_SIZE); + + if (resetPage && state.jsonData.length > 0) { + state.currentPage = 1; + } + + state.currentPage = Math.min(Math.max(1, state.currentPage), Math.max(1, totalPages)); + + const startIndex = (state.currentPage - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + const currentPageBids = state.jsonData.slice(startIndex, endIndex); + + if (elements.bidsBody) { + if (currentPageBids.length > 0) { + const rowPromises = currentPageBids.map(bid => createBidTableRow(bid)); + const rows = await Promise.all(rowPromises); + elements.bidsBody.innerHTML = rows.join(''); + + if (window.TooltipManager) { + window.TooltipManager.cleanup(); + const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]'); + tooltipTriggers.forEach(trigger => { + const targetId = trigger.getAttribute('data-tooltip-target'); + const tooltipContent = document.getElementById(targetId); + if (tooltipContent) { + window.TooltipManager.create(trigger, tooltipContent.innerHTML, { + placement: trigger.getAttribute('data-tooltip-placement') || 'top' + }); + } + }); + } + } else { + elements.bidsBody.innerHTML = ` + + + No available bids requests found + + `; + } + } + + if (elements.paginationControls) { + elements.paginationControls.style.display = totalPages > 1 ? 'flex' : 'none'; + } + + if (elements.currentPageSpan) { + elements.currentPageSpan.textContent = state.currentPage; + } + + if (elements.prevPageButton) { + elements.prevPageButton.style.display = state.currentPage > 1 ? 'inline-flex' : 'none'; + } + + if (elements.nextPageButton) { + elements.nextPageButton.style.display = state.currentPage < totalPages ? 'inline-flex' : 'none'; + } + + } catch (error) { + console.error('Error updating bids table:', error); + if (elements.bidsBody) { + elements.bidsBody.innerHTML = ` + + + Error loading bids. Please try again later. + + `; + } + } finally { + updateLoadingState(false); + } +} + +// Event +const setupEventListeners = () => { +if (elements.refreshBidsButton) { + elements.refreshBidsButton.addEventListener('click', async () => { + if (state.isRefreshing) return; + + updateLoadingState(true); + + await new Promise(resolve => setTimeout(resolve, 500)); + + try { + await updateBidsTable({ resetPage: true, refreshData: true }); + } finally { + updateLoadingState(false); + } + }); +} + + if (elements.prevPageButton) { + elements.prevPageButton.addEventListener('click', async () => { + if (state.isLoading) return; + if (state.currentPage > 1) { + state.currentPage--; + await updateBidsTable({ resetPage: false, refreshData: false }); + } + }); + } + + if (elements.nextPageButton) { + elements.nextPageButton.addEventListener('click', async () => { + if (state.isLoading) return; + const totalPages = Math.ceil(state.jsonData.length / PAGE_SIZE); + if (state.currentPage < totalPages) { + state.currentPage++; + await updateBidsTable({ resetPage: false, refreshData: false }); + } + }); + } +}; + +// Init +document.addEventListener('DOMContentLoaded', () => { + WebSocketManager.initialize(); + setupEventListeners(); +}); diff --git a/basicswap/static/js/bids_sentreceived.js b/basicswap/static/js/bids_sentreceived.js new file mode 100644 index 0000000..924aaf2 --- /dev/null +++ b/basicswap/static/js/bids_sentreceived.js @@ -0,0 +1,1426 @@ +// Constants and State +const PAGE_SIZE = 50; +const state = { + currentPage: { + sent: 1, + received: 1 + }, + isLoading: false, + isRefreshing: false, + currentTab: 'sent', + wsConnected: false, + refreshPromise: null, + data: { + sent: [], + received: [] + }, + filters: { + state: -1, + sort_by: 'created_at', + sort_dir: 'desc', + with_expired: true, + searchQuery: '', + coin_from: 'any', + coin_to: 'any' + } +}; + +const STATE_MAP = { + 1: ['Sent'], + 2: ['Receiving'], + 3: ['Received'], + 4: ['Receiving accept'], + 5: ['Accepted'], + 6: ['Initiated'], + 7: ['Participating'], + 8: ['Completed'], + 9: ['Script coin locked'], + 10: ['Script coin spend tx valid'], + 11: ['Scriptless coin locked'], + 12: ['Script coin lock released'], + 13: ['Script tx redeemed'], + 14: ['Script pre-refund tx in chain'], + 15: ['Scriptless tx redeemed'], + 16: ['Scriptless tx recovered'], + 17: ['Failed, refunded'], + 18: ['Failed, swiped'], + 19: ['Failed'], + 20: ['Delaying'], + 21: ['Timed-out', 'Expired'], + 22: ['Abandoned'], + 23: ['Error'], + 24: ['Stalled (debug)'], + 25: ['Rejected'], + 26: ['Unknown bid state'], + 27: ['Exchanged script lock tx sigs msg'], + 28: ['Exchanged script lock spend tx msg'], + 29: ['Request sent'], + 30: ['Request accepted'], + 31: ['Expired'], + 32: ['Auto accept delay'], + 33: ['Auto accept failed'] +}; + +const elements = { + sentBidsBody: document.querySelector('#sent tbody'), + receivedBidsBody: document.querySelector('#received tbody'), + filterForm: document.querySelector('form'), + stateSelect: document.querySelector('select[name="state"]'), + sortBySelect: document.querySelector('select[name="sort_by"]'), + sortDirSelect: document.querySelector('select[name="sort_dir"]'), + withExpiredSelect: document.querySelector('select[name="with_expired"]'), + tabButtons: document.querySelectorAll('#myTab button'), + sentContent: document.getElementById('sent'), + receivedContent: document.getElementById('received'), + + sentPaginationControls: document.getElementById('pagination-controls-sent'), + receivedPaginationControls: document.getElementById('pagination-controls-received'), + prevPageSent: document.getElementById('prevPageSent'), + nextPageSent: document.getElementById('nextPageSent'), + prevPageReceived: document.getElementById('prevPageReceived'), + nextPageReceived: document.getElementById('nextPageReceived'), + currentPageSent: document.getElementById('currentPageSent'), + currentPageReceived: document.getElementById('currentPageReceived'), + sentBidsCount: document.getElementById('sentBidsCount'), + receivedBidsCount: document.getElementById('receivedBidsCount'), + + statusDotSent: document.getElementById('status-dot-sent'), + statusTextSent: document.getElementById('status-text-sent'), + statusDotReceived: document.getElementById('status-dot-received'), + statusTextReceived: document.getElementById('status-text-received'), + + refreshSentBids: document.getElementById('refreshSentBids'), + refreshReceivedBids: document.getElementById('refreshReceivedBids') +}; + +// WebSocket Management +const WebSocketManager = { + ws: null, + processingQueue: false, + reconnectTimeout: null, + maxReconnectAttempts: 5, + reconnectAttempts: 0, + reconnectDelay: 5000, + + initialize() { + this.connect(); + this.startHealthCheck(); + }, + + isConnected() { + return this.ws?.readyState === WebSocket.OPEN; + }, + + connect() { + if (this.isConnected()) return; + + try { + const wsPort = window.ws_port || '11700'; + this.ws = new WebSocket(`ws://${window.location.hostname}:${wsPort}`); + this.setupEventHandlers(); + } catch (error) { + console.error('WebSocket connection error:', error); + this.handleReconnect(); + } + }, + + setupEventHandlers() { + this.ws.onopen = () => { + state.wsConnected = true; + this.reconnectAttempts = 0; + updateConnectionStatus('connected'); + console.log('🟢 WebSocket connection established'); + updateBidsTable(); + }; + + this.ws.onmessage = () => { + if (!this.processingQueue) { + this.processingQueue = true; + setTimeout(async () => { + try { + if (!state.isRefreshing) { + await updateBidsTable(); + } + } finally { + this.processingQueue = false; + } + }, 200); + } + }; + + this.ws.onclose = () => { + state.wsConnected = false; + updateConnectionStatus('disconnected'); + this.handleReconnect(); + }; + + this.ws.onerror = () => { + updateConnectionStatus('error'); + }; + }, + + startHealthCheck() { + setInterval(() => { + if (!this.isConnected()) { + this.handleReconnect(); + } + }, 30000); + }, + + handleReconnect() { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout); + } + + this.reconnectAttempts++; + if (this.reconnectAttempts <= this.maxReconnectAttempts) { + const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1); + this.reconnectTimeout = setTimeout(() => this.connect(), delay); + } else { + updateConnectionStatus('error'); + setTimeout(() => { + this.reconnectAttempts = 0; + this.connect(); + }, 60000); + } + } +}; + +// Core +const safeParseInt = (value) => { + const parsed = parseInt(value); + return isNaN(parsed) ? 0 : parsed; +}; + +const formatAddress = (address, displayLength = 15) => { + if (!address) return ''; + if (address.length <= displayLength) return address; + return `${address.slice(0, displayLength)}...`; +}; + +const formatTime = (timestamp) => { + if (!timestamp) return ''; + const date = new Date(timestamp * 1000); + return date.toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); +}; + +const getTimeStrokeColor = (expireTime) => { + const now = Math.floor(Date.now() / 1000); + return expireTime > now ? '#10B981' : '#9CA3AF'; +}; + +const getStatusClass = (status) => { + switch (status) { + case 'Completed': + return 'bg-green-100 text-green-800 dark:bg-green-500 dark:text-white'; + case 'Expired': + return 'bg-gray-100 text-gray-800 dark:bg-gray-400 dark:text-white'; + case 'Abandoned': + return 'bg-red-100 text-red-800 dark:bg-red-500 dark:text-white'; + case 'Failed': + return 'bg-rose-100 text-rose-800 dark:bg-rose-500 dark:text-white'; + case 'Failed, refunded': + return 'bg-gray-100 text-orange-800 dark:bg-gray-400 dark:text-red-500'; + default: + return 'bg-blue-100 text-blue-800 dark:bg-blue-500 dark:text-white'; + } +}; + +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; +} + +// State +function hasActiveFilters() { + const coinFromSelect = document.getElementById('coin_from'); + const coinToSelect = document.getElementById('coin_to'); + const withExpiredSelect = document.getElementById('with_expired'); + const stateSelect = document.getElementById('state'); + const hasNonDefaultState = stateSelect && stateSelect.value !== '-1'; + const hasSearchQuery = state.filters.searchQuery.trim() !== ''; + const hasNonDefaultCoinFrom = coinFromSelect && coinFromSelect.value !== 'any'; + const hasNonDefaultCoinTo = coinToSelect && coinToSelect.value !== 'any'; + const hasNonDefaultExpired = withExpiredSelect && withExpiredSelect.value !== 'true'; + + return hasNonDefaultState || + hasSearchQuery || + hasNonDefaultCoinFrom || + hasNonDefaultCoinTo || + hasNonDefaultExpired; +} + +function filterAndSortData(bids) { + if (!Array.isArray(bids)) { + console.log('Invalid bids data:', bids); + return []; + } + + const expiredStates = ['Expired', 'Timed-out']; + + return bids.filter(bid => { + if (state.filters.state !== -1) { + const allowedStates = STATE_MAP[state.filters.state] || []; + if (allowedStates.length > 0 && !allowedStates.includes(bid.bid_state)) { + return false; + } + } + + if (!state.filters.with_expired && expiredStates.includes(bid.bid_state)) { + return false; + } + + if (state.filters.coin_from !== 'any') { + const coinFromSelect = document.getElementById('coin_from'); + const selectedOption = coinFromSelect?.querySelector(`option[value="${state.filters.coin_from}"]`); + const coinName = selectedOption?.textContent.trim(); + + if (coinName) { + const coinToMatch = state.currentTab === 'sent' ? bid.coin_from : bid.coin_to; + if (!coinMatches(coinToMatch, coinName)) { + return false; + } + } + } + + if (state.filters.coin_to !== 'any') { + const coinToSelect = document.getElementById('coin_to'); + const selectedOption = coinToSelect?.querySelector(`option[value="${state.filters.coin_to}"]`); + const coinName = selectedOption?.textContent.trim(); + + if (coinName) { + const coinToMatch = state.currentTab === 'sent' ? bid.coin_to : bid.coin_from; + if (!coinMatches(coinToMatch, coinName)) { + return false; + } + } + } + + if (state.filters.searchQuery) { + const searchStr = state.filters.searchQuery.toLowerCase(); + const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr); + const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr); + const identity = IdentityManager.cache.get(bid.addr_from); + const label = identity?.data?.label || ''; + const matchesLabel = label.toLowerCase().includes(searchStr); + + if (!(matchesBidId || matchesIdentity || matchesLabel)) { + return false; + } + } + + return true; + }).sort((a, b) => { + if (state.filters.sort_by === 'created_at') { + const direction = state.filters.sort_dir === 'asc' ? 1 : -1; + return direction * (a.created_at - b.created_at); + } + return 0; + }); +} + +function updateCoinFilterImages() { + const coinToSelect = document.getElementById('coin_to'); + const coinFromSelect = document.getElementById('coin_from'); + const coinToButton = document.getElementById('coin_to_button'); + const coinFromButton = document.getElementById('coin_from_button'); + + function updateButtonImage(select, button) { + if (!select || !button) return; + + const selectedOption = select.options[select.selectedIndex]; + const imagePath = selectedOption.getAttribute('data-image'); + + if (imagePath && select.value !== 'any') { + button.style.backgroundImage = `url(${imagePath})`; + button.style.backgroundSize = '25px'; + button.style.backgroundRepeat = 'no-repeat'; + button.style.backgroundPosition = 'center'; + + } else { + button.style.backgroundImage = 'none'; + button.style.opacity = '1'; + } + } + + updateButtonImage(coinToSelect, coinToButton); + updateButtonImage(coinFromSelect, coinFromButton); +} + +const updateLoadingState = (isLoading) => { + state.isLoading = isLoading; + + ['Sent', 'Received'].forEach(type => { + const refreshButton = elements[`refresh${type}Bids`]; + const refreshText = refreshButton?.querySelector(`#refresh${type}Text`); + const refreshIcon = refreshButton?.querySelector('svg'); + + if (refreshButton) { + refreshButton.disabled = isLoading; + if (isLoading) { + refreshButton.classList.add('opacity-75', 'cursor-wait'); + } else { + refreshButton.classList.remove('opacity-75', 'cursor-wait'); + } + } + + if (refreshIcon) { + if (isLoading) { + refreshIcon.classList.add('animate-spin'); + refreshIcon.style.transform = 'rotate(0deg)'; + } else { + refreshIcon.classList.remove('animate-spin'); + refreshIcon.style.transform = ''; + } + } + + if (refreshText) { + refreshText.textContent = isLoading ? 'Refreshing...' : 'Refresh'; + } + }); +}; + +const updateConnectionStatus = (status) => { + const statusConfig = { + connected: { + dotClass: 'w-2.5 h-2.5 rounded-full bg-green-500 mr-2', + textClass: 'text-sm text-green-500', + message: 'Connected' + }, + disconnected: { + dotClass: 'w-2.5 h-2.5 rounded-full bg-red-500 mr-2', + textClass: 'text-sm text-red-500', + message: 'Disconnected - Reconnecting...' + }, + error: { + dotClass: 'w-2.5 h-2.5 rounded-full bg-yellow-500 mr-2', + textClass: 'text-sm text-yellow-500', + message: 'Connection Error' + } + }; + + const config = statusConfig[status] || statusConfig.connected; + + ['sent', 'received'].forEach(type => { + const dot = elements[`statusDot${type.charAt(0).toUpperCase() + type.slice(1)}`]; + const text = elements[`statusText${type.charAt(0).toUpperCase() + type.slice(1)}`]; + + if (dot && text) { + dot.className = config.dotClass; + text.className = config.textClass; + text.textContent = config.message; + } + }); +}; + +// Identity +const IdentityManager = { + cache: new Map(), + pendingRequests: new Map(), + retryDelay: 2000, + maxRetries: 3, + cacheTimeout: 5 * 60 * 1000, + + async getIdentityData(address) { + if (!address) return { address: '' }; + + const cachedData = this.getCachedIdentity(address); + if (cachedData) return { ...cachedData, address }; + + if (this.pendingRequests.has(address)) { + const pendingData = await this.pendingRequests.get(address); + return { ...pendingData, address }; + } + + const request = this.fetchWithRetry(address); + this.pendingRequests.set(address, request); + + try { + const data = await request; + this.cache.set(address, { + data, + timestamp: Date.now() + }); + return { ...data, address }; + } catch (error) { + console.warn(`Error fetching identity for ${address}:`, error); + return { address }; + } finally { + this.pendingRequests.delete(address); + } + }, + + getCachedIdentity(address) { + const cached = this.cache.get(address); + if (cached && (Date.now() - cached.timestamp) < this.cacheTimeout) { + return cached.data; + } + if (cached) { + this.cache.delete(address); + } + return null; + }, + + async fetchWithRetry(address, attempt = 1) { + try { + const response = await fetch(`/json/identities/${address}`); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return await response.json(); + } catch (error) { + if (attempt >= this.maxRetries) { + console.warn(`Failed to fetch identity for ${address} after ${attempt} attempts`); + return { address }; + } + await new Promise(resolve => setTimeout(resolve, this.retryDelay * attempt)); + return this.fetchWithRetry(address, attempt + 1); + } + } +}; + +// Stats +const processIdentityStats = (identity) => { + if (!identity) return null; + + const stats = { + sentSuccessful: safeParseInt(identity.num_sent_bids_successful), + recvSuccessful: safeParseInt(identity.num_recv_bids_successful), + sentFailed: safeParseInt(identity.num_sent_bids_failed), + recvFailed: safeParseInt(identity.num_recv_bids_failed), + sentRejected: safeParseInt(identity.num_sent_bids_rejected), + recvRejected: safeParseInt(identity.num_recv_bids_rejected) + }; + + stats.totalSuccessful = stats.sentSuccessful + stats.recvSuccessful; + stats.totalFailed = stats.sentFailed + stats.recvFailed; + stats.totalRejected = stats.sentRejected + stats.recvRejected; + stats.totalBids = stats.totalSuccessful + stats.totalFailed + stats.totalRejected; + + stats.successRate = stats.totalBids > 0 + ? ((stats.totalSuccessful / stats.totalBids) * 100).toFixed(1) + : '0.0'; + + return stats; +}; + +const createIdentityTooltipContent = (identity) => { + if (!identity) return ''; + + const stats = processIdentityStats(identity); + if (!stats) return ''; + + const getSuccessRateColor = (rate) => { + const numRate = parseFloat(rate); + if (numRate >= 80) return 'text-green-600'; + if (numRate >= 60) return 'text-yellow-600'; + return 'text-red-600'; + }; + + return ` +
+ ${identity.label ? ` +
+
Label:
+
${identity.label}
+
+ ` : ''} + +
+
Bid From Address:
+
+ ${identity.address || ''} +
+
+ + ${identity.note ? ` +
+
Note:
+
${identity.note}
+
+ ` : ''} + +
+
Swap History:
+
+
+
+ ${stats.successRate}% +
+
Success Rate
+
+
+
${stats.totalBids}
+
Total Trades
+
+
+
+
+
+ ${stats.totalSuccessful} +
+
Successful
+
+
+
+ ${stats.totalRejected} +
+
Rejected
+
+
+
+ ${stats.totalFailed} +
+
Failed
+
+
+
+
+ `; +}; + +// Table +const createTableRow = async (bid) => { + const identity = await IdentityManager.getIdentityData(bid.addr_from); + const uniqueId = `${bid.bid_id}_${Date.now()}`; + const timeColor = getTimeStrokeColor(bid.expire_at); + + return ` + + + +
+ + + + + + +
${formatTime(bid.created_at)}
+
+ + + + + +
+
+ ${state.currentTab === 'sent' ? 'Out' : 'In'} + +
+ +
+ + + + +
+ ${bid.coin_from} +
+
${bid.amount_from}
+
${bid.coin_from}
+
+
+ + + + +
+ ${bid.coin_to} +
+
${bid.amount_to}
+
${bid.coin_to}
+
+
+ + + + +
+ + ${bid.bid_state} + +
+ + + + + + View Bid + + + + + + + + + `; +}; + +const updateTableContent = async (type) => { + const tbody = elements[`${type}BidsBody`]; + if (!tbody) return; + + const filteredData = state.data[type]; + + const startIndex = (state.currentPage[type] - 1) * PAGE_SIZE; + const endIndex = startIndex + PAGE_SIZE; + const currentPageData = filteredData.slice(startIndex, endIndex); + + console.log('Updating table content:', { + type: type, + totalFilteredBids: filteredData.length, + currentPageBids: currentPageData.length, + startIndex: startIndex, + endIndex: endIndex + }); + + if (currentPageData.length > 0) { + const rowPromises = currentPageData.map(bid => createTableRow(bid)); + const rows = await Promise.all(rowPromises); + tbody.innerHTML = rows.join(''); + initializeTooltips(); + } else { + tbody.innerHTML = ` + + + No ${type} bids found + + `; + } + + updatePaginationControls(type); +}; + +const initializeTooltips = () => { + if (window.TooltipManager) { + window.TooltipManager.cleanup(); + + const tooltipTriggers = document.querySelectorAll('[data-tooltip-target]'); + tooltipTriggers.forEach(trigger => { + const targetId = trigger.getAttribute('data-tooltip-target'); + const tooltipContent = document.getElementById(targetId); + + if (tooltipContent) { + window.TooltipManager.create(trigger, tooltipContent.innerHTML, { + placement: trigger.getAttribute('data-tooltip-placement') || 'top', + interactive: true, + animation: 'shift-away', + maxWidth: 400, + allowHTML: true, + offset: [0, 8], + zIndex: 50 + }); + } + }); + } +}; + +// Fetching +const fetchBids = async () => { + try { + const endpoint = state.currentTab === 'sent' ? '/json/sentbids' : '/json/bids'; + const withExpiredSelect = document.getElementById('with_expired'); + const includeExpired = withExpiredSelect ? withExpiredSelect.value === 'true' : true; + + console.log('Fetching bids, include expired:', includeExpired); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify({ + sort_by: state.filters.sort_by || 'created_at', + sort_dir: state.filters.sort_dir || 'desc', + with_expired: true, // Always fetch all bids + state: state.filters.state ?? -1, + with_extra_info: true + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + let data = await response.json(); + console.log('Received raw data:', data.length, 'bids'); + + state.filters.with_expired = includeExpired; + + data = filterAndSortData(data); + + return data; + } catch (error) { + console.error('Error in fetchBids:', error); + throw error; + } +}; + +const updateBidsTable = async () => { + if (state.isLoading) { + console.log('Already loading, skipping update'); + return; + } + + try { + console.log('Starting updateBidsTable for tab:', state.currentTab); + console.log('Current filters:', state.filters); + + state.isLoading = true; + updateLoadingState(true); + + const bids = await fetchBids(); + + console.log('Fetched bids:', bids.length); + + const filteredBids = bids.filter(bid => { + // State filter + if (state.filters.state !== -1) { + const allowedStates = STATE_MAP[state.filters.state] || []; + if (allowedStates.length > 0 && !allowedStates.includes(bid.bid_state)) { + return false; + } + } + + const now = Math.floor(Date.now() / 1000); + if (!state.filters.with_expired && bid.expire_at <= now) { + return false; + } + + let yourCoinMatch = true; + let theirCoinMatch = true; + + if (state.filters.coin_from !== 'any') { + const coinFromSelect = document.getElementById('coin_from'); + const selectedOption = coinFromSelect?.querySelector(`option[value="${state.filters.coin_from}"]`); + const coinName = selectedOption?.textContent.trim(); + + if (coinName) { + const coinToMatch = state.currentTab === 'sent' ? bid.coin_from : bid.coin_to; + yourCoinMatch = coinMatches(coinToMatch, coinName); + console.log('Your Coin filtering:', { + filterCoin: coinName, + bidCoin: coinToMatch, + matches: yourCoinMatch + }); + } + } + + if (state.filters.coin_to !== 'any') { + const coinToSelect = document.getElementById('coin_to'); + const selectedOption = coinToSelect?.querySelector(`option[value="${state.filters.coin_to}"]`); + const coinName = selectedOption?.textContent.trim(); + + if (coinName) { + const coinToMatch = state.currentTab === 'sent' ? bid.coin_to : bid.coin_from; + theirCoinMatch = coinMatches(coinToMatch, coinName); + console.log('Their Coin filtering:', { + filterCoin: coinName, + bidCoin: coinToMatch, + matches: theirCoinMatch + }); + } + } + + if (!yourCoinMatch || !theirCoinMatch) { + return false; + } + + if (state.filters.searchQuery) { + const searchStr = state.filters.searchQuery.toLowerCase(); + const matchesBidId = bid.bid_id.toLowerCase().includes(searchStr); + const matchesIdentity = bid.addr_from?.toLowerCase().includes(searchStr); + + const identity = IdentityManager.cache.get(bid.addr_from); + const label = identity?.data?.label || ''; + const matchesLabel = label.toLowerCase().includes(searchStr); + + if (!(matchesBidId || matchesIdentity || matchesLabel)) { + return false; + } + } + + return true; + }); + + console.log('Filtered bids:', filteredBids.length); + + filteredBids.sort((a, b) => { + const direction = state.filters.sort_dir === 'asc' ? 1 : -1; + if (state.filters.sort_by === 'created_at') { + return direction * (a.created_at - b.created_at); + } + return 0; + }); + + state.data[state.currentTab] = filteredBids; + state.currentPage[state.currentTab] = 1; + + await updateTableContent(state.currentTab); + updatePaginationControls(state.currentTab); + + } catch (error) { + console.error('Error in updateBidsTable:', error); + updateConnectionStatus('error'); + } finally { + state.isLoading = false; + updateLoadingState(false); + } +}; + +const updatePaginationControls = (type) => { + const data = state.data[type] || []; + const totalPages = Math.ceil(data.length / PAGE_SIZE); + const controls = elements[`${type}PaginationControls`]; + const prevButton = elements[`prevPage${type.charAt(0).toUpperCase() + type.slice(1)}`]; + const nextButton = elements[`nextPage${type.charAt(0).toUpperCase() + type.slice(1)}`]; + const currentPageSpan = elements[`currentPage${type.charAt(0).toUpperCase() + type.slice(1)}`]; + const bidsCount = elements[`${type}BidsCount`]; + + console.log('Pagination controls update:', { + type: type, + totalBids: data.length, + totalPages: totalPages, + currentPage: state.currentPage[type] + }); + + if (state.currentPage[type] > totalPages) { + state.currentPage[type] = totalPages; + } + + if (controls) { + controls.style.display = totalPages > 1 ? 'flex' : 'none'; + } + + if (currentPageSpan) { + currentPageSpan.textContent = totalPages > 0 ? state.currentPage[type] : 0; + } + + if (prevButton) { + prevButton.style.display = state.currentPage[type] > 1 ? 'inline-flex' : 'none'; + } + + if (nextButton) { + nextButton.style.display = state.currentPage[type] < totalPages ? 'inline-flex' : 'none'; + } + + if (bidsCount) { + bidsCount.textContent = data.length; + } +}; + +// Filter +let searchTimeout; +function handleSearch(event) { + if (searchTimeout) { + clearTimeout(searchTimeout); + } + + searchTimeout = setTimeout(() => { + state.filters.searchQuery = event.target.value.toLowerCase(); + updateBidsTable(); + updateClearFiltersButton(); + }, 300); +} + +function clearFilters() { + if (!hasActiveFilters()) return; + + const filterElements = { + stateSelect: document.getElementById('state'), + withExpiredSelect: document.getElementById('with_expired'), + coinFrom: document.getElementById('coin_from'), + coinTo: document.getElementById('coin_to'), + searchInput: document.getElementById('searchInput') + }; + + if (filterElements.stateSelect) filterElements.stateSelect.value = '-1'; + if (filterElements.withExpiredSelect) filterElements.withExpiredSelect.value = 'true'; + if (filterElements.coinFrom) filterElements.coinFrom.value = 'any'; + if (filterElements.coinTo) filterElements.coinTo.value = 'any'; + if (filterElements.searchInput) filterElements.searchInput.value = ''; + + state.filters = { + state: -1, + sort_by: 'created_at', + sort_dir: 'desc', + with_expired: true, + searchQuery: '', + coin_from: 'any', + coin_to: 'any' + }; + + localStorage.removeItem('bidsTableSettings'); + updateCoinFilterImages(); + updateBidsTable(); + updateClearFiltersButton(); +} + +function applyFilters() { + const stateSelect = document.getElementById('state'); + const sortBySelect = document.getElementById('sort_by'); + const sortDirSelect = document.getElementById('sort_dir'); + const withExpiredSelect = document.getElementById('with_expired'); + const coinFromSelect = document.getElementById('coin_from'); + const coinToSelect = document.getElementById('coin_to'); + const searchInput = document.getElementById('searchInput'); + + state.filters = { + state: stateSelect ? parseInt(stateSelect.value) : -1, + sort_by: sortBySelect ? sortBySelect.value : 'created_at', + sort_dir: sortDirSelect ? sortDirSelect.value : 'desc', + with_expired: withExpiredSelect ? withExpiredSelect.value === 'true' : true, + searchQuery: searchInput ? searchInput.value.toLowerCase() : '', + coin_from: coinFromSelect ? coinFromSelect.value : 'any', + coin_to: coinToSelect ? coinToSelect.value : 'any' + }; + + updateBidsTable(); + updateClearFiltersButton(); +} + +function updateClearFiltersButton() { + const clearButton = document.getElementById('clearFilters'); + if (clearButton) { + const hasFilters = hasActiveFilters(); + + clearButton.disabled = !hasFilters; + + if (hasFilters) { + clearButton.classList.remove('opacity-50', 'cursor-not-allowed', 'bg-gray-300'); + clearButton.classList.add('hover:bg-green-600', 'hover:text-white', 'bg-coolGray-200'); + } else { + clearButton.classList.add('opacity-50', 'cursor-not-allowed', 'bg-gray-300'); + clearButton.classList.remove('hover:bg-green-600', 'hover:text-white', 'bg-coolGray-200'); + } + } +} + +const handleFilterChange = (e) => { + if (e) e.preventDefault(); + + state.filters = { + state: parseInt(elements.stateSelect.value), + sort_by: elements.sortBySelect.value, + sort_dir: elements.sortDirSelect.value, + with_expired: elements.withExpiredSelect.value === 'true' + }; + + state.currentPage[state.currentTab] = 1; + + updateBidsTable(); +}; + +function setupFilterEventListeners() { + const coinToSelect = document.getElementById('coin_to'); + const coinFromSelect = document.getElementById('coin_from'); + const withExpiredSelect = document.getElementById('with_expired'); + + if (coinToSelect) { + coinToSelect.addEventListener('change', () => { + state.filters.coin_to = coinToSelect.value; + updateBidsTable(); + updateCoinFilterImages(); + updateClearFiltersButton(); + }); + } + + if (coinFromSelect) { + coinFromSelect.addEventListener('change', () => { + state.filters.coin_from = coinFromSelect.value; + updateBidsTable(); + updateCoinFilterImages(); + updateClearFiltersButton(); + }); + } + + if (withExpiredSelect) { + withExpiredSelect.addEventListener('change', () => { + state.filters.with_expired = withExpiredSelect.value === 'true'; + updateBidsTable(); + updateClearFiltersButton(); + }); + } + + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.addEventListener('input', (event) => { + if (searchTimeout) { + clearTimeout(searchTimeout); + } + + searchTimeout = setTimeout(() => { + state.filters.searchQuery = event.target.value.toLowerCase(); + updateBidsTable(); + updateClearFiltersButton(); + }, 300); + }); + } +} + +// Tabs +const switchTab = (tabId) => { + if (state.isLoading) return; + + state.currentTab = tabId === '#sent' ? 'sent' : 'received'; + + elements.sentContent.classList.add('hidden'); + elements.receivedContent.classList.add('hidden'); + + const targetPanel = document.querySelector(tabId); + if (targetPanel) { + targetPanel.classList.remove('hidden'); + } + + elements.tabButtons.forEach(tab => { + const selected = tab.dataset.tabsTarget === tabId; + tab.setAttribute('aria-selected', selected); + if (selected) { + tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white'); + tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); + } else { + tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white'); + tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); + } + }); + + updateBidsTable(); +}; + +const setupEventListeners = () => { + const filterControls = document.querySelector('.flex.flex-wrap.justify-center'); + if (filterControls) { + filterControls.addEventListener('submit', (e) => { + e.preventDefault(); + }); + } + + const applyFiltersBtn = document.getElementById('applyFilters'); + if (applyFiltersBtn) { + applyFiltersBtn.remove(); + } + + if (elements.tabButtons) { + elements.tabButtons.forEach(button => { + button.addEventListener('click', () => { + if (state.isLoading) return; + + const targetId = button.getAttribute('data-tabs-target'); + if (!targetId) return; + + // Update tab button styles + elements.tabButtons.forEach(tab => { + const isSelected = tab.getAttribute('data-tabs-target') === targetId; + tab.setAttribute('aria-selected', isSelected); + + if (isSelected) { + tab.classList.add('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white'); + tab.classList.remove('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); + } else { + tab.classList.remove('bg-gray-100', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white'); + tab.classList.add('hover:text-gray-600', 'hover:bg-gray-50', 'dark:hover:bg-gray-500'); + } + }); + + elements.sentContent.classList.toggle('hidden', targetId !== '#sent'); + elements.receivedContent.classList.toggle('hidden', targetId !== '#received'); + + state.currentTab = targetId === '#sent' ? 'sent' : 'received'; + state.currentPage[state.currentTab] = 1; + + updateBidsTable(); + }); + }); + } + + ['Sent', 'Received'].forEach(type => { + const lowerType = type.toLowerCase(); + + if (elements[`prevPage${type}`]) { + elements[`prevPage${type}`].addEventListener('click', () => { + if (state.isLoading) return; + if (state.currentPage[lowerType] > 1) { + state.currentPage[lowerType]--; + updateTableContent(lowerType); + updatePaginationControls(lowerType); + } + }); + } + + if (elements[`nextPage${type}`]) { + elements[`nextPage${type}`].addEventListener('click', () => { + if (state.isLoading) return; + const totalPages = Math.ceil(state.data[lowerType].length / PAGE_SIZE); + if (state.currentPage[lowerType] < totalPages) { + state.currentPage[lowerType]++; + updateTableContent(lowerType); + updatePaginationControls(lowerType); + } + }); + } + }); + + const searchInput = document.getElementById('searchInput'); + if (searchInput) { + searchInput.addEventListener('input', handleSearch); + } + + const coinToSelect = document.getElementById('coin_to'); + const coinFromSelect = document.getElementById('coin_from'); + + if (coinToSelect) { + coinToSelect.addEventListener('change', () => { + state.filters.coin_to = coinToSelect.value; + updateBidsTable(); + updateCoinFilterImages(); + }); + } + + if (coinFromSelect) { + coinFromSelect.addEventListener('change', () => { + state.filters.coin_from = coinFromSelect.value; + updateBidsTable(); + updateCoinFilterImages(); + }); + } + + const filterElements = { + stateSelect: document.getElementById('state'), + sortBySelect: document.getElementById('sort_by'), + sortDirSelect: document.getElementById('sort_dir'), + withExpiredSelect: document.getElementById('with_expired'), + clearFiltersBtn: document.getElementById('clearFilters') + }; + + if (filterElements.stateSelect) { + filterElements.stateSelect.addEventListener('change', () => { + const stateValue = parseInt(filterElements.stateSelect.value); + + state.filters.state = isNaN(stateValue) ? -1 : stateValue; + + console.log('State filter changed:', { + selectedValue: filterElements.stateSelect.value, + parsedState: state.filters.state + }); + + updateBidsTable(); + updateClearFiltersButton(); + }); + } + + [ + filterElements.sortBySelect, + filterElements.sortDirSelect, + filterElements.withExpiredSelect + ].forEach(element => { + if (element) { + element.addEventListener('change', () => { + updateBidsTable(); + updateClearFiltersButton(); + }); + } + }); + + if (filterElements.clearFiltersBtn) { + filterElements.clearFiltersBtn.addEventListener('click', () => { + if (filterElements.clearFiltersBtn.disabled) return; + clearFilters(); + }); + } + + initializeTooltips(); + + document.addEventListener('change', (event) => { + const target = event.target; + const filterForm = document.querySelector('.flex.flex-wrap.justify-center'); + + if (filterForm && filterForm.contains(target)) { + const formData = { + state: filterElements.stateSelect?.value, + sort_by: filterElements.sortBySelect?.value, + sort_dir: filterElements.sortDirSelect?.value, + with_expired: filterElements.withExpiredSelect?.value, + coin_from: coinFromSelect?.value, + coin_to: coinToSelect?.value, + searchQuery: searchInput?.value + }; + + localStorage.setItem('bidsTableSettings', JSON.stringify(formData)); + } + }); + + const savedSettings = localStorage.getItem('bidsTableSettings'); + if (savedSettings) { + const settings = JSON.parse(savedSettings); + + Object.entries(settings).forEach(([key, value]) => { + const element = document.querySelector(`[name="${key}"]`); + if (element) { + element.value = value; + } + }); + + state.filters = { + state: settings.state ? parseInt(settings.state) : -1, + sort_by: settings.sort_by || 'created_at', + sort_dir: settings.sort_dir || 'desc', + with_expired: settings.with_expired === 'true', + searchQuery: settings.searchQuery || '', + coin_from: settings.coin_from || 'any', + coin_to: settings.coin_to || 'any' + }; + } + + updateCoinFilterImages(); + updateClearFiltersButton(); +}; + +const setupRefreshButtons = () => { + ['Sent', 'Received'].forEach(type => { + const refreshButton = elements[`refresh${type}Bids`]; + if (refreshButton) { + refreshButton.addEventListener('click', async () => { + const lowerType = type.toLowerCase(); + + if (state.isRefreshing) { + console.log('Already refreshing, skipping'); + return; + } + + try { + state.isRefreshing = true; + state.isLoading = true; + updateLoadingState(true); + + const response = await fetch(state.currentTab === 'sent' ? '/json/sentbids' : '/json/bids', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + sort_by: state.filters.sort_by, + sort_dir: state.filters.sort_dir, + with_expired: state.filters.with_expired, + state: state.filters.state, + with_extra_info: true + }) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + if (!Array.isArray(data)) { + throw new Error('Invalid response format'); + } + + state.data[lowerType] = data; + await updateTableContent(lowerType); + updatePaginationControls(lowerType); + + } catch (error) { + console.error(`Error refreshing ${type} bids:`, error); + } finally { + state.isLoading = false; + updateLoadingState(false); + } + }); + } + }); +}; + +// Init +document.addEventListener('DOMContentLoaded', () => { + const filterElements = { + stateSelect: document.getElementById('state'), + sortBySelect: document.getElementById('sort_by'), + sortDirSelect: document.getElementById('sort_dir'), + withExpiredSelect: document.getElementById('with_expired'), + coinFrom: document.getElementById('coin_from'), + coinTo: document.getElementById('coin_to') + }; + + if (filterElements.stateSelect) filterElements.stateSelect.value = '-1'; + if (filterElements.sortBySelect) filterElements.sortBySelect.value = 'created_at'; + if (filterElements.sortDirSelect) filterElements.sortDirSelect.value = 'desc'; + if (filterElements.withExpiredSelect) filterElements.withExpiredSelect.value = 'true'; + if (filterElements.coinFrom) filterElements.coinFrom.value = 'any'; + if (filterElements.coinTo) filterElements.coinTo.value = 'any'; + + WebSocketManager.initialize(); + setupEventListeners(); + setupRefreshButtons(); + setupFilterEventListeners(); + + updateClearFiltersButton(); + state.currentTab = 'sent'; + state.filters.state = -1; + updateBidsTable(); +}); diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offers.js similarity index 100% rename from basicswap/static/js/offerstable.js rename to basicswap/static/js/offers.js diff --git a/basicswap/templates/bids.html b/basicswap/templates/bids.html index 3254e63..ea27b9f 100644 --- a/basicswap/templates/bids.html +++ b/basicswap/templates/bids.html @@ -1,412 +1,348 @@ {% include 'header.html' %} -{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg %} +{% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, circular_arrows_svg, input_arrow_down_svg, arrow_right_svg %}
-
-
-
- -
-
-
- -
-
-
- - - -
-
-

Sent Bids / Received Bids

-

View, and manage bids

-
-
-
-
-
- - {% include 'inc_messages.html' %} - -
-
-
-
-
-
    -
  • - -
  • -
  • - -
  • -
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
- {{ input_arrow_down_svg | safe }} - -
-
-
-
- {{ input_arrow_down_svg | safe }} - -
-
-
-
-

State:

-
-
-
-
- {{ input_arrow_down_svg | safe }} - -
-
-
-
-

Include Expired:

-
-
-
-
- {{ input_arrow_down_svg | safe }} - -
-
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
-
-
-
-
-
-
- - - - - - - - - - - - - - - {% for b in sent_bids %} - - - - - - - - - - - {% endfor %} - -
-
- Date/Time at -
-
-
- Bid ID -
-
-
- Offer ID -
-
-
- Bid From -
-
-
- Bid Status -
-
-
- ITX Status -
-
-
- PTX Status -
-
-
- Details -
-
- - - - - - -
-
{{ b[0] }}
-
-
{{ b[1]|truncate(20, True) }}{{ b[2]|truncate(20, True) }}{{ b[6] }}{{ b[3] }}{{ b[4] }}{{ b[5] }} - Details -
-
-
-
-
- {% if filters.page_no > 1 %} -
- - - -
- {% endif %} -
-
-

Page: {{ filters.page_no }}

-
-
- {% if sent_bids_count >= filters.limit %} -
- - - -
- {% endif %} -
-
-
-
-
-
-
-
- - -
- - - - -
-
-
+
+
+
+ + + +
+
+

Sent Bids / Received Bids

+

View, and manage bids.

+
+
+
+ + {% include 'inc_messages.html' %} + +
+
+
+
+
+
    +
  • + +
  • +
  • + +
  • +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+ + + +
+
+
+ +
+
+ +
+ {{ input_arrow_down_svg | safe }} + +
+
+
+

{{ arrow_right_svg | safe }}

+
+
+ +
+ {{ input_arrow_down_svg | safe }} + +
+
+
+ +
+
+ {{ input_arrow_down_svg | safe }} + +
+
+ + + +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + + +
+
+ Date/Time +
+
+
+ Details +
+
+
+ You Send +
+
+
+ You Receive +
+
+
+ Status +
+
+
+ Actions +
+
+
+
+
+
+
+
+ + Connecting... +
+

+ Sent Bids: 0 +

+ {% if debug_ui_mode == true %} + + {% endif %} +
+ +
+
+
+
+
+
+
+
+ + + +
- + {% include 'footer.html' %} diff --git a/basicswap/templates/bids_available.html b/basicswap/templates/bids_available.html index 2182201..531611b 100644 --- a/basicswap/templates/bids_available.html +++ b/basicswap/templates/bids_available.html @@ -1,200 +1,118 @@ {% include 'header.html' %} {% from 'style.html' import breadcrumb_line_svg, page_back_svg, page_forwards_svg, filter_clear_svg, filter_apply_svg, input_arrow_down_svg %} -
-
-
-
- -
-
-
- -
-
-
- - - -
-
-

Bids Requests

-

Review and accept bids from other users

-
-
-
-
-
- - {% include 'inc_messages.html' %} - -
-
-
-
-
-
-
-
-
-
-
- {{ input_arrow_down_svg | safe }} - -
-
-
-
- {{ input_arrow_down_svg | safe }} - -
-
-
-
- -
-
-
-
- -
-
-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - {% for b in bids %} - - - - - - - - - {% endfor %} - -
-
- Date/Time at -
-
-
- Bid ID -
-
-
- Offer ID -
-
-
- From -
-
-
- Status -
-
-
- Actions -
-
- - - - - - -
-
{{ b[0] }}
-
-
{{ b[1]|truncate(20, True) }}{{ b[2]|truncate(20, True) }}{{ b[6] }}{{ b[3] }} -
- View - {% if b[3] == "Received" %} - - - - - {% endif %} -
-
-
-
-
- {% if filters.page_no > 1 %} -
- - - -
- {% endif %} -
-
-

Page: {{ filters.page_no }}

-
-
- {% if bids_count >= filters.limit %} -
- - - -
- {% endif %} -
-
-
-
-
-
-
- - - - - - +
+
+
+ + + +
+
+

Bid Requests

+

Review and accept bids from other users.

-
+ + +{% include 'inc_messages.html' %} + +
+
+
+
+
+ + + + + + + + + + + + + + +
+
+ +
+
+
+ Time +
+
+
+ You Send +
+
+
+ Swap +
+
+
+ You Get +
+
+
+ Rate +
+
+
+ Actions +
+
+
+
+
+
+
+
+
+ + Connecting... +
+

Available Bids: 0

+ {% if debug_ui_mode == true %} + + {% endif %} + +
+
+
+
+
+
+
+ + {% include 'footer.html' %} diff --git a/basicswap/templates/header.html b/basicswap/templates/header.html index 1213794..a5db479 100644 --- a/basicswap/templates/header.html +++ b/basicswap/templates/header.html @@ -449,7 +449,7 @@ - {{ summary.num_recv_active_bids }} + {{ summary.num_available_bids }} diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html index 09115ca..baa71ee 100644 --- a/basicswap/templates/offers.html +++ b/basicswap/templates/offers.html @@ -24,43 +24,24 @@ function getWebSocketConfig() { } -{% if sent_offers %} -
-
-
-
- -
-
-
-
-{% endif %} - -{% if sent_offers %} -
-{% else %} -
-{% endif %} +
-
- - wave -
-
-

{{ page_type }}

+
+ + + + @@ -442,5 +423,5 @@ function getWebSocketConfig() {
- + {% include 'footer.html' %} diff --git a/basicswap/templates/unlock.html b/basicswap/templates/unlock.html index 6bcb9d7..3feeb3c 100644 --- a/basicswap/templates/unlock.html +++ b/basicswap/templates/unlock.html @@ -10,7 +10,6 @@ -