diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py
index 2367f04..05c28b4 100644
--- a/basicswap/basicswap.py
+++ b/basicswap/basicswap.py
@@ -1329,39 +1329,43 @@ class BasicSwap(BaseApp):
self.closeDB(cursor)
def updateIdentityBidState(self, cursor, address: str, bid) -> None:
- identity_stats = self.queryOne(KnownIdentity, cursor, {"address": address})
- if not identity_stats:
- identity_stats = KnownIdentity(
- active_ind=1, address=address, created_at=self.getTime()
- )
-
- if bid.state == BidStates.SWAP_COMPLETED:
- if bid.was_sent:
- identity_stats.num_sent_bids_successful = (
- zeroIfNone(identity_stats.num_sent_bids_successful) + 1
+ offer = self.getOffer(bid.offer_id, cursor)
+ addresses_to_update = [offer.addr_from, bid.bid_addr]
+ for addr in addresses_to_update:
+ identity_stats = self.queryOne(KnownIdentity, cursor, {"address": addr})
+ if not identity_stats:
+ identity_stats = KnownIdentity(
+ active_ind=1,
+ address=addr,
+ created_at=self.getTime()
)
- else:
- identity_stats.num_recv_bids_successful = (
- zeroIfNone(identity_stats.num_recv_bids_successful) + 1
- )
- elif bid.state in (
- BidStates.BID_ERROR,
- BidStates.XMR_SWAP_FAILED_REFUNDED,
- BidStates.XMR_SWAP_FAILED_SWIPED,
- BidStates.XMR_SWAP_FAILED,
- BidStates.SWAP_TIMEDOUT,
- ):
- if bid.was_sent:
- identity_stats.num_sent_bids_failed = (
- zeroIfNone(identity_stats.num_sent_bids_failed) + 1
- )
- else:
- identity_stats.num_recv_bids_failed = (
- zeroIfNone(identity_stats.num_recv_bids_failed) + 1
- )
-
- identity_stats.updated_at = self.getTime()
- self.add(identity_stats, cursor, upsert=True)
+ is_offer_creator = addr == offer.addr_from
+ if bid.state == BidStates.SWAP_COMPLETED:
+ if is_offer_creator:
+ old_value = zeroIfNone(identity_stats.num_recv_bids_successful)
+ identity_stats.num_recv_bids_successful = old_value + 1
+ else:
+ old_value = zeroIfNone(identity_stats.num_sent_bids_successful)
+ identity_stats.num_sent_bids_successful = old_value + 1
+ elif bid.state in (BidStates.BID_ERROR,
+ BidStates.XMR_SWAP_FAILED_REFUNDED,
+ BidStates.XMR_SWAP_FAILED_SWIPED,
+ BidStates.XMR_SWAP_FAILED,
+ BidStates.SWAP_TIMEDOUT):
+ if is_offer_creator:
+ old_value = zeroIfNone(identity_stats.num_recv_bids_failed)
+ identity_stats.num_recv_bids_failed = old_value + 1
+ else:
+ old_value = zeroIfNone(identity_stats.num_sent_bids_failed)
+ identity_stats.num_sent_bids_failed = old_value + 1
+ elif bid.state == BidStates.BID_REJECTED:
+ if is_offer_creator:
+ old_value = zeroIfNone(identity_stats.num_recv_bids_rejected)
+ identity_stats.num_recv_bids_rejected = old_value + 1
+ else:
+ old_value = zeroIfNone(identity_stats.num_sent_bids_rejected)
+ identity_stats.num_sent_bids_rejected = old_value + 1
+ self.add(identity_stats, cursor, upsert=True)
def getPreFundedTx(
self, linked_type: int, linked_id: bytes, tx_type: int, cursor=None
diff --git a/basicswap/js_server.py b/basicswap/js_server.py
index 16a1a4e..f9a887e 100644
--- a/basicswap/js_server.py
+++ b/basicswap/js_server.py
@@ -250,6 +250,7 @@ def js_offers(self, url_split, post_string, is_json, sent=False) -> bytes:
"is_expired": o.expire_at <= swap_client.getTime(),
"is_own_offer": o.was_sent,
"is_revoked": True if o.active_ind == 2 else False,
+ "is_public": o.addr_to == swap_client.network_addr or o.addr_to.strip() == "",
}
if with_extra_info:
offer_data["amount_negotiable"] = o.amount_negotiable
@@ -655,6 +656,23 @@ def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
swap_client = self.server.swap_client
swap_client.checkSystemStatus()
+ if len(url_split) > 3:
+ address = url_split[3]
+ identity = swap_client.getIdentity(address)
+ if identity:
+ return bytes(json.dumps({
+ "label": identity.label if identity.label is not None else "",
+ "note": identity.note if identity.note is not None else "",
+ "automation_override": identity.automation_override if identity.automation_override is not None else 0,
+ "num_sent_bids_successful": identity.num_sent_bids_successful if identity.num_sent_bids_successful is not None else 0,
+ "num_recv_bids_successful": identity.num_recv_bids_successful if identity.num_recv_bids_successful is not None else 0,
+ "num_sent_bids_rejected": identity.num_sent_bids_rejected if identity.num_sent_bids_rejected is not None else 0,
+ "num_recv_bids_rejected": identity.num_recv_bids_rejected if identity.num_recv_bids_rejected is not None else 0,
+ "num_sent_bids_failed": identity.num_sent_bids_failed if identity.num_sent_bids_failed is not None else 0,
+ "num_recv_bids_failed": identity.num_recv_bids_failed if identity.num_recv_bids_failed is not None else 0
+ }), "UTF-8")
+ return bytes(json.dumps({}), "UTF-8")
+
filters = {
"page_no": 1,
"limit": PAGE_LIMIT,
@@ -662,10 +680,6 @@ def js_identities(self, url_split, post_string: str, is_json: bool) -> bytes:
"sort_dir": "desc",
}
- if len(url_split) > 3:
- address = url_split[3]
- filters["address"] = address
-
if post_string != "":
post_data = getFormData(post_string, is_json)
diff --git a/basicswap/static/css/style.css b/basicswap/static/css/style.css
index 96bfb70..fd764b2 100644
--- a/basicswap/static/css/style.css
+++ b/basicswap/static/css/style.css
@@ -356,4 +356,14 @@ select.disabled-select-enabled {
#toggle-auto-refresh[data-enabled="true"] {
@apply bg-green-500 hover:bg-green-600 focus:ring-green-300;
}
+
+ [data-popper-placement] {
+ will-change: transform;
+ transform: translateZ(0);
+}
+
+.tooltip {
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
+}
diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js
index fce0a20..cec0722 100644
--- a/basicswap/static/js/offerstable.js
+++ b/basicswap/static/js/offerstable.js
@@ -102,7 +102,7 @@ const WebSocketManager = {
},
initialize() {
- console.log('π Initializing WebSocket Manager');
+ console.log('Initializing WebSocket Manager');
this.setupPageVisibilityHandler();
this.connect();
this.startHealthCheck();
@@ -152,7 +152,7 @@ const WebSocketManager = {
performHealthCheck() {
if (!this.isConnected()) {
- console.warn('π₯ Health check: Connection lost, attempting reconnect');
+ console.warn('Health check: Connection lost, attempting reconnect');
this.handleReconnect();
return;
}
@@ -160,13 +160,13 @@ const WebSocketManager = {
const now = Date.now();
const lastCheck = this.connectionState.lastHealthCheck;
if (lastCheck && (now - lastCheck) > 60000) {
- console.warn('π₯ Health check: Connection stale, refreshing');
+ console.warn('Health check: Connection stale, refreshing');
this.handleReconnect();
return;
}
this.connectionState.lastHealthCheck = now;
- console.log('β
Health check passed');
+ console.log('Health check passed');
},
connect() {
@@ -183,7 +183,7 @@ const WebSocketManager = {
const wsPort = config.port || window.ws_port || '11700';
if (!wsPort) {
- console.error('β WebSocket port not configured');
+ console.error('WebSocket port not configured');
this.connectionState.isConnecting = false;
return false;
}
@@ -201,7 +201,7 @@ const WebSocketManager = {
return true;
} catch (error) {
- console.error('β Error creating WebSocket:', error);
+ console.error('Error creating WebSocket:', error);
this.connectionState.isConnecting = false;
this.handleReconnect();
return false;
@@ -226,13 +226,13 @@ const WebSocketManager = {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
- console.error('β Error processing WebSocket message:', error);
+ console.error('Error processing WebSocket message:', error);
updateConnectionStatus('error');
}
};
this.ws.onerror = (error) => {
- console.error('β WebSocket error:', error);
+ console.error('WebSocket error:', error);
updateConnectionStatus('error');
};
@@ -250,7 +250,7 @@ const WebSocketManager = {
handleMessage(message) {
if (this.messageQueue.length >= this.maxQueueSize) {
- console.warn('β οΈ Message queue full, dropping oldest message');
+ console.warn('β Message queue full, dropping oldest message');
this.messageQueue.shift();
}
@@ -286,7 +286,7 @@ const WebSocketManager = {
this.messageQueue = [];
} catch (error) {
- console.error('β Error processing message queue:', error);
+ console.error('Error processing message queue:', error);
} finally {
this.processingQueue = false;
}
@@ -299,7 +299,7 @@ const WebSocketManager = {
this.reconnectAttempts++;
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
- console.log(`π Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
+ console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
const delay = Math.min(
this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
@@ -312,7 +312,7 @@ const WebSocketManager = {
}
}, delay);
} else {
- console.error('β Max reconnection attempts reached');
+ console.error('Max reconnection attempts reached');
updateConnectionStatus('error');
setTimeout(() => {
@@ -323,7 +323,7 @@ const WebSocketManager = {
},
cleanup() {
- console.log('π§Ή Cleaning up WebSocket resources');
+ console.log('Cleaning up WebSocket resources');
clearTimeout(this.debounceTimeout);
clearTimeout(this.reconnectTimeout);
@@ -723,7 +723,7 @@ function checkOfferAgainstFilters(offer, filters) {
function initializeFlowbiteTooltips() {
if (typeof Tooltip === 'undefined') {
- //console.warn('Tooltip is not defined. Make sure the required library is loaded.');
+ console.warn('Tooltip is not defined. Make sure the required library is loaded.');
return;
}
@@ -824,6 +824,17 @@ function filterAndSortData() {
let filteredData = [...originalJsonData];
+ const sentFromFilter = filters.sent_from || 'any';
+
+ filteredData = filteredData.filter(offer => {
+ if (sentFromFilter === 'public') {
+ return offer.is_public;
+ } else if (sentFromFilter === 'private') {
+ return !offer.is_public;
+ }
+ return true;
+ });
+
filteredData = filteredData.filter(offer => {
if (!isSentOffers && isOfferExpired(offer)) {
return false;
@@ -1032,7 +1043,7 @@ async function fetchLatestPrices() {
}
if (data && Object.keys(data).length > 0) {
- console.log('β
Fresh price data received');
+ console.log('Fresh price data received');
latestPrices = data;
@@ -1047,7 +1058,7 @@ async function fetchLatestPrices() {
//console.warn('Received empty price data');
}
} catch (error) {
- //console.error('β Error fetching prices:', error);
+ //console.error('Error fetching prices:', error);
throw error;
}
@@ -1055,36 +1066,33 @@ async function fetchLatestPrices() {
}
async function fetchOffers(manualRefresh = false) {
- const refreshButton = document.getElementById('refreshOffers');
- const refreshIcon = document.getElementById('refreshIcon');
- const refreshText = document.getElementById('refreshText');
+ const refreshButton = document.getElementById('refreshOffers');
+ const refreshIcon = document.getElementById('refreshIcon');
+ const refreshText = document.getElementById('refreshText');
- refreshButton.disabled = true;
- refreshIcon.classList.add('animate-spin');
- refreshText.textContent = 'Refreshing...';
- refreshButton.classList.add('opacity-75', 'cursor-wait');
-
- try {
- const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
- const response = await fetch(endpoint);
- const data = await response.json();
-
- jsonData = formatInitialData(data);
- originalJsonData = [...jsonData];
+ try {
+ refreshButton.disabled = true;
+ refreshIcon.classList.add('animate-spin');
+ refreshText.textContent = 'Refreshing...';
+ refreshButton.classList.add('opacity-75', 'cursor-wait');
+
+ const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
+ const response = await fetch(endpoint);
+ const data = await response.json();
+
+ jsonData = formatInitialData(data);
+ originalJsonData = [...jsonData];
- await updateOffersTable();
- updateJsonView();
- updatePaginationInfo();
-
- } catch (error) {
- //console.error('[Debug] Error fetching offers:', error);
- ui.displayErrorMessage('Failed to fetch offers. Please try again later.');
- } finally {
- refreshButton.disabled = false;
- refreshIcon.classList.remove('animate-spin');
- refreshText.textContent = 'Refresh';
- refreshButton.classList.remove('opacity-75', 'cursor-wait');
- }
+ await updateOffersTable();
+ updateJsonView();
+ updatePaginationInfo();
+
+ } catch (error) {
+ console.error('[Debug] Error fetching offers:', error);
+ ui.displayErrorMessage('Failed to fetch offers. Please try again later.');
+ } finally {
+ stopRefreshAnimation();
+ }
}
function formatInitialData(data) {
@@ -1092,6 +1100,7 @@ function formatInitialData(data) {
offer_id: String(offer.offer_id || ''),
swap_type: String(offer.swap_type || 'N/A'),
addr_from: String(offer.addr_from || ''),
+ addr_to: String(offer.addr_to || ''),
coin_from: String(offer.coin_from || ''),
coin_to: String(offer.coin_to || ''),
amount_from: String(offer.amount_from || '0'),
@@ -1102,6 +1111,7 @@ function formatInitialData(data) {
is_own_offer: Boolean(offer.is_own_offer),
amount_negotiable: Boolean(offer.amount_negotiable),
is_revoked: Boolean(offer.is_revoked),
+ is_public: offer.is_public !== undefined ? Boolean(offer.is_public) : false,
unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}`
}));
}
@@ -1173,6 +1183,23 @@ function updateLastRefreshTime() {
}
}
+function stopRefreshAnimation() {
+ const refreshButton = document.getElementById('refreshOffers');
+ const refreshIcon = document.getElementById('refreshIcon');
+ const refreshText = document.getElementById('refreshText');
+
+ if (refreshButton) {
+ refreshButton.disabled = false;
+ refreshButton.classList.remove('opacity-75', 'cursor-wait');
+ }
+ if (refreshIcon) {
+ refreshIcon.classList.remove('animate-spin');
+ }
+ if (refreshText) {
+ refreshText.textContent = 'Refresh';
+ }
+}
+
function updatePaginationInfo() {
const validOffers = getValidOffers();
const totalItems = validOffers.length;
@@ -1280,8 +1307,18 @@ function updateCoinFilterImages() {
function updateClearFiltersButton() {
const clearButton = document.getElementById('clearFilters');
if (clearButton) {
- clearButton.classList.toggle('opacity-50', !hasActiveFilters());
- clearButton.disabled = !hasActiveFilters();
+ const hasFilters = hasActiveFilters();
+ clearButton.classList.toggle('opacity-50', !hasFilters);
+ clearButton.disabled = !hasFilters;
+
+ // Update button styles based on state
+ if (hasFilters) {
+ clearButton.classList.add('hover:bg-green-600', 'hover:text-white');
+ clearButton.classList.remove('cursor-not-allowed');
+ } else {
+ clearButton.classList.remove('hover:bg-green-600', 'hover:text-white');
+ clearButton.classList.add('cursor-not-allowed');
+ }
}
}
@@ -1292,30 +1329,31 @@ function handleNoOffersScenario() {
filters.coin_from !== 'any' ||
(filters.status && filters.status !== 'any');
+ stopRefreshAnimation();
+
if (hasActiveFilters) {
offersBody.innerHTML = `
-
- No offers match the selected filters. Try different filter options or
-
+ |
+
+ No offers match the selected filters. Try different filter options or
+
+
|
`;
} else {
offersBody.innerHTML = `
-
- No active offers available. ${!isSentOffers ? 'Refreshing data...' : ''}
+ |
+ No active offers available.
|
`;
- if (!isSentOffers) {
- setTimeout(() => fetchOffers(true), 2000);
- }
}
}
async function updateOffersTable() {
- //console.log('[Debug] Starting updateOffersTable function');
-
try {
const PRICES_CACHE_KEY = 'prices_coingecko';
const cachedPrices = CacheManager.get(PRICES_CACHE_KEY);
@@ -1323,27 +1361,25 @@ async function updateOffersTable() {
if (!cachedPrices || !cachedPrices.remainingTime || cachedPrices.remainingTime < 60000) {
console.log('Fetching fresh price data...');
const priceData = await fetchLatestPrices();
- if (!priceData) {
- //console.error('Failed to fetch latest prices');
- } else {
- console.log('Latest prices fetched successfully');
+ if (priceData) {
latestPrices = priceData;
}
} else {
- console.log('Using cached price data (still valid)');
latestPrices = cachedPrices.value;
}
- const totalOffers = originalJsonData.filter(offer => !isOfferExpired(offer));
+ const validOffers = getValidOffers();
- const networkOffersCount = document.getElementById('network-offers-count');
- if (networkOffersCount && !isSentOffers) {
- networkOffersCount.textContent = totalOffers.length;
- }
-
- let validOffers = getValidOffers();
- console.log('[Debug] Valid offers:', validOffers.length);
+ const startIndex = (currentPage - 1) * itemsPerPage;
+ const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
+ const itemsToDisplay = validOffers.slice(startIndex, endIndex);
+ const identityPromises = itemsToDisplay.map(offer =>
+ offer.addr_from ? getIdentityData(offer.addr_from) : Promise.resolve(null)
+ );
+
+ const identities = await Promise.all(identityPromises);
+
if (validOffers.length === 0) {
handleNoOffersScenario();
return;
@@ -1351,15 +1387,12 @@ async function updateOffersTable() {
const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage));
currentPage = Math.min(currentPage, totalPages);
- const startIndex = (currentPage - 1) * itemsPerPage;
- const endIndex = Math.min(startIndex + itemsPerPage, validOffers.length);
- const itemsToDisplay = validOffers.slice(startIndex, endIndex);
const fragment = document.createDocumentFragment();
- const currentOffers = new Set();
- itemsToDisplay.forEach(offer => {
- const row = createTableRow(offer, isSentOffers);
+ itemsToDisplay.forEach((offer, index) => {
+ const identity = identities[index];
+ const row = createTableRow(offer, identity);
if (row) {
fragment.appendChild(row);
}
@@ -1380,22 +1413,14 @@ async function updateOffersTable() {
lastRefreshTime = Date.now();
if (newEntriesCountSpan) {
- const displayCount = isSentOffers ? jsonData.length : validOffers.length;
- newEntriesCountSpan.textContent = displayCount;
+ newEntriesCountSpan.textContent = validOffers.length;
}
if (lastRefreshTimeSpan) {
lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString();
}
- if (!isSentOffers) {
- const nextUpdateTime = getTimeUntilNextExpiration() * 1000;
- setTimeout(() => {
- updateRowTimes();
- }, nextUpdateTime);
- }
-
} catch (error) {
- //console.error('[Debug] Error in updateOffersTable:', error);
+ console.error('[Debug] Error in updateOffersTable:', error);
offersBody.innerHTML = `
@@ -1405,46 +1430,124 @@ async function updateOffersTable() {
}
}
-function createTableRow(offer, isSentOffers) {
+async function getIdentityData(address) {
+ try {
+ const response = await fetch(`/json/identities/${address}`);
+ if (!response.ok) {
+ return null;
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('Error fetching identity:', error);
+ return null;
+ }
+}
+
+function getIdentityInfo(address, identity) {
+ if (!identity) {
+ return {
+ displayAddr: address ? `${address.substring(0, 10)}...` : 'Unspecified',
+ fullAddress: address || '',
+ label: '',
+ note: '',
+ automationOverride: 0,
+ stats: {
+ sentBidsSuccessful: 0,
+ recvBidsSuccessful: 0,
+ sentBidsRejected: 0,
+ recvBidsRejected: 0,
+ sentBidsFailed: 0,
+ recvBidsFailed: 0
+ }
+ };
+ }
+
+ return {
+ displayAddr: address ? `${address.substring(0, 10)}...` : 'Unspecified',
+ fullAddress: address || '',
+ label: identity.label || '',
+ note: identity.note || '',
+ automationOverride: identity.automation_override || 0,
+ stats: {
+ sentBidsSuccessful: identity.num_sent_bids_successful || 0,
+ recvBidsSuccessful: identity.num_recv_bids_successful || 0,
+ sentBidsRejected: identity.num_sent_bids_rejected || 0,
+ recvBidsRejected: identity.num_recv_bids_rejected || 0,
+ sentBidsFailed: identity.num_sent_bids_failed || 0,
+ recvBidsFailed: identity.num_recv_bids_failed || 0
+ }
+ };
+}
+
+function createTableRow(offer, identity = null) {
const row = document.createElement('tr');
const uniqueId = `${offer.offer_id}_${offer.created_at}`;
- row.className = `opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600`;
+
+ row.className = 'relative opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600';
row.setAttribute('data-offer-id', uniqueId);
- const coinFrom = offer.coin_from;
- const coinTo = offer.coin_to;
+ const {
+ coin_from: coinFrom,
+ coin_to: coinTo,
+ created_at: createdAt,
+ expire_at: expireAt,
+ amount_from: amountFrom,
+ amount_to: amountTo,
+ is_own_offer: isOwnOffer,
+ is_revoked: isRevoked,
+ is_public: isPublic
+ } = offer;
+
const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase();
const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase();
const coinFromDisplay = getDisplayName(coinFrom);
const coinToDisplay = getDisplayName(coinTo);
-
- const postedTime = formatTime(offer.created_at, true);
- const expiresIn = formatTime(offer.expire_at);
+ const postedTime = formatTime(createdAt, true);
+ const expiresIn = formatTime(expireAt);
const currentTime = Math.floor(Date.now() / 1000);
- const isActuallyExpired = currentTime > offer.expire_at;
-
- const fromAmount = parseFloat(offer.amount_from) || 0;
- const toAmount = parseFloat(offer.amount_to) || 0;
+ const isActuallyExpired = currentTime > expireAt;
+ const fromAmount = parseFloat(amountFrom) || 0;
+ const toAmount = parseFloat(amountTo) || 0;
+ // Build row content
row.innerHTML = `
+ ${!isPublic ? createPrivateIndicator() : ' | | '}
${createTimeColumn(offer, postedTime, expiresIn)}
- ${createDetailsColumn(offer)}
+ ${createDetailsColumn(offer, identity)}
${createTakerAmountColumn(offer, coinTo, coinFrom)}
${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)}
${createOrderbookColumn(offer, coinFrom, coinTo)}
${createRateColumn(offer, coinFrom, coinTo)}
${createPercentageColumn(offer)}
${createActionColumn(offer, isActuallyExpired)}
- ${createTooltips(offer, offer.is_own_offer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, Boolean(offer.is_revoked))}
+ ${createTooltips(
+ offer,
+ isOwnOffer,
+ coinFrom,
+ coinTo,
+ fromAmount,
+ toAmount,
+ postedTime,
+ expiresIn,
+ isActuallyExpired,
+ Boolean(isRevoked),
+ identity
+ )}
`;
updateTooltipTargets(row, uniqueId);
- updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, offer.is_own_offer);
+ updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer);
return row;
}
+function createPrivateIndicator() {
+ return `
+
+ | `;
+}
+
function createTimeColumn(offer, postedTime, expiresIn) {
const now = Math.floor(Date.now() / 1000);
const timeLeft = offer.expire_at - now;
@@ -1457,10 +1560,10 @@ function createTimeColumn(offer, postedTime, expiresIn) {
}
return `
-
+ |
-
- Posted: ${escapeHtml(postedTime)}
- Expires in: ${escapeHtml(expiresIn)}
+ Posted: ${escapeHtml(postedTime)}
+ Expires in: ${escapeHtml(expiresIn)}
|
`;
}
-function createDetailsColumn(offer) {
+function shouldShowPublicTag(offers) {
+ return offers.some(offer => !offer.is_public);
+}
+
+function truncateText(text, maxLength = 15) {
+ if (typeof text !== 'string') return '';
+ return text.length > maxLength
+ ? text.slice(0, maxLength) + '...'
+ : text;
+}
+
+function createDetailsColumn(offer, identity = null) {
const addrFrom = offer.addr_from || '';
+ const identityInfo = getIdentityInfo(addrFrom, identity);
+
+ const showPublicPrivateTags = originalJsonData.some(o => o.is_public !== offer.is_public);
+
+ const tagClass = offer.is_public
+ ? 'bg-green-600 dark:bg-green-600'
+ : 'bg-red-500 dark:bg-red-500';
+ const tagText = offer.is_public ? 'Public' : 'Private';
+
+ const displayIdentifier = truncateText(
+ identityInfo.label || addrFrom || 'Unspecified'
+ );
+
+ const identifierTextClass = identityInfo.label
+ ? 'text-white dark:text-white'
+ : 'monospace';
+
return `
-
- Recipient: ${escapeHtml(addrFrom.substring(0, 10))}...
-
+
|
`;
}
@@ -1635,12 +1776,28 @@ function createActionColumn(offer, isActuallyExpired = false) {
}
// TOOLTIP FUNCTIONS
-function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked) {
+function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked, identity = null) {
const rate = parseFloat(offer.rate);
const fromSymbol = getCoinSymbolLowercase(coinFrom);
const toSymbol = getCoinSymbolLowercase(coinTo);
const uniqueId = `${offer.offer_id}_${offer.created_at}`;
+
+ const addrFrom = offer.addr_from || '';
+ const identityInfo = getIdentityInfo(addrFrom, identity);
+
+ const totalBids = identity ? (
+ identityInfo.stats.sentBidsSuccessful +
+ identityInfo.stats.recvBidsSuccessful +
+ identityInfo.stats.sentBidsFailed +
+ identityInfo.stats.recvBidsFailed +
+ identityInfo.stats.sentBidsRejected +
+ identityInfo.stats.recvBidsRejected
+ ) : 0;
+ const successRate = totalBids ? (
+ ((identityInfo.stats.sentBidsSuccessful + identityInfo.stats.recvBidsSuccessful) / totalBids) * 100
+ ).toFixed(1) : 0;
+
const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
const rateInUSD = rate * toPriceUSD;
@@ -1689,13 +1846,8 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t
-
-
-
+
-
+
+ ${createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids)}
`;
}
+function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, totalBids) {
+
+ const getSuccessRateColor = (rate) => {
+ if (rate >= 80) return 'text-green-600';
+ if (rate >= 60) return 'text-yellow-600';
+ return 'text-red-600';
+ };
+
+
+ const truncateText = (text, maxLength) => {
+ if (text.length <= maxLength) return text;
+ return text.substring(0, maxLength) + '...';
+ };
+
+ return `
+
`;
+}
+
function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) {
if (!coinFrom || !coinTo) {
//console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`);
@@ -1890,19 +2125,31 @@ function applyFilters() {
filterTimeout = null;
}, 250);
} catch (error) {
- //console.error('Error in filter timeout:', error);
+ console.error('Error in filter timeout:', error);
filterTimeout = null;
}
}
function clearFilters() {
+
filterForm.reset();
+
+ const selectElements = filterForm.querySelectorAll('select');
+ selectElements.forEach(select => {
+ select.value = 'any';
+ // Trigger change event
+ const event = new Event('change', { bubbles: true });
+ select.dispatchEvent(event);
+ });
+
const statusSelect = document.getElementById('status');
if (statusSelect) {
statusSelect.value = 'any';
}
+
jsonData = [...originalJsonData];
currentPage = 1;
+
updateOffersTable();
updateJsonView();
updateCoinFilterImages();
@@ -1916,26 +2163,18 @@ function hasActiveFilters() {
coin_from: formData.get('coin_from'),
status: formData.get('status')
};
-
- //console.log('Current filters:', filters);
- const hasFilters =
- filters.coin_to !== 'any' ||
- filters.coin_from !== 'any' ||
- (filters.status && filters.status !== 'any');
- //console.log('Has active filters:', hasFilters);
-
- return hasFilters;
-}
-function getActiveFilters() {
- const formData = new FormData(filterForm);
- return {
- coin_to: formData.get('coin_to'),
- coin_from: formData.get('coin_from'),
- status: formData.get('status')
- };
-}
+ const selectElements = filterForm.querySelectorAll('select');
+ let hasChangedFilters = false;
+
+ selectElements.forEach(select => {
+ if (select.value !== 'any') {
+ hasChangedFilters = true;
+ }
+ });
+ return hasChangedFilters;
+}
// UTILITY FUNCTIONS
function formatTimeLeft(timestamp) {
const now = Math.floor(Date.now() / 1000);
@@ -2075,7 +2314,6 @@ function getCoinSymbol(fullName) {
}
// EVENT LISTENERS
-
document.querySelectorAll('th[data-sortable="true"]').forEach(header => {
header.addEventListener('click', () => {
const columnIndex = parseInt(header.getAttribute('data-column-index'));
@@ -2192,12 +2430,24 @@ const timerManager = {
};
// INITIALIZATION AND EVENT BINDING
-
document.addEventListener('DOMContentLoaded', () => {
//console.log('DOM content loaded, initializing...');
console.log('View type:', isSentOffers ? 'sent offers' : 'received offers');
updateClearFiltersButton();
+
+ // Add event listeners for filter controls
+ const selectElements = filterForm.querySelectorAll('select');
+ selectElements.forEach(select => {
+ select.addEventListener('change', () => {
+ updateClearFiltersButton();
+ });
+ });
+
+ filterForm.addEventListener('change', () => {
+ applyFilters();
+ updateClearFiltersButton();
+ });
setTimeout(() => {
console.log('Starting WebSocket initialization...');
@@ -2215,7 +2465,7 @@ document.addEventListener('DOMContentLoaded', () => {
clearInterval(retryInterval);
continueInitialization();
} else if (retryCount >= maxRetries) {
- //console.error('β Failed to load tableRateModule after multiple attempts');
+ //console.error('Failed to load tableRateModule after multiple attempts');
clearInterval(retryInterval);
continueInitialization();
}
@@ -2255,47 +2505,47 @@ document.addEventListener('DOMContentLoaded', () => {
});
eventListeners.add(document.getElementById('refreshOffers'), 'click', async () => {
- console.log('Manual refresh initiated');
-
- const refreshButton = document.getElementById('refreshOffers');
- const refreshIcon = document.getElementById('refreshIcon');
- const refreshText = document.getElementById('refreshText');
+ console.log('Manual refresh initiated');
+
+ const refreshButton = document.getElementById('refreshOffers');
+ const refreshIcon = document.getElementById('refreshIcon');
+ const refreshText = document.getElementById('refreshText');
- refreshButton.disabled = true;
- refreshIcon.classList.add('animate-spin');
- refreshText.textContent = 'Refreshing...';
- refreshButton.classList.add('opacity-75', 'cursor-wait');
+ refreshButton.disabled = true;
+ refreshIcon.classList.add('animate-spin');
+ refreshText.textContent = 'Refreshing...';
+ refreshButton.classList.add('opacity-75', 'cursor-wait');
- try {
- const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
- const response = await fetch(endpoint);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
+ try {
+ const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
+ const response = await fetch(endpoint);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const newData = await response.json();
+
+ const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
+ console.log('Fetched offers:', processedNewData.length);
+
+ jsonData = formatInitialData(processedNewData);
+ originalJsonData = [...jsonData];
+
+ await updateOffersTable();
+ updateJsonView();
+ updatePaginationInfo();
+
+ console.log(' Manual refresh completed successfully');
+
+ } catch (error) {
+ console.error('Error during manual refresh:', error);
+ ui.displayErrorMessage('Failed to refresh offers. Please try again later.');
+ } finally {
+ refreshButton.disabled = false;
+ refreshIcon.classList.remove('animate-spin');
+ refreshText.textContent = 'Refresh';
+ refreshButton.classList.remove('opacity-75', 'cursor-wait');
}
- const newData = await response.json();
-
- const processedNewData = Array.isArray(newData) ? newData : Object.values(newData);
- console.log('Fetched offers:', processedNewData.length);
-
- jsonData = formatInitialData(processedNewData);
- originalJsonData = [...jsonData];
-
- await updateOffersTable();
- updateJsonView();
- updatePaginationInfo();
-
- console.log('β
Manual refresh completed successfully');
-
- } catch (error) {
- console.error('β Error during manual refresh:', error);
- ui.displayErrorMessage('Failed to refresh offers. Please try again later.');
- } finally {
- refreshButton.disabled = false;
- refreshIcon.classList.remove('animate-spin');
- refreshText.textContent = 'Refresh';
- refreshButton.classList.remove('opacity-75', 'cursor-wait');
- }
-});
+ });
eventListeners.add(prevPageButton, 'click', () => {
if (currentPage > 1) {
@@ -2332,7 +2582,7 @@ document.addEventListener('DOMContentLoaded', () => {
//console.log('Initial offers fetched');
applyFilters();
}).catch(error => {
- console.error('β Error fetching initial offers:', error);
+ console.error('Error fetching initial offers:', error);
});
const listingLabel = document.querySelector('span[data-listing-label]');
@@ -2351,7 +2601,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
- console.log('β
Initialization completed');
+ console.log('Initialization completed');
});
console.log('Offers Table Module fully initialized');
diff --git a/basicswap/templates/offer_new_1.html b/basicswap/templates/offer_new_1.html
index 00a5464..e85d32c 100644
--- a/basicswap/templates/offer_new_1.html
+++ b/basicswap/templates/offer_new_1.html
@@ -86,7 +86,7 @@
@@ -471,7 +471,7 @@ if (document.readyState === 'loading') {
}
function getRateInferred(event) {
- event.preventDefault(); // Prevent default form submission behavior
+ event.preventDefault();
const coin_from = document.getElementById('coin_from').value;
const coin_to = document.getElementById('coin_to').value;
@@ -558,7 +558,7 @@ if (document.readyState === 'loading') {
return;
} else
if (amt_from == '' && amt_to != '') {
- if (value_changed == 'amt_from') { // Don't try and set a value just cleared
+ if (value_changed == 'amt_from') {
return;
}
params += '&rate=' + rate + '&amt_to=' + amt_to;
diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html
index 9153180..48bbbb7 100644
--- a/basicswap/templates/offers.html
+++ b/basicswap/templates/offers.html
@@ -290,6 +290,18 @@ function getWebSocketConfig() {
{% endif %}
+
+
+
+ {{ input_arrow_down_svg | safe }}
+
+
+
+