Merge pull request #193 from gerlofvanek/private-2
Some checks failed
ci / ci (3.12) (push) Has been cancelled

Private orderbook display + Identity stats + Various fixes.
This commit is contained in:
tecnovert 2024-12-26 08:14:13 +00:00 committed by GitHub
commit 7ad92b1bbd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 538 additions and 236 deletions

View file

@ -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

View file

@ -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)

View file

@ -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;
}

View file

@ -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');

View file

@ -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;

View file

@ -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 }}

View file

@ -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()