diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 2367f04..149c942 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1329,39 +1329,57 @@ 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 + # self.log.debug(f"Starting updateIdentityBidState for address {address}, bid {bid.bid_id.hex()}") + offer = self.getOffer(bid.offer_id, cursor) + # self.log.debug(f"Offer from: {offer.addr_from}, Bid from: {bid.bid_addr}, Reverse bid: {reverse_bid}") + addresses_to_update = [offer.addr_from, bid.bid_addr] + for addr in addresses_to_update: + # self.log.debug(f"Processing address: {addr}") + identity_stats = self.queryOne(KnownIdentity, cursor, {"address": addr}) + if not identity_stats: + # self.log.debug(f"Creating new identity record for {addr}") + 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 + # self.log.debug(f"Is offer creator: {is_offer_creator}, Current state: {bid.state}") + if bid.state == BidStates.SWAP_COMPLETED: + # self.log.debug("Processing successful swap") + if is_offer_creator: + old_value = zeroIfNone(identity_stats.num_recv_bids_successful) + identity_stats.num_recv_bids_successful = old_value + 1 + # self.log.debug(f"Updated received successful: {old_value} -> {identity_stats.num_recv_bids_successful}") + else: + old_value = zeroIfNone(identity_stats.num_sent_bids_successful) + identity_stats.num_sent_bids_successful = old_value + 1 + # self.log.debug(f"Updated sent successful: {old_value} -> {identity_stats.num_sent_bids_successful}") + elif bid.state in (BidStates.BID_ERROR, + BidStates.XMR_SWAP_FAILED_REFUNDED, + BidStates.XMR_SWAP_FAILED_SWIPED, + BidStates.XMR_SWAP_FAILED, + BidStates.SWAP_TIMEDOUT): + # self.log.debug(f"Processing failed swap: {bid.state}") + if is_offer_creator: + old_value = zeroIfNone(identity_stats.num_recv_bids_failed) + identity_stats.num_recv_bids_failed = old_value + 1 + # self.log.debug(f"Updated received failed: {old_value} -> {identity_stats.num_recv_bids_failed}") + else: + old_value = zeroIfNone(identity_stats.num_sent_bids_failed) + identity_stats.num_sent_bids_failed = old_value + 1 + # self.log.debug(f"Updated sent failed: {old_value} -> {identity_stats.num_sent_bids_failed}") + elif bid.state == BidStates.BID_REJECTED: + # self.log.debug("Processing rejected bid") + if is_offer_creator: + old_value = zeroIfNone(identity_stats.num_recv_bids_rejected) + identity_stats.num_recv_bids_rejected = old_value + 1 + # self.log.debug(f"Updated received rejected: {old_value} -> {identity_stats.num_recv_bids_rejected}") + else: + old_value = zeroIfNone(identity_stats.num_sent_bids_rejected) + identity_stats.num_sent_bids_rejected = old_value + 1 + # self.log.debug(f"Updated sent rejected: {old_value} -> {identity_stats.num_sent_bids_rejected}") + 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 = ` <tr> - <td colspan="8" class="text-center py-4 text-gray-500 dark:text-white"> - No offers match the selected filters. Try different filter options or - <button onclick="clearFilters()" class="text-blue-500 hover:text-blue-700 bold">clear filters</button> + <td colspan="9" class="text-center py-8"> + <div class="flex items-center justify-center text-gray-500 dark:text-white"> + No offers match the selected filters. Try different filter options or + <button onclick="clearFilters()" class="ml-1 text-blue-500 hover:text-blue-700 font-semibold"> + clear filters + </button> + </div> </td> </tr>`; } else { offersBody.innerHTML = ` <tr> - <td colspan="8" class="text-center py-4 text-gray-500 dark:text-white"> - No active offers available. ${!isSentOffers ? 'Refreshing data...' : ''} + <td colspan="9" class="text-center py-8 text-gray-500 dark:text-white"> + No active offers available. </td> </tr>`; - 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 = ` <tr> <td colspan="8" class="text-center py-4 text-red-500"> @@ -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() : '<td class="w-0 p-0 m-0"></td>'} ${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 `<td class="relative w-0 p-0 m-0"> + <div class="absolute top-0 bottom-0 left-0 w-1 bg-red-700" style="min-height: 100%;"></div> + </td>`; +} + 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 ` - <td class="py-3 pl-6 text-xs"> + <td class="py-3 pl-1 pr-2 text-xs whitespace-nowrap"> <div class="flex items-center"> <div class="relative" data-tooltip-target="tooltip-active${escapeHtml(offer.offer_id)}"> - <svg alt="" class="w-5 h-5 rounded-full mr-3 cursor-pointer" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"> + <svg alt="" class="w-5 h-5 rounded-full mr-4 cursor-pointer" xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 24 24"> <g stroke-linecap="round" stroke-width="2" fill="none" stroke="${strokeColor}" stroke-linejoin="round"> <circle cx="12" cy="12" r="11"></circle> <polyline points="12,6 12,12 18,12" stroke="${strokeColor}"></polyline> @@ -1468,21 +1571,59 @@ function createTimeColumn(offer, postedTime, expiresIn) { </svg> </div> <div class="flex flex-col hidden xl:block"> - <div class="text-xs"><span class="bold">Posted:</span> ${escapeHtml(postedTime)}</div> - <div class="text-xs"><span class="bold">Expires in:</span> ${escapeHtml(expiresIn)}</div> + <div class="text-xs whitespace-nowrap"><span class="bold">Posted:</span> ${escapeHtml(postedTime)}</div> + <div class="text-xs whitespace-nowrap"><span class="bold">Expires in:</span> ${escapeHtml(expiresIn)}</div> </div> </div> </td> `; } -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 ` <td class="py-8 px-4 text-xs text-left hidden xl:block"> - <a data-tooltip-target="tooltip-recipient${escapeHtml(offer.offer_id)}" href="/identity/${escapeHtml(addrFrom)}"> - <span class="bold">Recipient:</span> ${escapeHtml(addrFrom.substring(0, 10))}... - </a> + <div class="flex flex-col gap-2 relative"> + ${showPublicPrivateTags ? `<span class="inline-flex pl-6 pr-6 py-1 justify-center text-[10px] w-1/4 font-medium text-gray-100 rounded-md ${tagClass}">${tagText}</span> + ` : ''} + + <a data-tooltip-target="tooltip-recipient-${escapeHtml(offer.offer_id)}" href="/identity/${escapeHtml(addrFrom)}" class="flex items-center"> + <svg class="w-4 h-4 mr-2 text-gray-400 dark:text-white" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> + <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path> + </svg> + <span class="${identifierTextClass} font-semibold"> + ${escapeHtml(displayIdentifier)} + </span> + </a> + </div> </td> `; } @@ -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 </div> <div class="tooltip-arrow" data-popper-arrow></div> </div> - - <div id="tooltip-recipient-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"> - <div class="active-revoked-expired"><span class="bold monospace">${offer.addr_from}</span></div> - <div class="tooltip-arrow" data-popper-arrow></div> - </div> - <div id="tooltip-wallet-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"> + <div id="tooltip-wallet-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"> <div class="active-revoked-expired"> <span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinTo} Wallet</span> </div> @@ -1711,7 +1863,7 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t <div class="tooltip-arrow pr-6" data-popper-arrow></div> </div> - <div id="tooltip-wallet-maker-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-blue-500 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"> + <div id="tooltip-wallet-maker-${uniqueId}" role="tooltip" class="inline-block absolute invisible z-50 py-2 px-3 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"> <div class="active-revoked-expired"> <span class="bold">${treatAsSentOffer ? 'My' : ''} ${coinFrom} Wallet</span> </div> @@ -1731,9 +1883,92 @@ function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, t </div> <div class="tooltip-arrow" data-popper-arrow></div> </div> + + ${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 ` + <div id="tooltip-recipient-${uniqueId}" role="tooltip" + class="fixed z-50 py-3 px-4 text-sm font-medium text-white bg-gray-400 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip max-w-sm pointer-events-none"> + <div class="identity-info space-y-2"> + ${identityInfo.label ? ` + <div class="border-b border-gray-400 pb-2"> + <div class="text-white text-xs tracking-wide font-semibold">Label:</div> + <div class="text-white">${escapeHtml(identityInfo.label)}</div> + </div> + ` : ''} + + <div class="space-y-1"> + <div class="text-white text-xs tracking-wide font-semibold">Recipient Address:</div> + <div class="monospace text-xs break-all bg-gray-500 p-2 rounded-md text-white"> + ${escapeHtml(identityInfo.fullAddress)} + </div> + </div> + + ${identityInfo.note ? ` + <div class="space-y-1 hidden"> + <div class="text-white text-xs tracking-wide font-semibold">Note:</div> + <div class="text-white text-sm italic" title="${escapeHtml(identityInfo.note)}"> + ${escapeHtml(truncateText(identityInfo.note, 150))} + </div> + </div> + ` : ''} + + ${identity ? ` + <div class="border-t border-gray-400 pt-2 mt-2"> + <div class="text-white text-xs tracking-wide font-semibold mb-2">Swap History:</div> + <div class="grid grid-cols-2 gap-2"> + <div class="text-center p-2 bg-gray-500 rounded-md"> + <div class="text-lg font-bold ${getSuccessRateColor(successRate)}">${successRate}%</div> + <div class="text-xs text-white">Success Rate</div> + </div> + <div class="text-center p-2 bg-gray-500 rounded-md"> + <div class="text-lg font-bold text-blue-500">${totalBids}</div> + <div class="text-xs text-white">Total Trades</div> + </div> + </div> + <div class="grid grid-cols-3 gap-2 mt-2 text-center text-xs"> + <div> + <div class="text-green-600 font-semibold"> + ${identityInfo.stats.sentBidsSuccessful + identityInfo.stats.recvBidsSuccessful} + </div> + <div class="text-white">Successful</div> + </div> + <div> + <div class="text-yellow-600 font-semibold"> + ${identityInfo.stats.sentBidsRejected + identityInfo.stats.recvBidsRejected} + </div> + <div class="text-white">Rejected</div> + </div> + <div> + <div class="text-red-600 font-semibold"> + ${identityInfo.stats.sentBidsFailed + identityInfo.stats.recvBidsFailed} + </div> + <div class="text-white">Failed</div> + </div> + </div> + </div> + ` : ''} + </div> + <div class="tooltip-arrow" data-popper-arrow></div> + </div>`; +} + 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 @@ <select class="pl-10 hover:border-blue-500 pl-10 bg-gray-50 text-gray-900 appearance-none pr-10 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-white text-sm rounded-lg outline-none focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 focus:ring-0" name="addr_to"> <option{% if data.addr_to=="-1" %} selected{% endif %} value="-1">Public Network</option> {% for a in addrs_to %} - <option{% if data.addr_to==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} {{ a[1] }}</option> + <option{% if data.addr_to==a[0] %} selected{% endif %} value="{{ a[0] }}">{{ a[0] }} ({{ a[1] }})</option> {% endfor %} </select> </div> @@ -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 %} </div> </div> + <div class="pt-3 px-3 md:w-auto hover-container"> + <div class="flex"> + <div class="relative"> + {{ input_arrow_down_svg | safe }} + <select name="sent_from" id="sent_from" class="bg-gray-50 text-gray-900 appearance-none pr-10 pl-5 dark:bg-gray-500 dark:text-white border border-gray-300 dark:border-gray-400 dark:text-gray-50 dark:placeholder-gray-50 text-sm rounded-lg outline-none block w-full p-2.5 focus:ring-0"> + <option value="any" {% if not filters.sent_from %} selected{% endif %}>All Offers</option> + <option value="public" {% if filters.sent_from == 'public' %} selected{% endif %}>Public</option> + <option value="private" {% if filters.sent_from == 'private' %} selected{% endif %}>Private</option> + </select> + </div> + </div> + </div> <div class="w-full md:w-auto pt-3 px-3"> <div class="relative"> <button type="button" id="clearFilters" class="transition-opacity duration-200 flex flex-wrap justify-center w-full px-4 py-2.5 font-medium text-sm hover:text-white dark:text-white dark:bg-gray-500 bg-coolGray-200 hover:bg-green-600 hover:border-green-600 rounded-lg transition duration-200 border border-coolGray-200 dark:border-gray-400 rounded-md shadow-button focus:ring-0 focus:outline-none" disabled> @@ -322,15 +334,20 @@ function getWebSocketConfig() { <div id="jsonView" class="hidden mb-4"> <pre id="jsonContent" class="bg-gray-100 p-4 rounded overflow-auto" style="max-height: 300px;"></pre> </div> - <div class="container mt-5 mx-auto"> + <div class="container mt-5 mx-auto px-4"> <div class="pt-0 pb-6 bg-coolGray-100 dark:bg-gray-500 rounded-xl"> <div class="px-0"> - <div class="w-auto mt-6 pb-6 overflow-x-auto"> + <div class="w-auto mt-6 overflow-x-auto"> <table class="w-full min-w-max"> <thead class="uppercase"> <tr> <th class="p-0" data-sortable="true" data-column-index="0"> <div class="py-3 pl-4 justify-center rounded-tl-xl bg-coolGray-200 dark:bg-gray-600"> + <span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold"></span> + </div> + </th> + <th class="p-0" data-sortable="true" data-column-index="0"> + <div class="py-3 pl-4 justify-center bg-coolGray-200 dark:bg-gray-600"> <span class="text-sm mr-1 text-gray-600 dark:text-gray-300 font-semibold">Time</span> <span class="sort-icon ml-1 text-gray-600 dark:text-gray-400" id="sort-icon-0">β</span> </div> @@ -399,19 +416,19 @@ function getWebSocketConfig() { <div class="rounded-b-md"> <div class="w-full"> <div class="flex flex-wrap justify-between items-center pl-6 pt-6 pr-6 border-t border-gray-100 dark:border-gray-400"> -<div class="flex items-center"> - <div class="flex items-center mr-4"> - <span id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span> - <span id="status-text" class="text-sm text-gray-500">Connecting...</span> - </div> - <p class="text-sm font-heading dark:text-gray-400 mr-4"> - Last refreshed: <span id="lastRefreshTime">Never</span> - </p> - <p class="text-sm font-heading dark:text-gray-400 mr-4"> - <span data-listing-label>Network Listings: </span> - <span id="newEntriesCount"></span> - </p> -</div> + <div class="flex items-center"> + <div class="flex items-center mr-4"> + <span id="status-dot" class="w-2.5 h-2.5 rounded-full bg-gray-500 mr-2"></span> + <span id="status-text" class="text-sm text-gray-500">Connecting...</span> + </div> + <p class="text-sm font-heading dark:text-gray-400 mr-4"> + Last refreshed: <span id="lastRefreshTime">Never</span> + </p> + <p class="text-sm font-heading dark:text-gray-400 mr-4"> + <span data-listing-label>Network Listings: </span> + <span id="newEntriesCount"></span> + </p> + </div> <div class="flex items-center space-x-2"> <button type="button" id="prevPage" class="inline-flex items-center h-9 py-1 px-4 text-xs text-blue-50 font-semibold bg-blue-500 hover:bg-green-600 rounded-lg transition duration-200 focus:ring-0 focus:outline-none"> {{ page_back_svg | safe }} diff --git a/basicswap/ui/page_offers.py b/basicswap/ui/page_offers.py index f89f4d6..9f76e41 100644 --- a/basicswap/ui/page_offers.py +++ b/basicswap/ui/page_offers.py @@ -528,6 +528,15 @@ def page_newoffer(self, url_split, post_string): coins_from, coins_to = listAvailableCoins(swap_client, split_from=True) + addrs_from_raw = swap_client.listSMSGAddresses("offer_send_from") + addrs_to_raw = swap_client.listSMSGAddresses("offer_send_to") + + all_addresses = swap_client.listAllSMSGAddresses({}) + addr_notes = {addr["addr"]: addr["note"] for addr in all_addresses} + + addrs_from = [(addr[0], addr_notes.get(addr[0], "")) for addr in addrs_from_raw] + addrs_to = [(addr[0], addr_notes.get(addr[0], "")) for addr in addrs_to_raw] + automation_filters = {"type_ind": Concepts.OFFER, "sort_by": "label"} automation_strategies = swap_client.listAutomationStrategies(automation_filters) @@ -556,8 +565,8 @@ def page_newoffer(self, url_split, post_string): "err_messages": err_messages, "coins_from": coins_from, "coins": coins_to, - "addrs": swap_client.listSMSGAddresses("offer_send_from"), - "addrs_to": swap_client.listSMSGAddresses("offer_send_to"), + "addrs": addrs_from, + "addrs_to": addrs_to, "data": page_data, "automation_strategies": automation_strategies, "summary": summary, @@ -581,7 +590,7 @@ def page_offer(self, url_split, post_string): offer, xmr_offer = swap_client.getXmrOffer(offer_id) ensure(offer, "Unknown offer ID") - extend_data = { # Defaults + extend_data = { "nb_validmins": 10, } messages = [] @@ -598,7 +607,6 @@ def page_offer(self, url_split, post_string): reverse_bid: bool = True if offer.bid_reversed else False - # Set defaults debugind = -1 bid_amount = ci_from.format_amount(offer.amount_from) bid_rate = ci_to.format_amount(offer.rate) @@ -617,7 +625,6 @@ def page_offer(self, url_split, post_string): except Exception as ex: err_messages.append("Revoke offer failed: " + str(ex)) elif b"repeat_offer" in form_data: - # Can't set the post data here as browsers will always resend the original post data when responding to redirects self.send_response(302) self.send_header("Location", "/newoffer?offer_from=" + offer_id.hex()) self.end_headers()