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 ` - +
- + @@ -1468,21 +1571,59 @@ function createTimeColumn(offer, postedTime, 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))}... - +
+ ${showPublicPrivateTags ? `${tagText} + ` : ''} + + + + + + + ${escapeHtml(displayIdentifier)} + + +
`; } @@ -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
- - - @@ -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 }} + +
+
+