mirror of
https://github.com/basicswap/basicswap.git
synced 2025-01-07 03:09:34 +00:00
Merge pull request #193 from gerlofvanek/private-2
Some checks failed
ci / ci (3.12) (push) Has been cancelled
Some checks failed
ci / ci (3.12) (push) Has been cancelled
Private orderbook display + Identity stats + Various fixes.
This commit is contained in:
commit
7ad92b1bbd
7 changed files with 538 additions and 236 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue