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 504d708..ea72870 100644
Binary files a/basicswap/static/images/coins/Wownero.png and b/basicswap/static/images/coins/Wownero.png differ
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 `
+
+ |
+
+ |
+
+
+
+
+
+
+
+ Posted: ${formatTimeAgo(bid.created_at)}
+
+
+ Expires in: ${formatTime(bid.expire_at)}
+
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+ ${fromAmount.toFixed(8)}
+ ${bid.coin_from}
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ ${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'}
+
+
+
+
+ |
+
+
+
+
+ }.png)
+
+ ${bid.amount_from}
+ ${bid.coin_from}
+
+
+ |
+
+
+
+
+ }.png)
+
+ ${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' %}
-
-
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+

+

+

+
+
+
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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ Date/Time
+
+ |
+
+
+ Details
+
+ |
+
+
+ You Send
+
+ |
+
+
+ You Receive
+
+ |
+
+
+ Status
+
+ |
+
+
+ Actions
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ Connecting...
+
+
+ Received 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' %}
-
-
-
-
-
-
+
+
+
+

+

+

+
+
+
Bid Requests
+
Review and accept bids from other users.
-
+
+
+{% include 'inc_messages.html' %}
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+ Time
+
+ |
+
+
+ Details
+
+ |
+
+
+ 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 %}
+
-
-
-

-
-
-
{{ page_type }}
+
+

+

+

+
+
+
{{ page_type }}
{{ page_type_description }}
-
@@ -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 @@
-