diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js
index 530a837..c570147 100644
--- a/basicswap/static/js/offerstable.js
+++ b/basicswap/static/js/offerstable.js
@@ -1,176 +1,69 @@
-// Config
-
-const coinNameToSymbol = {
- 'Bitcoin': 'bitcoin',
- 'Particl': 'particl',
- 'Particl Blind': 'particl',
- 'Particl Anon': 'particl',
- 'Monero': 'monero',
- 'Wownero': 'wownero',
- 'Litecoin': 'litecoin',
- 'Firo': 'firo',
- 'Zcoin': 'firo',
- 'Dash': 'dash',
- 'PIVX': 'pivx',
- 'Decred': 'decred',
- 'Zano': 'zano',
- 'Dogecoin': 'dogecoin',
- 'Bitcoin Cash': 'bitcoin-cash'
-};
-
-function makePostRequest(url, headers = {}) {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.open('POST', '/json/readurl');
- xhr.setRequestHeader('Content-Type', 'application/json');
- xhr.timeout = 30000;
- xhr.ontimeout = () => reject(new Error('Request timed out'));
- xhr.onload = () => {
- console.log(`Response for ${url}:`, xhr.responseText);
- if (xhr.status === 200) {
- try {
- const response = JSON.parse(xhr.responseText);
- if (response.Error) {
- console.error(`API Error for ${url}:`, response.Error);
- reject(new Error(response.Error));
- } else {
- resolve(response);
- }
- } catch (error) {
- console.error(`Invalid JSON response for ${url}:`, xhr.responseText);
- reject(new Error(`Invalid JSON response: ${error.message}`));
- }
- } else {
- console.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`);
- reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
- }
- };
- xhr.onerror = () => reject(new Error('Network error occurred'));
- xhr.send(JSON.stringify({
- url: url,
- headers: headers
- }));
- });
-}
-
-const symbolToCoinName = {
- ...Object.fromEntries(Object.entries(coinNameToSymbol).map(([key, value]) => [value, key])),
- 'zcoin': 'Firo',
- 'firo': 'Firo'
-};
-
-function getDisplayName(coinName) {
- return coinNameToDisplayName[coinName] || coinName;
-}
-
-const coinNameToDisplayName = {
- 'Bitcoin': 'Bitcoin',
- 'Litecoin': 'Litecoin',
- 'Monero': 'Monero',
- 'Particl': 'Particl',
- 'Particl Blind': 'Particl Blind',
- 'Particl Anon': 'Particl Anon',
- 'PIVX': 'PIVX',
- 'Firo': 'Firo',
- 'Zcoin': 'Firo',
- 'Dash': 'Dash',
- 'Decred': 'Decred',
- 'Wownero': 'Wownero',
- 'Bitcoin Cash': 'Bitcoin Cash',
- 'Dogecoin': 'Dogecoin',
- 'Zano': 'Zano'
-};
-
let latestPrices = null;
+let lastRefreshTime = null;
+let newEntriesCount = 0;
+let nextRefreshCountdown = 60;
+let currentPage = 1;
+const itemsPerPage = 100;
+let lastAppliedFilters = {};
const CACHE_KEY = 'latestPricesCache';
-const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes in milliseconds
+const CACHE_DURATION = 10 * 60 * 1000; // 10 min
+const MIN_REFRESH_INTERVAL = 30; // 30 sec
-async function fetchLatestPrices() {
- console.log('Checking for cached prices...');
- const cachedData = getCachedPrices();
-
- if (cachedData) {
- console.log('Using cached price data');
- latestPrices = cachedData;
- return cachedData;
- }
-
- console.log('Fetching latest prices...');
- const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC';
-
- try {
- const data = await makePostRequest(url);
- console.log('Fetched price data:', data);
-
- latestPrices = data;
- setCachedPrices(data);
- return data;
- } catch (error) {
- console.error('Error fetching price data:', error.message);
- return null;
- }
-}
-
-function getCachedPrices() {
- const cachedItem = localStorage.getItem(CACHE_KEY);
- if (cachedItem) {
- const { data, timestamp } = JSON.parse(cachedItem);
- if (Date.now() - timestamp < CACHE_DURATION) {
- return data;
- }
- }
- return null;
-}
-
-function setCachedPrices(data) {
- const cacheItem = {
- data: data,
- timestamp: Date.now()
- };
- localStorage.setItem(CACHE_KEY, JSON.stringify(cacheItem));
-}
-
-const getUsdValue = async (cryptoValue, coinSymbol) => {
- try {
- const prices = await fetchLatestPrices();
- const apiSymbol = coinNameToSymbol[coinSymbol] || coinSymbol.toLowerCase();
- const coinData = prices[apiSymbol];
- if (coinData && coinData.usd) {
- return cryptoValue * coinData.usd;
- } else {
- throw new Error(`Price data not available for ${coinSymbol}`);
- }
- } catch (error) {
- console.error(`Error getting USD value for ${coinSymbol}:`, error);
- throw error;
- }
-};
-
-// Global
let jsonData = [];
let originalJsonData = [];
let isInitialLoad = true;
let tableRateModule;
-
-let lastRefreshTime = null;
-let newEntriesCount = 0;
-
-let nextRefreshCountdown = 60; // Default to 60 seconds
-const MIN_REFRESH_INTERVAL = 30; // Minimum refresh interval in seconds
-
const isSentOffers = window.offersTableConfig.isSentOffers;
-let currentPage = 1;
-const itemsPerPage = 100;
-
-const coinIdToName = {
- 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
- 6: 'monero', 7: 'particl blind', 8: 'particl anon',
- 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash'
+const coinNameToSymbol = {
+ 'Bitcoin': 'bitcoin',
+ 'Particl': 'particl',
+ 'Particl Blind': 'particl',
+ 'Particl Anon': 'particl',
+ 'Monero': 'monero',
+ 'Wownero': 'wownero',
+ 'Litecoin': 'litecoin',
+ 'Firo': 'firo',
+ 'Zcoin': 'firo',
+ 'Dash': 'dash',
+ 'PIVX': 'pivx',
+ 'Decred': 'decred',
+ 'Zano': 'zano',
+ 'Dogecoin': 'dogecoin',
+ 'Bitcoin Cash': 'bitcoin-cash'
+};
+
+const symbolToCoinName = {
+ ...Object.fromEntries(Object.entries(coinNameToSymbol).map(([key, value]) => [value, key])),
+ 'zcoin': 'Firo',
+ 'firo': 'Firo'
+};
+
+const coinNameToDisplayName = {
+ 'Bitcoin': 'Bitcoin',
+ 'Litecoin': 'Litecoin',
+ 'Monero': 'Monero',
+ 'Particl': 'Particl',
+ 'Particl Blind': 'Particl Blind',
+ 'Particl Anon': 'Particl Anon',
+ 'PIVX': 'PIVX',
+ 'Firo': 'Firo',
+ 'Zcoin': 'Firo',
+ 'Dash': 'Dash',
+ 'Decred': 'Decred',
+ 'Wownero': 'Wownero',
+ 'Bitcoin Cash': 'Bitcoin Cash',
+ 'Dogecoin': 'Dogecoin',
+ 'Zano': 'Zano'
+};
+
+const coinIdToName = {
+ 1: 'particl', 2: 'bitcoin', 3: 'litecoin', 4: 'decred',
+ 6: 'monero', 7: 'particl blind', 8: 'particl anon',
+ 9: 'wownero', 11: 'pivx', 13: 'firo', 17: 'bitcoincash'
};
-// DOM
const toggleButton = document.getElementById('toggleView');
const tableView = document.getElementById('tableView');
const jsonView = document.getElementById('jsonView');
@@ -185,57 +78,311 @@ const lastRefreshTimeSpan = document.getElementById('lastRefreshTime');
const newEntriesCountSpan = document.getElementById('newEntriesCount');
const nextRefreshTimeSpan = document.getElementById('nextRefreshTime');
-// Utility
+window.tableRateModule = {
+ coinNameToSymbol: {
+ 'Bitcoin': 'BTC',
+ 'Particl': 'PART',
+ 'Particl Blind': 'PART',
+ 'Particl Anon': 'PART',
+ 'Monero': 'XMR',
+ 'Wownero': 'WOW',
+ 'Litecoin': 'LTC',
+ 'Firo': 'FIRO',
+ 'Dash': 'DASH',
+ 'PIVX': 'PIVX',
+ 'Decred': 'DCR',
+ 'Zano': 'ZANO',
+ 'Bitcoin Cash': 'BCH',
+ 'Dogecoin': 'DOGE'
+ },
+
+ cache: {},
+ processedOffers: new Set(),
+
+ getCachedValue(key) {
+ const cachedItem = localStorage.getItem(key);
+ if (cachedItem) {
+ const parsedItem = JSON.parse(cachedItem);
+ if (Date.now() < parsedItem.expiry) {
+ return parsedItem.value;
+ } else {
+ localStorage.removeItem(key);
+ }
+ }
+ return null;
+ },
+
+ setCachedValue(key, value, ttl = 900000) {
+ const item = {
+ value: value,
+ expiry: Date.now() + ttl,
+ };
+ localStorage.setItem(key, JSON.stringify(item));
+ },
+
+ setFallbackValue(coinSymbol, value) {
+ this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 24 * 60 * 60 * 1000);
+ },
+
+ isNewOffer(offerId) {
+ if (this.processedOffers.has(offerId)) {
+ return false;
+ }
+ this.processedOffers.add(offerId);
+ return true;
+ },
+
+ formatUSD(value) {
+ if (Math.abs(value) < 0.000001) {
+ return value.toExponential(8) + ' USD';
+ } else if (Math.abs(value) < 0.01) {
+ return value.toFixed(8) + ' USD';
+ } else {
+ return value.toFixed(2) + ' USD';
+ }
+ },
+
+ formatNumber(value, decimals) {
+ if (Math.abs(value) < 0.000001) {
+ return value.toExponential(decimals);
+ } else if (Math.abs(value) < 0.01) {
+ return value.toFixed(decimals);
+ } else {
+ return value.toFixed(Math.min(2, decimals));
+ }
+ },
+
+ getFallbackValue(coinSymbol) {
+ const value = localStorage.getItem(`fallback_${coinSymbol}_usd`);
+ return value ? parseFloat(value) : null;
+ },
+
+ initializeTable() {
+ document.querySelectorAll('.coinname-value').forEach(coinNameValue => {
+ const coinFullNameOrSymbol = coinNameValue.getAttribute('data-coinname');
+ if (!coinFullNameOrSymbol || coinFullNameOrSymbol === 'Unknown') {
+ console.warn('Missing or unknown coin name/symbol in data-coinname attribute');
+ return;
+ }
+ coinNameValue.classList.remove('hidden');
+ if (!coinNameValue.textContent.trim()) {
+ coinNameValue.textContent = 'N/A';
+ }
+ });
+
+ document.querySelectorAll('.usd-value').forEach(usdValue => {
+ if (!usdValue.textContent.trim()) {
+ usdValue.textContent = 'N/A';
+ }
+ });
+
+ document.querySelectorAll('.profit-loss').forEach(profitLoss => {
+ if (!profitLoss.textContent.trim() || profitLoss.textContent === 'Calculating...') {
+ profitLoss.textContent = 'N/A';
+ }
+ });
+ },
+
+ init() {
+ console.log('Initializing TableRateModule');
+ this.initializeTable();
+ }
+};
+
+function makePostRequest(url, headers = {}) {
+ return new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', '/json/readurl');
+ xhr.setRequestHeader('Content-Type', 'application/json');
+ xhr.timeout = 30000;
+ xhr.ontimeout = () => reject(new Error('Request timed out'));
+ xhr.onload = () => {
+ console.log(`Response for ${url}:`, xhr.responseText);
+ if (xhr.status === 200) {
+ try {
+ const response = JSON.parse(xhr.responseText);
+ if (response.Error) {
+ console.error(`API Error for ${url}:`, response.Error);
+ reject(new Error(response.Error));
+ } else {
+ resolve(response);
+ }
+ } catch (error) {
+ console.error(`Invalid JSON response for ${url}:`, xhr.responseText);
+ reject(new Error(`Invalid JSON response: ${error.message}`));
+ }
+ } else {
+ console.error(`HTTP Error for ${url}: ${xhr.status} ${xhr.statusText}`);
+ reject(new Error(`HTTP Error: ${xhr.status} ${xhr.statusText}`));
+ }
+ };
+ xhr.onerror = () => reject(new Error('Network error occurred'));
+ xhr.send(JSON.stringify({
+ url: url,
+ headers: headers
+ }));
+ });
+}
+
+function escapeHtml(unsafe) {
+ if (typeof unsafe !== 'string') {
+ console.warn('escapeHtml received a non-string value:', unsafe);
+ return '';
+ }
+ return unsafe
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+}
+
+function formatTimeDifference(timestamp) {
+ const now = Math.floor(Date.now() / 1000);
+ const diff = Math.abs(now - timestamp);
+
+ if (diff < 60) return `${diff} seconds`;
+ if (diff < 3600) return `${Math.floor(diff / 60)} minutes`;
+ if (diff < 86400) return `${Math.floor(diff / 3600)} hours`;
+ if (diff < 2592000) return `${Math.floor(diff / 86400)} days`;
+ if (diff < 31536000) return `${Math.floor(diff / 2592000)} months`;
+ return `${Math.floor(diff / 31536000)} years`;
+}
+
+function formatTimeAgo(timestamp) {
+ return `${formatTimeDifference(timestamp)} ago`;
+}
+
+function formatTimeLeft(timestamp) {
+ const now = Math.floor(Date.now() / 1000);
+ if (timestamp <= now) return "Expired";
+ return formatTimeDifference(timestamp);
+}
+
function getCoinSymbol(fullName) {
- const symbolMap = {
- 'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR',
- 'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART',
- 'PIVX': 'PIVX', 'Firo': 'FIRO', 'Zcoin': 'FIRO',
- 'Dash': 'DASH', 'Decred': 'DCR', 'Wownero': 'WOW',
- 'Bitcoin Cash': 'BCH'
- };
- return symbolMap[fullName] || fullName;
+ const symbolMap = {
+ 'Bitcoin': 'BTC', 'Litecoin': 'LTC', 'Monero': 'XMR',
+ 'Particl': 'PART', 'Particl Blind': 'PART', 'Particl Anon': 'PART',
+ 'PIVX': 'PIVX', 'Firo': 'FIRO', 'Zcoin': 'FIRO',
+ 'Dash': 'DASH', 'Decred': 'DCR', 'Wownero': 'WOW',
+ 'Bitcoin Cash': 'BCH'
+ };
+ return symbolMap[fullName] || fullName;
}
function getDisplayName(coinName) {
- if (coinName.toLowerCase() === 'zcoin') {
- return 'Firo';
- }
- return coinNameToDisplayName[coinName] || coinName;
+ if (coinName.toLowerCase() === 'zcoin') {
+ return 'Firo';
+ }
+ return coinNameToDisplayName[coinName] || coinName;
}
-function getValidOffers() {
- if (isSentOffers) {
- return jsonData;
+function getCoinSymbolLowercase(coin) {
+ if (typeof coin === 'string') {
+ if (coin.toLowerCase() === 'bitcoin cash') {
+ return 'bitcoin-cash';
+ }
+ return (coinNameToSymbol[coin] || coin).toLowerCase();
+ } else if (coin && typeof coin === 'object' && coin.symbol) {
+ return coin.symbol.toLowerCase();
} else {
- const currentTime = Math.floor(Date.now() / 1000);
- return jsonData.filter(offer => offer.expire_at > currentTime);
+ console.warn('Invalid coin input:', coin);
+ return 'unknown';
}
}
-function removeExpiredOffers() {
- if (isSentOffers) {
- return false;
- }
- const currentTime = Math.floor(Date.now() / 1000);
- const initialLength = jsonData.length;
- jsonData = jsonData.filter(offer => offer.expire_at > currentTime);
+function coinMatches(offerCoin, filterCoin) {
+ if (!offerCoin || !filterCoin) return false;
- if (jsonData.length < initialLength) {
- console.log(`Removed ${initialLength - jsonData.length} expired offers`);
+ offerCoin = offerCoin.toLowerCase();
+ filterCoin = filterCoin.toLowerCase();
+
+ if (offerCoin === filterCoin) return true;
+
+ if ((offerCoin === 'firo' || offerCoin === 'zcoin') &&
+ (filterCoin === 'firo' || filterCoin === 'zcoin')) {
return true;
}
+
+ if ((offerCoin === 'bitcoincash' && filterCoin === 'bitcoin cash') ||
+ (offerCoin === 'bitcoin cash' && filterCoin === 'bitcoincash')) {
+ return true;
+ }
+
+ const particlVariants = ['particl', 'particl anon', 'particl blind'];
+ if (filterCoin === 'particl' && particlVariants.includes(offerCoin)) {
+ return true;
+ }
+
+ if (particlVariants.includes(filterCoin)) {
+ return offerCoin === filterCoin;
+ }
+
return false;
}
-function handleNoOffersScenario() {
- offersBody.innerHTML = '
No active offers available. Refreshing data... ';
- fetchOffers(true);
+function getCachedPrices() {
+ const cachedItem = localStorage.getItem(CACHE_KEY);
+ if (cachedItem) {
+ const { data, timestamp } = JSON.parse(cachedItem);
+ if (Date.now() - timestamp < CACHE_DURATION) {
+ return data;
+ }
+ }
+ return null;
}
-function logOfferStatus() {
- const validOffers = getValidOffers();
- console.log(`Total offers: ${jsonData.length}, Valid offers: ${validOffers.length}, Current page: ${currentPage}, Total pages: ${Math.ceil(validOffers.length / itemsPerPage)}`);
+function setCachedPrices(data) {
+ const cacheItem = {
+ data: data,
+ timestamp: Date.now()
+ };
+ localStorage.setItem(CACHE_KEY, JSON.stringify(cacheItem));
+}
+
+function getButtonProperties(isActuallyExpired, isSentOffers, isTreatedAsSentOffer, isRevoked) {
+ if (isRevoked) {
+ return {
+ buttonClass: 'bg-red-500 text-white hover:bg-red-600 transition duration-200',
+ buttonText: 'Revoked'
+ };
+ } else if (isActuallyExpired && isSentOffers) {
+ return {
+ buttonClass: 'bg-gray-400 text-white dark:border-gray-300 text-white hover:bg-red-700 transition duration-200',
+ buttonText: 'Expired'
+ };
+ } else if (isTreatedAsSentOffer) {
+ return {
+ buttonClass: 'bg-gray-300 bold text-white bold hover:bg-green-600 transition duration-200',
+ buttonText: 'Edit'
+ };
+ } else {
+ return {
+ buttonClass: 'bg-blue-500 text-white hover:bg-green-600 transition duration-200',
+ buttonText: 'Swap'
+ };
+ }
+}
+
+function getTimerColor(offer) {
+ const now = Math.floor(Date.now() / 1000);
+ const timeLeft = offer.expire_at - now;
+
+ if (timeLeft <= 300) { // 5 min or less
+ return "#9CA3AF"; // Grey
+ } else if (timeLeft <= 1800) { // 5-30 min
+ return "#3B82F6"; // Blue
+ } else { // More than 30 min
+ return "#10B981"; // Green
+ }
+}
+
+function getProfitColorClass(percentage) {
+ const numericPercentage = parseFloat(percentage);
+ if (numericPercentage > 0) return 'text-green-500';
+ if (numericPercentage < 0) return 'text-red-500';
+ if (numericPercentage === 0) return 'text-yellow-400';
+ return 'text-white';
}
function isOfferExpired(offer) {
@@ -250,51 +397,6 @@ function isOfferExpired(offer) {
return isExpired;
}
-function setRefreshButtonLoading(isLoading) {
- const refreshButton = document.getElementById('refreshOffers');
- const refreshIcon = document.getElementById('refreshIcon');
- const refreshText = document.getElementById('refreshText');
-
- refreshButton.disabled = isLoading;
- refreshIcon.classList.toggle('animate-spin', isLoading);
- refreshText.textContent = isLoading ? 'Refresh' : 'Refresh'; //to-do
-}
-
-function escapeHtml(unsafe) {
- if (typeof unsafe !== 'string') {
- console.warn('escapeHtml received a non-string value:', unsafe);
- return '';
- }
- return unsafe
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-}
-
-function formatTimeDifference(timestamp) {
- const now = Math.floor(Date.now() / 1000);
- const diff = Math.abs(now - timestamp);
-
- if (diff < 60) return `${diff} seconds`;
- if (diff < 3600) return `${Math.floor(diff / 60)} minutes`;
- if (diff < 86400) return `${Math.floor(diff / 3600)} hours`;
- if (diff < 2592000) return `${Math.floor(diff / 86400)} days`;
- if (diff < 31536000) return `${Math.floor(diff / 2592000)} months`;
- return `${Math.floor(diff / 31536000)} years`;
-}
-
-function formatTimeAgo(timestamp) {
- return `${formatTimeDifference(timestamp)} ago`;
-}
-
-function formatTimeLeft(timestamp) {
- const now = Math.floor(Date.now() / 1000);
- if (timestamp <= now) return "Expired";
- return formatTimeDifference(timestamp);
-}
-
function getTimeUntilNextExpiration() {
const currentTime = Math.floor(Date.now() / 1000);
const nextExpiration = jsonData.reduce((earliest, offer) => {
@@ -305,1332 +407,70 @@ function getTimeUntilNextExpiration() {
return Math.max(MIN_REFRESH_INTERVAL, Math.min(nextExpiration, 300));
}
-function getNoOffersMessage() {
- const formData = new FormData(filterForm);
- const filters = Object.fromEntries(formData);
- let message = 'No offers available';
- if (filters.coin_to !== 'any') {
- const coinToName = coinIdToName[filters.coin_to] || filters.coin_to;
- message += ` for bids to ${coinToName}`;
- }
- if (filters.coin_from !== 'any') {
- const coinFromName = coinIdToName[filters.coin_from] || filters.coin_from;
- message += ` for offers from ${coinFromName}`;
- }
- if (isSentOffers && filters.active && filters.active !== 'any') {
- message += ` with status: ${filters.active}`;
- }
- return message;
-}
-
-window.tableRateModule = {
- coinNameToSymbol: {
- 'Bitcoin': 'BTC',
- 'Particl': 'PART',
- 'Particl Blind': 'PART',
- 'Particl Anon': 'PART',
- 'Monero': 'XMR',
- 'Wownero': 'WOW',
- 'Litecoin': 'LTC',
- 'Firo': 'FIRO',
- 'Dash': 'DASH',
- 'PIVX': 'PIVX',
- 'Decred': 'DCR',
- 'Zano': 'ZANO',
- 'Bitcoin Cash': 'BCH',
- 'Dogecoin': 'DOGE'
- },
-
- cache: {},
- processedOffers: new Set(),
-
- getCachedValue(key) {
- const cachedItem = localStorage.getItem(key);
- if (cachedItem) {
- const parsedItem = JSON.parse(cachedItem);
- if (Date.now() < parsedItem.expiry) {
- return parsedItem.value;
- } else {
- localStorage.removeItem(key);
- }
- }
- return null;
- },
-
- setCachedValue(key, value, ttl = 900000) {
- const item = {
- value: value,
- expiry: Date.now() + ttl,
- };
- localStorage.setItem(key, JSON.stringify(item));
- },
-
- setFallbackValue(coinSymbol, value) {
- this.setCachedValue(`fallback_${coinSymbol}_usd`, value, 24 * 60 * 60 * 1000);
- },
-
- isNewOffer(offerId) {
- if (this.processedOffers.has(offerId)) {
- return false;
- }
- this.processedOffers.add(offerId);
- return true;
- },
-
- formatUSD(value) {
- if (Math.abs(value) < 0.000001) {
- return value.toExponential(8) + ' USD';
- } else if (Math.abs(value) < 0.01) {
- return value.toFixed(8) + ' USD';
- } else {
- return value.toFixed(2) + ' USD';
- }
- },
-
- formatNumber(value, decimals) {
- if (Math.abs(value) < 0.000001) {
- return value.toExponential(decimals);
- } else if (Math.abs(value) < 0.01) {
- return value.toFixed(decimals);
- } else {
- return value.toFixed(Math.min(2, decimals));
- }
- },
-
- getFallbackValue(coinSymbol) {
- const value = localStorage.getItem(`fallback_${coinSymbol}_usd`);
- return value ? parseFloat(value) : null;
- },
-
- initializeTable() {
- console.log('Initializing table');
- document.querySelectorAll('.coinname-value').forEach(coinNameValue => {
- const coinFullNameOrSymbol = coinNameValue.getAttribute('data-coinname');
- console.log('Processing coin:', coinFullNameOrSymbol);
- if (!coinFullNameOrSymbol || coinFullNameOrSymbol === 'Unknown') {
- console.warn('Missing or unknown coin name/symbol in data-coinname attribute');
- return;
- }
- coinNameValue.classList.remove('hidden');
- if (!coinNameValue.textContent.trim()) {
- coinNameValue.textContent = 'N/A';
- }
- });
-
- document.querySelectorAll('.usd-value').forEach(usdValue => {
- if (!usdValue.textContent.trim()) {
- usdValue.textContent = 'N/A';
- }
- });
-
- document.querySelectorAll('.profit-loss').forEach(profitLoss => {
- if (!profitLoss.textContent.trim() || profitLoss.textContent === 'Calculating...') {
- profitLoss.textContent = 'N/A';
- }
- });
- },
-
- init() {
- console.log('Initializing TableRateModule');
- this.initializeTable();
- }
-};
-
-function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
- const profitLossElement = row.querySelector('.profit-loss');
- if (!profitLossElement) {
- console.warn('Profit loss element not found in row');
- return;
- }
-
- calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
- .then(percentDiff => {
- if (percentDiff === null) {
- profitLossElement.textContent = 'N/A';
- profitLossElement.className = 'profit-loss text-lg font-bold text-gray-500';
- console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`);
- return;
- }
-
- const formattedPercentDiff = percentDiff.toFixed(2);
- const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
- (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
-
- const colorClass = getProfitColorClass(percentDiff);
- profitLossElement.textContent = `${percentDiffDisplay}%`;
- profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
-
- const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
- const tooltipElement = document.getElementById(tooltipId);
- if (tooltipElement) {
- const tooltipContent = createTooltipContent(isOwnOffer, fromCoin, toCoin, fromAmount, toAmount);
- tooltipElement.innerHTML = `
-
- ${tooltipContent}
-
-
- `;
- }
-
- console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isOwnOffer: ${isOwnOffer}`);
- })
- .catch(error => {
- console.error('Error in updateProfitLoss:', error);
- profitLossElement.textContent = 'Error';
- profitLossElement.className = 'profit-loss text-lg font-bold text-red-500';
- });
-}
-
-function fetchOffers(manualRefresh = false) {
- return new Promise((resolve, reject) => {
- const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
- console.log(`Fetching offers from: ${endpoint}`);
-
- if (newEntriesCountSpan) {
- newEntriesCountSpan.textContent = 'Loading...';
- }
-
- if (manualRefresh) {
- offersBody.innerHTML = 'Refreshing offers... ';
- }
-
- setRefreshButtonLoading(true);
-
- const requestBody = {
- with_extra_info: true
- };
-
- fetch(endpoint, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify(requestBody)
- })
- .then(response => {
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- return response.json();
- })
- .then(data => {
- console.log('Raw data received:', data.length, 'offers');
-
- let newData = Array.isArray(data) ? data : Object.values(data);
- console.log('Processed data length before filtering:', newData.length);
-
- newData = newData.map(offer => ({
- ...offer,
- offer_id: String(offer.offer_id || ''),
- swap_type: String(offer.swap_type || 'N/A'),
- addr_from: String(offer.addr_from || ''),
- coin_from: String(offer.coin_from || ''),
- coin_to: String(offer.coin_to || ''),
- amount_from: String(offer.amount_from || '0'),
- amount_to: String(offer.amount_to || '0'),
- rate: String(offer.rate || '0'),
- created_at: Number(offer.created_at || 0),
- expire_at: Number(offer.expire_at || 0),
- is_own_offer: Boolean(offer.is_own_offer),
- amount_negotiable: Boolean(offer.amount_negotiable),
- unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}`
- }));
-
- if (!isSentOffers) {
- const currentTime = Math.floor(Date.now() / 1000);
- const beforeFilterCount = newData.length;
- newData = newData.filter(offer => {
- const keepOffer = !isOfferExpired(offer);
- if (!keepOffer) {
- console.log('Filtered out expired offer:', offer.offer_id);
- }
- return keepOffer;
- });
- console.log(`Filtered out ${beforeFilterCount - newData.length} expired offers`);
- }
-
- console.log('Processed data length after filtering:', newData.length);
-
- if (isInitialLoad || manualRefresh) {
- console.log('Initial load or manual refresh - replacing all data');
- jsonData = newData;
- originalJsonData = [...newData];
- isInitialLoad = false;
- } else {
- console.log('Updating existing data');
- console.log('Current jsonData length:', jsonData.length);
-
- const mergedData = [...jsonData];
- newData.forEach(newOffer => {
- const existingIndex = mergedData.findIndex(existing => existing.offer_id === newOffer.offer_id);
- if (existingIndex !== -1) {
- mergedData[existingIndex] = newOffer;
- } else {
- mergedData.push(newOffer);
- }
- });
-
- jsonData = isSentOffers ? mergedData : mergedData.filter(offer => !isOfferExpired(offer));
- }
-
- console.log('Final jsonData length:', jsonData.length);
-
- if (!isSentOffers) {
- removeExpiredOffers();
- }
-
- const validOffers = getValidOffers();
- const validItemCount = validOffers.length;
- if (newEntriesCountSpan) {
- newEntriesCountSpan.textContent = validItemCount;
- }
- console.log('Valid offers count:', validItemCount);
-
- lastRefreshTime = Date.now();
- nextRefreshCountdown = getTimeUntilNextExpiration();
- updateLastRefreshTime();
- updateNextRefreshTime();
-
- updateOffersTable();
- updateJsonView();
- updatePaginationInfo();
-
- if (validItemCount === 0) {
- handleNoOffersScenario();
- }
-
- if (manualRefresh) {
- console.log('Offers refreshed successfully');
- }
-
- resolve();
- })
- .catch(error => {
- console.error(`Error fetching ${isSentOffers ? 'sent offers' : 'offers'}:`, error);
-
- let errorMessage = 'An error occurred while fetching offers. ';
- if (error.message.includes('HTTP error')) {
- errorMessage += 'The server returned an error. ';
- } else if (error.message.includes('empty data')) {
- errorMessage += 'No offer data was received. ';
- } else if (error.name === 'TypeError') {
- errorMessage += 'There was a problem parsing the response. ';
- } else {
- errorMessage += 'Please check your network connection. ';
- }
- errorMessage += 'Please try again later.';
-
- if (typeof ui !== 'undefined' && ui.displayErrorMessage) {
- ui.displayErrorMessage(errorMessage);
- } else {
- console.error(errorMessage);
- offersBody.innerHTML = `${escapeHtml(errorMessage)} `;
- }
-
- isInitialLoad = false;
- updateOffersTable();
- updateJsonView();
- updatePaginationInfo();
-
- if (newEntriesCountSpan) {
- newEntriesCountSpan.textContent = '0';
- }
-
- reject(error);
- })
- .finally(() => {
- setRefreshButtonLoading(false);
- });
- });
-}
-
-function applyFilters() {
- console.log('Applying filters');
- jsonData = filterAndSortData();
- updateOffersTable();
- updateJsonView();
- updatePaginationInfo();
- console.log('Filters applied, table updated');
-}
-
-function filterAndSortData() {
- console.log('Filtering and sorting data');
-
- const formData = new FormData(filterForm);
- const filters = Object.fromEntries(formData);
- console.log('Processed filters:', filters);
-
- if (filters.coin_to !== 'any') {
- filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to;
- }
- if (filters.coin_from !== 'any') {
- filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from;
- }
-
- console.log('Processed filters:', filters);
-
- const currentTime = Math.floor(Date.now() / 1000);
-
- const uniqueOffersMap = new Map();
-
- const isFiroOrZcoin = (coin) => ['firo', 'zcoin'].includes(coin.toLowerCase());
- const isParticlVariant = (coin) => ['particl', 'particl anon', 'particl blind'].includes(coin.toLowerCase());
-
- const coinMatches = (offerCoin, filterCoin) => {
- offerCoin = offerCoin.toLowerCase();
- filterCoin = filterCoin.toLowerCase();
-
- if (offerCoin === filterCoin) return true;
- if (isFiroOrZcoin(offerCoin) && isFiroOrZcoin(filterCoin)) return true;
- if (filterCoin === 'bitcoincash' && offerCoin === 'bitcoin cash') return true;
- if (filterCoin === 'bitcoin cash' && offerCoin === 'bitcoincash') return true;
-
- if (isParticlVariant(filterCoin)) {
- return offerCoin === filterCoin;
- }
-
- if (filterCoin === 'particl' && isParticlVariant(offerCoin)) return true;
-
- return false;
-};
-
- originalJsonData.forEach(offer => {
- const coinFrom = (offer.coin_from || '');
- const coinTo = (offer.coin_to || '');
- const isExpired = offer.expire_at <= currentTime;
-
- if (!isSentOffers && isExpired) {
- return;
- }
-
- let passesFilter = true;
-
- if (filters.coin_to !== 'any' && !coinMatches(coinTo, filters.coin_to)) {
- passesFilter = false;
- }
- if (filters.coin_from !== 'any' && !coinMatches(coinFrom, filters.coin_from)) {
- passesFilter = false;
- }
-
- if (isSentOffers && filters.active && filters.active !== 'any') {
- const offerState = isExpired ? 'expired' : 'active';
- if (filters.active !== offerState) {
- passesFilter = false;
- }
- }
-
- if (passesFilter) {
- uniqueOffersMap.set(offer.unique_id, offer);
- }
- });
-
- let filteredData = Array.from(uniqueOffersMap.values());
-
- console.log('Filtered data length:', filteredData.length);
-
- const sortBy = filters.sort_by || 'created_at';
- const sortDir = filters.sort_dir || 'desc';
-
- filteredData.sort((a, b) => {
- let aValue, bValue;
-
- switch (sortBy) {
- case 'created_at':
- aValue = a.created_at;
- bValue = b.created_at;
- break;
- case 'rate':
- aValue = parseFloat(a.rate);
- bValue = parseFloat(b.rate);
- break;
- default:
- aValue = a.created_at;
- bValue = b.created_at;
- }
-
- if (sortDir === 'asc') {
- return aValue - bValue;
- } else {
- return bValue - aValue;
- }
- });
-
- console.log(`Sorted offers by ${sortBy} in ${sortDir} order`);
-
- return filteredData;
-}
-
-function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
- const profitLossElement = row.querySelector('.profit-loss');
- if (!profitLossElement) {
- console.warn('Profit loss element not found in row');
- return;
- }
-
- calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
- .then(percentDiff => {
- if (percentDiff === null) {
- profitLossElement.textContent = 'N/A';
- profitLossElement.className = 'profit-loss text-lg font-bold text-gray-500';
- console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`);
- return;
- }
-
- // Format the percentage to two decimal places
- const formattedPercentDiff = percentDiff.toFixed(2);
- const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
- (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
-
- const colorClass = getProfitColorClass(percentDiff);
- profitLossElement.textContent = `${percentDiffDisplay}%`;
- profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
-
- const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
- const tooltipElement = document.getElementById(tooltipId);
- if (tooltipElement) {
- const tooltipContent = createTooltipContent(isOwnOffer, fromCoin, toCoin, fromAmount, toAmount);
- tooltipElement.innerHTML = `
-
- ${tooltipContent}
-
-
- `;
- }
-
- console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isOwnOffer: ${isOwnOffer}`);
- })
- .catch(error => {
- console.error('Error in updateProfitLoss:', error);
- profitLossElement.textContent = 'Error';
- profitLossElement.className = 'profit-loss text-lg font-bold text-red-500';
- });
-}
-
-function createTableRow(offer, isSentOffers) {
- 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.setAttribute('data-offer-id', uniqueId);
-
- const coinFrom = offer.coin_from;
- const coinTo = offer.coin_to;
- const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase();
- const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase();
- const coinFromDisplay = getDisplayName(coinFrom);
- const coinToDisplay = getDisplayName(coinTo);
-
- const postedTime = formatTimeAgo(offer.created_at);
- const expiresIn = formatTimeLeft(offer.expire_at);
-
- const currentTime = Math.floor(Date.now() / 1000);
- const isActuallyExpired = currentTime > offer.expire_at;
-
- const isOwnOffer = offer.is_own_offer;
-
- const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, isOwnOffer);
-
- const fromAmount = parseFloat(offer.amount_from) || 0;
- const toAmount = parseFloat(offer.amount_to) || 0;
-
- row.innerHTML = `
- ${createTimeColumn(offer, postedTime, expiresIn)}
- ${createDetailsColumn(offer)}
- ${createTakerAmountColumn(offer, coinTo, coinFrom)}
- ${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)}
- ${createOrderbookColumn(offer, coinFrom, coinTo)}
- ${createRateColumn(offer, coinFrom, coinTo)}
- ${createPercentageColumn(offer)}
- ${createActionColumn(offer, buttonClass, buttonText)}
- ${createTooltips(offer, isOwnOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired)}
- `;
-
- updateTooltipTargets(row, uniqueId);
-
- updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer);
-
- return row;
-}
-
-function prepareOfferData(offer, isSentOffers) {
- const coinFrom = offer.coin_from;
- const coinTo = offer.coin_to;
-
- const postedTime = formatTimeAgo(offer.created_at);
- const expiresIn = formatTimeLeft(offer.expire_at);
-
- const currentTime = Math.floor(Date.now() / 1000);
- const isActuallyExpired = currentTime > offer.expire_at;
-
- const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, offer.is_own_offer);
-
- const clockColor = isActuallyExpired ? "#9CA3AF" : "#3B82F6";
-
- return {
- coinFrom, coinTo,
- postedTime, expiresIn, isActuallyExpired,
- buttonClass, buttonText, clockColor
- };
-}
-
-// to-do revoked
-function getButtonProperties(isActuallyExpired, isSentOffers, isTreatedAsSentOffer, isRevoked) {
- if (isRevoked) {
- return {
- buttonClass: 'bg-red-500 text-white hover:bg-red-600 transition duration-200',
- buttonText: 'Revoked'
- };
- } else if (isActuallyExpired && isSentOffers) {
- return {
- buttonClass: 'bg-gray-400 text-white dark:border-gray-300 text-white hover:bg-red-700 transition duration-200',
- buttonText: 'Expired'
- };
- } else if (isTreatedAsSentOffer) {
- return {
- buttonClass: 'bg-gray-300 bold text-white bold hover:bg-green-600 transition duration-200',
- buttonText: 'Edit'
- };
- } else {
- return {
- buttonClass: 'bg-blue-500 text-white hover:bg-green-600 transition duration-200',
- buttonText: 'Swap'
- };
- }
-}
-
-async function updateOffersTable() {
- console.log('Starting updateOffersTable function');
- console.log(`Is Sent Offers page: ${isSentOffers}`);
-
- try {
- const priceData = await fetchLatestPrices();
- if (!priceData) {
- console.error('Failed to fetch latest prices. Using last known prices or proceeding without price data.');
- } else {
- console.log('Latest prices fetched successfully');
- latestPrices = priceData;
- }
-
- let validOffers = getValidOffers();
- console.log(`Valid offers: ${validOffers.length}`);
-
- if (validOffers.length === 0) {
- console.log('No valid offers found. Handling no offers scenario.');
- handleNoOffersScenario();
- return;
- }
-
- const totalPages = Math.max(1, Math.ceil(validOffers.length / itemsPerPage));
- currentPage = Math.min(currentPage, totalPages);
-
- const startIndex = (currentPage - 1) * itemsPerPage;
- const endIndex = startIndex + itemsPerPage;
- const itemsToDisplay = validOffers.slice(startIndex, endIndex);
-
- console.log(`Displaying offers ${startIndex + 1} to ${endIndex} of ${validOffers.length}`);
-
- offersBody.innerHTML = '';
-
- for (const offer of itemsToDisplay) {
- const row = createTableRow(offer, isSentOffers);
- if (row) {
- offersBody.appendChild(row);
- try {
- const fromAmount = parseFloat(offer.amount_from);
- const toAmount = parseFloat(offer.amount_to);
- await updateProfitLoss(row, offer.coin_from, offer.coin_to, fromAmount, toAmount, offer.is_own_offer);
- } catch (error) {
- console.error(`Error updating profit/loss for offer ${offer.offer_id}:`, error);
- }
- }
- }
-
- updateRowTimes();
- initializeFlowbiteTooltips();
- updatePaginationInfo();
-
- if (tableRateModule && typeof tableRateModule.initializeTable === 'function') {
- tableRateModule.initializeTable();
- }
-
- logOfferStatus();
-
- lastRefreshTime = Date.now();
- nextRefreshCountdown = getTimeUntilNextExpiration();
- updateLastRefreshTime();
- updateNextRefreshTime();
-
- if (newEntriesCountSpan) {
- newEntriesCountSpan.textContent = validOffers.length;
- }
-
- } catch (error) {
- console.error('Error updating offers table:', error);
- offersBody.innerHTML = `An error occurred while updating the offers table. Please try again later. `;
- } finally {
- setRefreshButtonLoading(false);
- }
-}
-
-function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
- const profitLossElement = row.querySelector('.profit-loss');
- if (!profitLossElement) {
- console.warn('Profit loss element not found in row');
- return;
- }
-
- if (!fromCoin || !toCoin) {
- console.error(`Invalid coin names: fromCoin=${fromCoin}, toCoin=${toCoin}`);
- profitLossElement.textContent = 'Error';
- profitLossElement.className = 'profit-loss text-lg font-bold text-red-500';
- return;
- }
-
- calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
- .then(percentDiff => {
- if (percentDiff === null) {
- profitLossElement.textContent = 'N/A';
- profitLossElement.className = 'profit-loss text-lg font-bold text-gray-500';
- console.log(`Unable to calculate profit/loss for ${fromCoin} to ${toCoin}`);
- return;
- }
-
- // Format the percentage to two decimal places
- const formattedPercentDiff = percentDiff.toFixed(2);
- const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
- (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
-
- const colorClass = getProfitColorClass(percentDiff);
- profitLossElement.textContent = `${percentDiffDisplay}%`;
- profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
-
- const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
- const tooltipElement = document.getElementById(tooltipId);
- if (tooltipElement) {
- const tooltipContent = createTooltipContent(isSentOffers || isOwnOffer, fromCoin, toCoin, fromAmount, toAmount);
- tooltipElement.innerHTML = `
-
- ${tooltipContent}
-
-
- `;
- }
-
- console.log(`Updated profit/loss display: ${profitLossElement.textContent}, isSentOffers: ${isSentOffers}, isOwnOffer: ${isOwnOffer}`);
- })
- .catch(error => {
- console.error('Error in updateProfitLoss:', error);
- profitLossElement.textContent = 'Error';
- profitLossElement.className = 'profit-loss text-lg font-bold text-red-500';
- });
-}
-
-async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
- console.log(`Calculating profit/loss for ${fromAmount} ${fromCoin} to ${toAmount} ${toCoin}, isOwnOffer: ${isOwnOffer}`);
-
- if (!latestPrices) {
- console.error('Latest prices not available. Unable to calculate profit/loss.');
- return null;
- }
-
- const getPriceKey = (coin) => {
- const lowerCoin = coin.toLowerCase();
- if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
- return 'zcoin';
- }
- if (lowerCoin === 'bitcoin cash') {
- return 'bitcoin-cash';
- }
- return coinNameToSymbol[coin] || lowerCoin;
- };
-
- const fromSymbol = getPriceKey(fromCoin);
- const toSymbol = getPriceKey(toCoin);
-
- const fromPriceUSD = latestPrices[fromSymbol]?.usd;
- const toPriceUSD = latestPrices[toSymbol]?.usd;
-
- if (!fromPriceUSD || !toPriceUSD) {
- console.error(`Price data missing for ${fromSymbol} or ${toSymbol}`);
- return null;
- }
-
- const fromValueUSD = fromAmount * fromPriceUSD;
- const toValueUSD = toAmount * toPriceUSD;
-
- let percentDiff;
- if (isOwnOffer) {
- percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
- } else {
- percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
- }
-
- console.log(`Percent difference: ${percentDiff.toFixed(2)}%`);
- return percentDiff;
-}
-
-function getProfitColorClass(percentage) {
- const numericPercentage = parseFloat(percentage);
- if (numericPercentage > 0) return 'text-green-500';
- if (numericPercentage < 0) return 'text-red-500';
- if (numericPercentage === 0) return 'text-yellow-400';
- return 'text-white';
-}
-
-function getMarketRate(fromCoin, toCoin) {
- return new Promise((resolve) => {
- console.log(`Attempting to get market rate for ${fromCoin} to ${toCoin}`);
- if (!latestPrices) {
- console.warn('Latest prices object is not available');
- resolve(null);
- return;
- }
-
- const getPriceKey = (coin) => {
- const lowerCoin = coin.toLowerCase();
- if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
- return 'zcoin';
- }
- if (lowerCoin === 'bitcoin cash') {
- return 'bitcoin-cash';
- }
- return coinNameToSymbol[coin] || lowerCoin;
- };
-
- const fromSymbol = getPriceKey(fromCoin);
- const toSymbol = getPriceKey(toCoin);
-
- const fromPrice = latestPrices[fromSymbol]?.usd;
- const toPrice = latestPrices[toSymbol]?.usd;
- if (!fromPrice || !toPrice) {
- console.warn(`Missing price data for ${!fromPrice ? fromCoin : toCoin}`);
- resolve(null);
- return;
- }
- const rate = toPrice / fromPrice;
- console.log(`Market rate calculated: ${rate} ${toCoin}/${fromCoin}`);
- resolve(rate);
- });
-}
-
-function getTimerColor(offer) {
- const now = Math.floor(Date.now() / 1000);
- const timeLeft = offer.expire_at - now;
-
- if (timeLeft <= 300) { // 5 minutes or less
- return "#9CA3AF"; // Grey
- } else if (timeLeft <= 1800) { // 5-30 minutes
- return "#3B82F6"; // Blue
- } else { // More than 30 minutes
- return "#10B981"; // Green/Turquoise
- }
-}
-
-function createTimeColumn(offer, postedTime, expiresIn) {
- const timerColor = getTimerColor(offer);
-
- return `
-
-
-
-
-
Posted: ${escapeHtml(postedTime)}
-
Expires in: ${escapeHtml(expiresIn)}
-
-
-
- `;
-}
-
-function createDetailsColumn(offer) {
- const addrFrom = offer.addr_from || '';
- return `
-
-
- Recipient: ${escapeHtml(addrFrom.substring(0, 10))}...
-
-
-
- `;
-}
-
-function createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol) {
- const getImageFilename = (symbol, displayName) => {
- if (displayName.toLowerCase() === 'zcoin' || displayName.toLowerCase() === 'firo') {
- return 'Firo.png';
- }
- return `${displayName.replace(' ', '-')}.png`;
- };
-
- return `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- `;
-}
-
-function createTakerAmountColumn(offer, coinFrom, coinTo) {
- const fromAmount = parseFloat(offer.amount_to);
- const fromSymbol = getCoinSymbol(coinFrom);
- return `
-
-
-
- `;
-}
-
-function createOrderbookColumn(offer, coinTo, coinFrom) {
- const toAmount = parseFloat(offer.amount_from);
- const toSymbol = getCoinSymbol(coinTo);
- return `
-
-
-
- `;
-}
-
function calculateInverseRate(rate) {
return (1 / parseFloat(rate)).toFixed(8);
}
-function createRateColumn(offer, coinFrom, coinTo) {
- const rate = parseFloat(offer.rate);
- const inverseRate = 1 / rate;
- const fromSymbol = getCoinSymbol(coinFrom);
- const toSymbol = getCoinSymbol(coinTo);
-
- const getPriceKey = (coin) => {
- const lowerCoin = coin.toLowerCase();
- if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
- return 'zcoin';
- }
- if (lowerCoin === 'bitcoin cash') {
- return 'bitcoin-cash';
- }
- return coinNameToSymbol[coin] || lowerCoin;
- };
-
- const fromPriceUSD = latestPrices[getPriceKey(coinFrom)]?.usd || 0;
- const toPriceUSD = latestPrices[getPriceKey(coinTo)]?.usd || 0;
-
- const rateInUSD = rate * toPriceUSD;
-
- // Add logging for debugging
- console.log(`Rate calculation for ${fromSymbol} to ${toSymbol}:`);
- console.log(`Using price keys: ${getPriceKey(coinFrom)}, ${getPriceKey(coinTo)}`);
- console.log(`Got prices: $${fromPriceUSD}, $${toPriceUSD}`);
- console.log(`Rate: ${rate} ${toSymbol}/${fromSymbol}`);
- console.log(`Rate in USD: $${rateInUSD.toFixed(2)}`);
-
- return `
-
-
-
-
- $${rateInUSD.toFixed(2)} USD
-
-
- ${rate.toFixed(8)} ${toSymbol}/${fromSymbol}
-
-
- ${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol}
-
-
-
-
- `;
-}
-
-function createPercentageColumn(offer) {
- return `
-
-
-
-
- Calculating...
-
-
-
-
- `;
-}
-
-function createActionColumn(offer, buttonClass, buttonText) {
- return `
-
-
-
- `;
-}
-
-function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired) {
- const rate = parseFloat(offer.rate);
- const fromSymbol = getCoinSymbolLowercase(coinFrom);
- const toSymbol = getCoinSymbolLowercase(coinTo);
-
- const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
- const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
- const rateInUSD = rate * toPriceUSD;
-
- const combinedRateTooltip = createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer);
-
- const percentageTooltipContent = createTooltipContent(treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount);
-
- const uniqueId = `${offer.offer_id}_${offer.created_at}`;
-
- return `
-
+function hasActiveFilters() {
+ const formData = new FormData(filterForm);
+ const filters = Object.fromEntries(formData);
-
-
-
-
-
-
-
-
-
+ 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 updateTooltipTargets(row, uniqueId) {
- row.querySelector('[data-tooltip-target^="tooltip-active"]').setAttribute('data-tooltip-target', `tooltip-active-${uniqueId}`);
- row.querySelector('[data-tooltip-target^="tooltip-recipient"]').setAttribute('data-tooltip-target', `tooltip-recipient-${uniqueId}`);
- row.querySelector('[data-tooltip-target^="tooltip-wallet"]').setAttribute('data-tooltip-target', `tooltip-wallet-${uniqueId}`);
- row.querySelector('[data-tooltip-target^="tooltip-offer"]').setAttribute('data-tooltip-target', `tooltip-offer-${uniqueId}`);
- row.querySelector('[data-tooltip-target^="tooltip-wallet-maker"]').setAttribute('data-tooltip-target', `tooltip-wallet-maker-${uniqueId}`);
- row.querySelector('[data-tooltip-target^="tooltip-rate"]').setAttribute('data-tooltip-target', `tooltip-rate-${uniqueId}`);
- row.querySelector('[data-tooltip-target^="percentage-tooltip"]').setAttribute('data-tooltip-target', `percentage-tooltip-${uniqueId}`);
-}
-
-function getCoinSymbolLowercase(coin) {
- if (typeof coin === 'string') {
- if (coin.toLowerCase() === 'bitcoin cash') {
- return 'bitcoin-cash';
+function updateClearFiltersButton() {
+ const clearButton = document.getElementById('clearFilters');
+ if (clearButton) {
+ clearButton.classList.toggle('opacity-50', !hasActiveFilters());
+ clearButton.disabled = !hasActiveFilters();
}
- return (coinNameToSymbol[coin] || coin).toLowerCase();
- } else if (coin && typeof coin === 'object' && coin.symbol) {
- return coin.symbol.toLowerCase();
- } else {
- console.warn('Invalid coin input:', coin);
- return 'unknown';
- }
}
-function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) {
- if (!coinFrom || !coinTo) {
- console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`);
- return `Unable to calculate profit/loss
- Invalid coin data.
`;
- }
+function setRefreshButtonLoading(isLoading) {
+ const refreshButton = document.getElementById('refreshOffers');
+ const refreshIcon = document.getElementById('refreshIcon');
+ const refreshText = document.getElementById('refreshText');
- fromAmount = parseFloat(fromAmount) || 0;
- toAmount = parseFloat(toAmount) || 0;
-
- const getPriceKey = (coin) => {
- const lowerCoin = coin.toLowerCase();
- return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' : coinNameToSymbol[coin] || lowerCoin;
- };
-
- const fromSymbol = getPriceKey(coinFrom);
- const toSymbol = getPriceKey(coinTo);
- const fromPriceUSD = latestPrices[fromSymbol]?.usd;
- const toPriceUSD = latestPrices[toSymbol]?.usd;
-
- if (!fromPriceUSD || !toPriceUSD) {
- return `Unable to calculate profit/loss
- Price data is missing for one or both coins.
`;
- }
-
- const fromValueUSD = fromAmount * fromPriceUSD;
- const toValueUSD = toAmount * toPriceUSD;
- const profitUSD = toValueUSD - fromValueUSD;
-
- const marketRate = fromPriceUSD / toPriceUSD;
- const offerRate = toAmount / fromAmount;
- let percentDiff;
-
- if (isSentOffers || isOwnOffer) {
- percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
- } else {
- percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
- }
-
- const formattedPercentDiff = percentDiff.toFixed(2);
- const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
- (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
-
- const profitLabel = (isSentOffers || isOwnOffer) ? "Max Profit" : "Max Loss";
- const actionLabel = (isSentOffers || isOwnOffer) ? "selling" : "buying";
- const directionLabel = (isSentOffers || isOwnOffer) ? "receiving" : "paying";
-
- return `
- Profit/Loss Calculation:
- You are ${actionLabel} ${fromAmount.toFixed(8)} ${coinFrom} ($${fromValueUSD.toFixed(2)} USD) and ${directionLabel} ${toAmount.toFixed(8)} ${coinTo} ($${toValueUSD.toFixed(2)} USD).
- Percentage difference: ${percentDiffDisplay}%
- ${profitLabel}: ${profitUSD > 0 ? '' : '-'}$${profitUSD.toFixed(2)} USD
- Calculation:
- Percentage = ${(isSentOffers || isOwnOffer) ?
- "((To Amount in USD / From Amount in USD) - 1) * 100" :
- "((From Amount in USD / To Amount in USD) - 1) * 100"}
- USD ${profitLabel} = To Amount in USD - From Amount in USD
- Interpretation:
- ${(isSentOffers || isOwnOffer) ? `
- Positive percentage: You're selling above market rate (profitable)
- Negative percentage: You're selling below market rate (loss)
- ` : `
- Positive percentage: You're buying below market rate (savings)
- Negative percentage: You're buying above market rate (premium)
- `}
- Note: ${(isSentOffers || isOwnOffer) ?
- "As a seller, a positive percentage means you're selling for more than the current market value." :
- "As a buyer, a positive percentage indicates potential savings compared to current market rates."}
- Market Rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}
- Offer Rate: 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}
- `;
+ refreshButton.disabled = isLoading;
+ refreshIcon.classList.toggle('animate-spin', isLoading);
+ refreshText.textContent = isLoading ? 'Refresh' : 'Refresh';
}
-function createCombinedRateTooltip(offer, coinFrom, coinTo, isSentOffers, treatAsSentOffer) {
- const rate = parseFloat(offer.rate);
- const inverseRate = 1 / rate;
-
- const getPriceKey = (coin) => {
- const lowerCoin = coin.toLowerCase();
- if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
- return 'zcoin';
- }
- if (lowerCoin === 'bitcoin cash') {
- return 'bitcoin-cash';
- }
- return coinNameToSymbol[coin] || lowerCoin;
- };
-
- const fromSymbol = getPriceKey(coinFrom);
- const toSymbol = getPriceKey(coinTo);
-
- const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
- const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
- const rateInUSD = rate * toPriceUSD;
-
- const marketRate = fromPriceUSD / toPriceUSD;
-
- const percentDiff = ((rate - marketRate) / marketRate) * 100;
- const formattedPercentDiff = percentDiff.toFixed(2);
- const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
- (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
- const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at";
-
- const action = isSentOffers || treatAsSentOffer ? "selling" : "buying";
-
- return `
- Exchange Rate Explanation:
- This offer is ${action} ${coinFrom} for ${coinTo} at a rate that is ${percentDiffDisplay}% ${aboveOrBelow} market price.
- Exchange Rates:
- 1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}
- 1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}
- USD Equivalent:
- 1 ${coinFrom} = $${rateInUSD.toFixed(2)} USD
- Current market prices:
- ${coinFrom}: $${fromPriceUSD.toFixed(2)} USD
- ${coinTo}: $${toPriceUSD.toFixed(2)} USD
- Market rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}
- `;
-}
-
-function updatePaginationInfo() {
- const validOffers = getValidOffers();
- const validItemCount = validOffers.length;
- const totalPages = Math.max(1, Math.ceil(validItemCount / itemsPerPage));
-
- currentPage = Math.max(1, Math.min(currentPage, totalPages));
-
- currentPageSpan.textContent = currentPage;
- totalPagesSpan.textContent = totalPages;
-
- prevPageButton.classList.toggle('invisible', currentPage === 1 || validItemCount === 0);
- nextPageButton.classList.toggle('invisible', currentPage === totalPages || validItemCount === 0);
-
- prevPageButton.style.display = currentPage === 1 ? 'none' : 'inline-flex';
- nextPageButton.style.display = (currentPage === totalPages || validItemCount === 0) ? 'none' : 'inline-flex';
-
- if (lastRefreshTime) {
- lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString();
- }
-
- const newEntriesCountSpan = document.getElementById('newEntriesCount');
- if (newEntriesCountSpan) {
- newEntriesCountSpan.textContent = validItemCount;
- }
-
- logOfferStatus();
-}
-
-function updateJsonView() {
- jsonContent.textContent = JSON.stringify(jsonData, null, 2);
-}
-
-function updateLastRefreshTime() {
- lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString();
-}
-
-function updateNextRefreshTime() {
- if (isSentOffers) return;
-
- const nextRefreshTimeSpan = document.getElementById('nextRefreshTime');
- if (!nextRefreshTimeSpan) {
- console.warn('nextRefreshTime element not found');
+function initializeFlowbiteTooltips() {
+ if (typeof Tooltip === 'undefined') {
+ console.warn('Tooltip is not defined. Make sure the required library is loaded.');
return;
}
- const minutes = Math.floor(nextRefreshCountdown / 60);
- const seconds = nextRefreshCountdown % 60;
-
- nextRefreshTimeSpan.textContent = `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
- // console.log(`Next refresh in: ${minutes}m ${seconds}s`);
+ const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
+ tooltipElements.forEach((el) => {
+ const tooltipId = el.getAttribute('data-tooltip-target');
+ const tooltipElement = document.getElementById(tooltipId);
+ if (tooltipElement) {
+ new Tooltip(tooltipElement, el);
+ }
+ });
}
-function updateRowTimes() {
- const currentTime = Math.floor(Date.now() / 1000);
- const rows = document.querySelectorAll('[data-offer-id]');
-
- rows.forEach(row => {
- const offerId = row.getAttribute('data-offer-id');
- const offer = jsonData.find(o => o.offer_id === offerId);
- if (!offer) return;
+function initializeFooter() {
+ if (isSentOffers) {
+ const nextRefreshContainer = document.getElementById('nextRefreshContainer');
+ if (nextRefreshContainer) {
+ nextRefreshContainer.style.display = 'none';
+ }
- const timeColumn = row.querySelector('td:first-child');
- if (!timeColumn) return;
-
- const postedTime = formatTimeAgo(offer.created_at);
- const expiresIn = formatTimeLeft(offer.expire_at);
- const timerColor = getTimerColor(offer);
-
- const svg = timeColumn.querySelector('svg');
- if (svg) {
- svg.querySelector('g').setAttribute('stroke', timerColor);
- svg.querySelector('polyline').setAttribute('stroke', timerColor);
+ if (typeof nextRefreshCountdown !== 'undefined') {
+ clearInterval(nextRefreshCountdown);
+ }
}
-
- const textContainer = timeColumn.querySelector('.xl\\:block');
- if (textContainer) {
- const postedElement = textContainer.querySelector('.text-xs:first-child');
- const expiresElement = textContainer.querySelector('.text-xs:last-child');
-
- if (postedElement) postedElement.textContent = `Posted: ${postedTime}`;
- if (expiresElement) expiresElement.textContent = `Expires in: ${expiresIn}`;
- }
-
- const tooltipElement = document.getElementById(`tooltip-active${offerId}`);
- if (tooltipElement) {
- const tooltipContent = tooltipElement.querySelector('.active-revoked-expired');
- if (tooltipContent) {
- const postedElement = tooltipContent.querySelector('.text-xs:first-child');
- const expiresElement = tooltipContent.querySelector('.text-xs:last-child');
-
- if (postedElement) postedElement.textContent = `Posted: ${postedTime}`;
- if (expiresElement) expiresElement.textContent = `Expires in: ${expiresIn}`;
- }
- }
- });
}
function updateCoinFilterImages() {
@@ -1656,66 +496,1074 @@ function updateCoinFilterImages() {
updateButtonImage(coinFromSelect, coinFromButton);
}
-function updateCoinFilterOptions() {
- const coinToSelect = document.getElementById('coin_to');
- const coinFromSelect = document.getElementById('coin_from');
-
- const updateOptions = (select) => {
- const options = select.options;
- for (let i = 0; i < options.length; i++) {
- const option = options[i];
- if (option.value !== 'any') {
- const displayName = symbolToCoinName[option.value] || option.value;
- option.textContent = displayName;
- }
- }
- };
+function applyFilters() {
+ console.log('Applying filters');
- updateOptions(coinToSelect);
- updateOptions(coinFromSelect);
+ setTimeout(() => {
+ jsonData = filterAndSortData();
+ updateOffersTable();
+ updateJsonView();
+ updatePaginationInfo();
+ updateClearFiltersButton();
+ console.log('Filters applied, table updated');
+ }, 100);
}
-function initializeFlowbiteTooltips() {
- if (typeof Tooltip === 'undefined') {
- console.warn('Tooltip is not defined. Make sure the required library is loaded.');
+function updateRowTimes() {
+ const rows = document.querySelectorAll('[data-offer-id]');
+ rows.forEach(row => {
+ const offerId = row.getAttribute('data-offer-id');
+ const offer = jsonData.find(o => o.offer_id === offerId);
+ if (!offer) return;
+
+ const timeColumn = row.querySelector('td:first-child');
+ if (!timeColumn) return;
+
+ const postedTime = formatTimeAgo(offer.created_at);
+ const expiresIn = formatTimeLeft(offer.expire_at);
+ const timerColor = getTimerColor(offer);
+
+ const svg = timeColumn.querySelector('svg');
+ if (svg) {
+ svg.querySelector('g').setAttribute('stroke', timerColor);
+ svg.querySelector('polyline').setAttribute('stroke', timerColor);
+ }
+
+ const textContainer = timeColumn.querySelector('.xl\\:block');
+ if (textContainer) {
+ const postedElement = textContainer.querySelector('.text-xs:first-child');
+ const expiresElement = textContainer.querySelector('.text-xs:last-child');
+
+ if (postedElement) postedElement.textContent = `Posted: ${postedTime}`;
+ if (expiresElement) expiresElement.textContent = `Expires in: ${expiresIn}`;
+ }
+ });
+}
+
+function updateJsonView() {
+ jsonContent.textContent = JSON.stringify(jsonData, null, 2);
+}
+
+function updateLastRefreshTime() {
+ if (lastRefreshTimeSpan) {
+ lastRefreshTimeSpan.textContent = lastRefreshTime ? new Date(lastRefreshTime).toLocaleTimeString() : 'Never';
+ }
+}
+
+function updateNextRefreshTime() {
+ if (isSentOffers) return;
+
+ const nextRefreshTimeSpan = document.getElementById('nextRefreshTime');
+ if (!nextRefreshTimeSpan) {
+ console.warn('nextRefreshTime element not found');
return;
}
- const tooltipElements = document.querySelectorAll('[data-tooltip-target]');
- tooltipElements.forEach((el) => {
- const tooltipId = el.getAttribute('data-tooltip-target');
- const tooltipElement = document.getElementById(tooltipId);
- if (tooltipElement) {
- new Tooltip(tooltipElement, el);
+ const minutes = Math.floor(nextRefreshCountdown / 60);
+ const seconds = nextRefreshCountdown % 60;
+ nextRefreshTimeSpan.textContent = `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
+}
+
+function updatePaginationInfo() {
+ const validOffers = getValidOffers();
+ const totalItems = validOffers.length;
+ const totalPages = Math.max(1, Math.ceil(totalItems / itemsPerPage));
+
+ currentPage = Math.max(1, Math.min(currentPage, totalPages));
+
+ currentPageSpan.textContent = currentPage;
+ totalPagesSpan.textContent = totalPages;
+
+ const showPrev = currentPage > 1;
+ const showNext = currentPage < totalPages && totalItems > 0;
+
+ prevPageButton.style.display = showPrev ? 'inline-flex' : 'none';
+ nextPageButton.style.display = showNext ? 'inline-flex' : 'none';
+
+ if (lastRefreshTime) {
+ lastRefreshTimeSpan.textContent = new Date(lastRefreshTime).toLocaleTimeString();
+ }
+
+ if (newEntriesCountSpan) {
+ newEntriesCountSpan.textContent = totalItems;
+ }
+}
+
+function updatePaginationControls(totalPages) {
+ prevPageButton.style.display = currentPage > 1 ? 'inline-flex' : 'none';
+ nextPageButton.style.display = currentPage < totalPages ? 'inline-flex' : 'none';
+ currentPageSpan.textContent = currentPage;
+ totalPagesSpan.textContent = totalPages;
+}
+
+function checkExpiredAndFetchNew() {
+ if (isSentOffers) return Promise.resolve();
+
+ console.log('Starting checkExpiredAndFetchNew');
+
+ return fetch('/json/offers')
+ .then(response => response.json())
+ .then(data => {
+ let newListings = Array.isArray(data) ? data : Object.values(data);
+
+ newListings = newListings.map(offer => ({
+ ...offer,
+ offer_id: String(offer.offer_id || ''),
+ swap_type: String(offer.swap_type || 'N/A'),
+ addr_from: String(offer.addr_from || ''),
+ coin_from: String(offer.coin_from || ''),
+ coin_to: String(offer.coin_to || ''),
+ amount_from: String(offer.amount_from || '0'),
+ amount_to: String(offer.amount_to || '0'),
+ rate: String(offer.rate || '0'),
+ created_at: Number(offer.created_at || 0),
+ expire_at: Number(offer.expire_at || 0),
+ is_own_offer: Boolean(offer.is_own_offer),
+ amount_negotiable: Boolean(offer.amount_negotiable),
+ unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}`
+ }));
+
+ newListings = newListings.filter(offer => !isOfferExpired(offer));
+ originalJsonData = newListings;
+
+ const currentFilters = new FormData(filterForm);
+ const hasActiveFilters = currentFilters.get('coin_to') !== 'any' ||
+ currentFilters.get('coin_from') !== 'any';
+
+ if (hasActiveFilters) {
+ jsonData = filterAndSortData();
+ } else {
+ jsonData = [...newListings];
+ }
+
+ updateOffersTable();
+ updateJsonView();
+ updatePaginationInfo();
+
+ if (jsonData.length === 0) {
+ handleNoOffersScenario();
+ }
+
+ nextRefreshCountdown = getTimeUntilNextExpiration();
+ console.log(`Next refresh in ${nextRefreshCountdown} seconds`);
+
+ return jsonData.length;
+ })
+ .catch(error => {
+ console.error('Error fetching new listings:', error);
+ nextRefreshCountdown = 60;
+ return Promise.reject(error);
+ });
+}
+
+function createTimeColumn(offer, postedTime, expiresIn) {
+ const timerColor = getTimerColor(offer);
+ return `
+
+
+
+
+
Posted: ${escapeHtml(postedTime)}
+
Expires in: ${escapeHtml(expiresIn)}
+
+
+
+ `;
+}
+
+function createDetailsColumn(offer) {
+ const addrFrom = offer.addr_from || '';
+ return `
+
+
+ Recipient: ${escapeHtml(addrFrom.substring(0, 10))}...
+
+
+ `;
+}
+
+function createTakerAmountColumn(offer, coinTo, coinFrom) {
+ const fromAmount = parseFloat(offer.amount_to);
+ const fromSymbol = getCoinSymbol(coinFrom);
+ return `
+
+
+
+ `;
+}
+
+function createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol) {
+ const getImageFilename = (symbol, displayName) => {
+ if (displayName.toLowerCase() === 'zcoin' || displayName.toLowerCase() === 'firo') {
+ return 'Firo.png';
+ }
+ return `${displayName.replace(' ', '-')}.png`;
+ };
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+function createOrderbookColumn(offer, coinFrom, coinTo) {
+ const toAmount = parseFloat(offer.amount_from);
+ const toSymbol = getCoinSymbol(coinTo);
+ return `
+
+
+
+ `;
+}
+
+function createRateColumn(offer, coinFrom, coinTo) {
+ const rate = parseFloat(offer.rate);
+ const inverseRate = 1 / rate;
+ const fromSymbol = getCoinSymbol(coinFrom);
+ const toSymbol = getCoinSymbol(coinTo);
+
+ const getPriceKey = (coin) => {
+ const lowerCoin = coin.toLowerCase();
+ if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
+ return 'zcoin';
+ }
+ if (lowerCoin === 'bitcoin cash') {
+ return 'bitcoin-cash';
+ }
+ return coinNameToSymbol[coin] || lowerCoin;
+ };
+
+ const fromPriceUSD = latestPrices[getPriceKey(coinFrom)]?.usd || 0;
+ const toPriceUSD = latestPrices[getPriceKey(coinTo)]?.usd || 0;
+ const rateInUSD = rate * toPriceUSD;
+
+ return `
+
+
+
+
+ $${rateInUSD.toFixed(2)} USD
+
+
+ ${rate.toFixed(8)} ${toSymbol}/${fromSymbol}
+
+
+ ${inverseRate.toFixed(8)} ${fromSymbol}/${toSymbol}
+
+
+
+
+ `;
+}
+
+function createPercentageColumn(offer) {
+ return `
+
+
+
+
+ Calculating...
+
+
+
+
+ `;
+}
+
+function createActionColumn(offer, buttonClass, buttonText) {
+ return `
+
+
+
+ `;
+}
+
+function updateProfitLoss(row, fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
+ const profitLossElement = row.querySelector('.profit-loss');
+ if (!profitLossElement) {
+ console.warn('Profit loss element not found in row');
+ return;
+ }
+
+ if (!fromCoin || !toCoin) {
+ console.error(`Invalid coin names: fromCoin=${fromCoin}, toCoin=${toCoin}`);
+ profitLossElement.textContent = 'Error';
+ profitLossElement.className = 'profit-loss text-lg font-bold text-red-500';
+ return;
+ }
+
+ calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
+ .then(percentDiff => {
+ if (percentDiff === null) {
+ profitLossElement.textContent = 'N/A';
+ profitLossElement.className = 'profit-loss text-lg font-bold text-gray-400';
+ return;
+ }
+
+ const formattedPercentDiff = percentDiff.toFixed(2);
+ const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
+ (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
+
+ const colorClass = getProfitColorClass(percentDiff);
+ profitLossElement.textContent = `${percentDiffDisplay}%`;
+ profitLossElement.className = `profit-loss text-lg font-bold ${colorClass}`;
+
+ const tooltipId = `percentage-tooltip-${row.getAttribute('data-offer-id')}`;
+ const tooltipElement = document.getElementById(tooltipId);
+ if (tooltipElement) {
+ const tooltipContent = createTooltipContent(isSentOffers || isOwnOffer, fromCoin, toCoin, fromAmount, toAmount);
+ tooltipElement.innerHTML = `
+
+ ${tooltipContent}
+
+
+ `;
+ }
+ })
+ .catch(error => {
+ console.error('Error in updateProfitLoss:', error);
+ profitLossElement.textContent = 'Error';
+ profitLossElement.className = 'profit-loss text-lg font-bold text-red-500';
+ });
+}
+
+function createTooltips(offer, treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked) {
+ const rate = parseFloat(offer.rate);
+ const fromSymbol = getCoinSymbolLowercase(coinFrom);
+ const toSymbol = getCoinSymbolLowercase(coinTo);
+ const uniqueId = `${offer.offer_id}_${offer.created_at}`;
+
+ const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
+ const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
+ const rateInUSD = rate * toPriceUSD;
+
+ const combinedRateTooltip = createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer);
+ const percentageTooltipContent = createTooltipContent(treatAsSentOffer, coinFrom, coinTo, fromAmount, toAmount);
+
+ return `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) {
+ if (!coinFrom || !coinTo) {
+ console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`);
+ return `Unable to calculate profit/loss
+ Invalid coin data.
`;
+ }
+
+ fromAmount = parseFloat(fromAmount) || 0;
+ toAmount = parseFloat(toAmount) || 0;
+
+ const getPriceKey = (coin) => {
+ const lowerCoin = coin.toLowerCase();
+ return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' : coinNameToSymbol[coin] || lowerCoin;
+ };
+
+ const fromSymbol = getPriceKey(coinFrom);
+ const toSymbol = getPriceKey(coinTo);
+ const fromPriceUSD = latestPrices[fromSymbol]?.usd;
+ const toPriceUSD = latestPrices[toSymbol]?.usd;
+
+ if (!fromPriceUSD || !toPriceUSD) {
+ return `Unable to calculate profit/loss
+ Price data is missing for one or both coins.
`;
+ }
+
+ const fromValueUSD = fromAmount * fromPriceUSD;
+ const toValueUSD = toAmount * toPriceUSD;
+ const profitUSD = toValueUSD - fromValueUSD;
+
+ const marketRate = fromPriceUSD / toPriceUSD;
+ const offerRate = toAmount / fromAmount;
+ let percentDiff;
+
+ if (isSentOffers || isOwnOffer) {
+ percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
+ } else {
+ percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
+ }
+
+ const formattedPercentDiff = percentDiff.toFixed(2);
+ const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
+ (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
+
+ const profitLabel = (isSentOffers || isOwnOffer) ? "Max Profit" : "Max Loss";
+ const actionLabel = (isSentOffers || isOwnOffer) ? "selling" : "buying";
+ const directionLabel = (isSentOffers || isOwnOffer) ? "receiving" : "paying";
+
+ return `
+ Profit/Loss Calculation:
+ You are ${actionLabel} ${fromAmount.toFixed(8)} ${coinFrom} ($${fromValueUSD.toFixed(2)} USD) and ${directionLabel} ${toAmount.toFixed(8)} ${coinTo} ($${toValueUSD.toFixed(2)} USD).
+ Percentage difference: ${percentDiffDisplay}%
+ ${profitLabel}: ${profitUSD > 0 ? '' : '-'}$${Math.abs(profitUSD).toFixed(2)} USD
+ Calculation:
+ Percentage = ${(isSentOffers || isOwnOffer) ?
+ "((To Amount in USD / From Amount in USD) - 1) * 100" :
+ "((From Amount in USD / To Amount in USD) - 1) * 100"}
+ USD ${profitLabel} = To Amount in USD - From Amount in USD
+ Interpretation:
+ ${(isSentOffers || isOwnOffer) ? `
+ Positive percentage: You're selling above market rate (profitable)
+ Negative percentage: You're selling below market rate (loss)
+ ` : `
+ Positive percentage: You're buying below market rate (savings)
+ Negative percentage: You're buying above market rate (premium)
+ `}
+ Note: ${(isSentOffers || isOwnOffer) ?
+ "As a seller, a positive percentage means you're selling for more than the current market value." :
+ "As a buyer, a positive percentage indicates potential savings compared to current market rates."}
+ Market Rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}
+ Offer Rate: 1 ${coinFrom} = ${offerRate.toFixed(8)} ${coinTo}
+ `;
+}
+
+function createCombinedRateTooltip(offer, coinFrom, coinTo, isSentOffers, treatAsSentOffer) {
+ const rate = parseFloat(offer.rate);
+ const inverseRate = 1 / rate;
+
+ const getPriceKey = (coin) => {
+ const lowerCoin = coin.toLowerCase();
+ if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
+ return 'zcoin';
+ }
+ if (lowerCoin === 'bitcoin cash') {
+ return 'bitcoin-cash';
+ }
+ return coinNameToSymbol[coin] || lowerCoin;
+ };
+
+ const fromSymbol = getPriceKey(coinFrom);
+ const toSymbol = getPriceKey(coinTo);
+
+ const fromPriceUSD = latestPrices[fromSymbol]?.usd || 0;
+ const toPriceUSD = latestPrices[toSymbol]?.usd || 0;
+ const rateInUSD = rate * toPriceUSD;
+
+ const marketRate = fromPriceUSD / toPriceUSD;
+
+ const percentDiff = ((rate - marketRate) / marketRate) * 100;
+ const formattedPercentDiff = percentDiff.toFixed(2);
+ const percentDiffDisplay = formattedPercentDiff === "0.00" ? "0.00" :
+ (percentDiff > 0 ? `+${formattedPercentDiff}` : formattedPercentDiff);
+ const aboveOrBelow = percentDiff > 0 ? "above" : percentDiff < 0 ? "below" : "at";
+
+ const action = isSentOffers || treatAsSentOffer ? "selling" : "buying";
+
+ return `
+ Exchange Rate Explanation:
+ This offer is ${action} ${coinFrom} for ${coinTo} at a rate that is ${percentDiffDisplay}% ${aboveOrBelow} market price.
+ Exchange Rates:
+ 1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}
+ 1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}
+ USD Equivalent:
+ 1 ${coinFrom} = $${rateInUSD.toFixed(2)} USD
+ Current market prices:
+ ${coinFrom}: $${fromPriceUSD.toFixed(2)} USD
+ ${coinTo}: $${toPriceUSD.toFixed(2)} USD
+ Market rate: 1 ${coinFrom} = ${marketRate.toFixed(8)} ${coinTo}
+ `;
+}
+
+function updateTooltipTargets(row, uniqueId) {
+ const tooltipElements = [
+ { prefix: 'tooltip-active', selector: '[data-tooltip-target^="tooltip-active"]' },
+ { prefix: 'tooltip-recipient', selector: '[data-tooltip-target^="tooltip-recipient"]' },
+ { prefix: 'tooltip-wallet', selector: '[data-tooltip-target^="tooltip-wallet"]' },
+ { prefix: 'tooltip-offer', selector: '[data-tooltip-target^="tooltip-offer"]' },
+ { prefix: 'tooltip-wallet-maker', selector: '[data-tooltip-target^="tooltip-wallet-maker"]' },
+ { prefix: 'tooltip-rate', selector: '[data-tooltip-target^="tooltip-rate"]' },
+ { prefix: 'percentage-tooltip', selector: '[data-tooltip-target^="percentage-tooltip"]' }
+ ];
+
+ tooltipElements.forEach(({ prefix, selector }) => {
+ const element = row.querySelector(selector);
+ if (element) {
+ element.setAttribute('data-tooltip-target', `${prefix}-${uniqueId}`);
}
});
}
+function clearFilters() {
+ filterForm.reset();
+ const statusSelect = document.getElementById('status');
+ if (statusSelect) {
+ statusSelect.value = 'any';
+ }
+ jsonData = [...originalJsonData];
+ currentPage = 1;
+ updateOffersTable();
+ updateJsonView();
+ updateCoinFilterImages();
+ updateClearFiltersButton();
+}
+
+async function fetchLatestPrices() {
+ const MAX_RETRIES = 3;
+ const BASE_DELAY = 1000;
+
+ const cachedData = getCachedPrices();
+ if (cachedData) {
+ latestPrices = cachedData;
+ return cachedData;
+ }
+
+ const url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC';
+
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
+ try {
+ const data = await makePostRequest(url);
+ if (data && Object.keys(data).length > 0) {
+ latestPrices = data;
+ setCachedPrices(data);
+ // Store as fallback values with 24h expiry
+ Object.entries(data).forEach(([coin, prices]) => {
+ tableRateModule.setFallbackValue(coin, prices.usd);
+ });
+ return data;
+ }
+ } catch (error) {
+ const delay = Math.min(BASE_DELAY * Math.pow(2, attempt), 10000);
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+
+ return getFallbackPrices();
+}
+
+function getFallbackPrices() {
+ const fallbacks = {};
+ const coins = ['bitcoin', 'bitcoin-cash', 'dash', 'dogecoin', 'decred', 'litecoin',
+ 'particl', 'pivx', 'monero', 'zano', 'wownero', 'zcoin'];
+
+ for (const coin of coins) {
+ const fallbackValue = tableRateModule.getFallbackValue(coin);
+ if (fallbackValue) {
+ fallbacks[coin] = { usd: fallbackValue };
+ }
+ }
+
+ return Object.keys(fallbacks).length > 0 ? fallbacks : null;
+}
+
+async function fetchOffers(manualRefresh = false) {
+ return new Promise((resolve, reject) => {
+ const endpoint = isSentOffers ? '/json/sentoffers' : '/json/offers';
+ console.log(`[Debug] Fetching from endpoint: ${endpoint}`);
+
+ fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ with_extra_info: true,
+ limit: 1000
+ })
+ })
+ .then(response => response.json())
+ .then(data => {
+ console.log('[Debug] Raw data received:', data);
+
+ let newData = Array.isArray(data) ? data : Object.values(data);
+
+ newData = newData.map(offer => ({
+ ...offer,
+ offer_id: String(offer.offer_id || ''),
+ swap_type: String(offer.swap_type || 'N/A'),
+ addr_from: String(offer.addr_from || ''),
+ coin_from: String(offer.coin_from || ''),
+ coin_to: String(offer.coin_to || ''),
+ amount_from: String(offer.amount_from || '0'),
+ amount_to: String(offer.amount_to || '0'),
+ rate: String(offer.rate || '0'),
+ created_at: Number(offer.created_at || 0),
+ expire_at: Number(offer.expire_at || 0),
+ is_own_offer: Boolean(offer.is_own_offer),
+ amount_negotiable: Boolean(offer.amount_negotiable),
+ is_revoked: Boolean(offer.is_revoked),
+ unique_id: `${offer.offer_id}_${offer.created_at}_${offer.coin_from}_${offer.coin_to}`
+ }));
+
+ if (isInitialLoad || manualRefresh) {
+ jsonData = newData;
+ originalJsonData = [...newData];
+ } else {
+ const mergedData = mergeSentOffers(jsonData, newData);
+ jsonData = mergedData;
+ originalJsonData = [...mergedData];
+ }
+
+ if (newEntriesCountSpan) {
+ newEntriesCountSpan.textContent = jsonData.length;
+ }
+
+ updateOffersTable();
+ updateJsonView();
+ updatePaginationInfo();
+
+ resolve();
+ })
+ .catch(error => {
+ console.error('[Debug] Error fetching offers:', error);
+ reject(error);
+ });
+ });
+}
+
+function mergeSentOffers(existingOffers, newOffers) {
+ console.log('[Debug] Merging offers:', {
+ existing: existingOffers.length,
+ new: newOffers.length
+ });
+
+ const offerMap = new Map();
+ existingOffers.forEach(offer => {
+ offerMap.set(offer.offer_id, offer);
+ });
+
+ newOffers.forEach(offer => {
+ offerMap.set(offer.offer_id, offer);
+ });
+
+ const mergedOffers = Array.from(offerMap.values());
+ console.log('[Debug] After merge:', mergedOffers.length);
+
+ return mergedOffers;
+}
+
+function getValidOffers() {
+ if (!jsonData) {
+ console.warn('jsonData is undefined or null');
+ return [];
+ }
+
+ const filteredData = filterAndSortData();
+ console.log(`getValidOffers: Found ${filteredData.length} valid offers`);
+ return filteredData;
+}
+
+function filterAndSortData() {
+ console.log('[Debug] Starting filter with data length:', originalJsonData.length);
+
+ const formData = new FormData(filterForm);
+ const filters = Object.fromEntries(formData);
+ console.log('[Debug] Active filters:', filters);
+
+ if (filters.coin_to !== 'any') {
+ filters.coin_to = coinIdToName[filters.coin_to] || filters.coin_to;
+ }
+ if (filters.coin_from !== 'any') {
+ filters.coin_from = coinIdToName[filters.coin_from] || filters.coin_from;
+ }
+
+ let filteredData = [...originalJsonData];
+
+ filteredData = filteredData.filter(offer => {
+ if (!isSentOffers && isOfferExpired(offer)) {
+ return false;
+ }
+
+ const coinFrom = (offer.coin_from || '').toLowerCase();
+ const coinTo = (offer.coin_to || '').toLowerCase();
+
+ if (filters.coin_to !== 'any') {
+ if (!coinMatches(coinTo, filters.coin_to)) {
+ return false;
+ }
+ }
+
+ if (filters.coin_from !== 'any') {
+ if (!coinMatches(coinFrom, filters.coin_from)) {
+ return false;
+ }
+ }
+
+ if (isSentOffers && filters.status && filters.status !== 'any') {
+ const currentTime = Math.floor(Date.now() / 1000);
+ const isExpired = offer.expire_at <= currentTime;
+ const isRevoked = Boolean(offer.is_revoked);
+
+ switch (filters.status) {
+ case 'active':
+ return !isExpired && !isRevoked;
+ case 'expired':
+ return isExpired && !isRevoked;
+ case 'revoked':
+ return isRevoked;
+ default:
+ return true;
+ }
+ }
+
+ return true;
+ });
+
+ return filteredData;
+}
+
+async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
+ console.log(`Calculating profit/loss for ${fromAmount} ${fromCoin} to ${toAmount} ${toCoin}, isOwnOffer: ${isOwnOffer}`);
+
+ if (!latestPrices) {
+ console.error('Latest prices not available. Unable to calculate profit/loss.');
+ return null;
+ }
+
+ const getPriceKey = (coin) => {
+ const lowerCoin = coin.toLowerCase();
+ if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
+ return 'zcoin';
+ }
+ if (lowerCoin === 'bitcoin cash') {
+ return 'bitcoin-cash';
+ }
+ return coinNameToSymbol[coin] || lowerCoin;
+ };
+
+ const fromSymbol = getPriceKey(fromCoin);
+ const toSymbol = getPriceKey(toCoin);
+
+ const fromPriceUSD = latestPrices[fromSymbol]?.usd;
+ const toPriceUSD = latestPrices[toSymbol]?.usd;
+
+ if (!fromPriceUSD || !toPriceUSD) {
+ console.error(`Price data missing for ${fromSymbol} or ${toSymbol}`);
+ return null;
+ }
+
+ const fromValueUSD = fromAmount * fromPriceUSD;
+ const toValueUSD = toAmount * toPriceUSD;
+
+ let percentDiff;
+ if (isOwnOffer) {
+ percentDiff = ((toValueUSD / fromValueUSD) - 1) * 100;
+ } else {
+ percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
+ }
+
+ console.log(`Percent difference: ${percentDiff.toFixed(2)}%`);
+ return percentDiff;
+}
+
+async function getMarketRate(fromCoin, toCoin) {
+ return new Promise((resolve) => {
+ console.log(`Attempting to get market rate for ${fromCoin} to ${toCoin}`);
+ if (!latestPrices) {
+ console.warn('Latest prices object is not available');
+ resolve(null);
+ return;
+ }
+
+ const getPriceKey = (coin) => {
+ const lowerCoin = coin.toLowerCase();
+ if (lowerCoin === 'firo' || lowerCoin === 'zcoin') {
+ return 'zcoin';
+ }
+ if (lowerCoin === 'bitcoin cash') {
+ return 'bitcoin-cash';
+ }
+ return coinNameToSymbol[coin] || lowerCoin;
+ };
+
+ const fromSymbol = getPriceKey(fromCoin);
+ const toSymbol = getPriceKey(toCoin);
+
+ const fromPrice = latestPrices[fromSymbol]?.usd;
+ const toPrice = latestPrices[toSymbol]?.usd;
+ if (!fromPrice || !toPrice) {
+ console.warn(`Missing price data for ${!fromPrice ? fromCoin : toCoin}`);
+ resolve(null);
+ return;
+ }
+ const rate = toPrice / fromPrice;
+ console.log(`Market rate calculated: ${rate} ${toCoin}/${fromCoin}`);
+ resolve(rate);
+ });
+}
+
+function handleNoOffersScenario() {
+ const formData = new FormData(filterForm);
+ const filters = Object.fromEntries(formData);
+ const hasActiveFilters = filters.coin_to !== 'any' ||
+ filters.coin_from !== 'any' ||
+ (filters.status && filters.status !== 'any');
+
+ if (hasActiveFilters) {
+ offersBody.innerHTML = `
+
+
+ No offers match the selected filters. Try different filter options or
+ clear filters
+
+ `;
+ } else {
+ offersBody.innerHTML = `
+
+
+ No active offers available. ${!isSentOffers ? 'Refreshing data...' : ''}
+
+ `;
+ if (!isSentOffers) {
+ setTimeout(() => fetchOffers(true), 2000);
+ }
+ }
+}
+
+function createTableRow(offer, isSentOffers) {
+ 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.setAttribute('data-offer-id', uniqueId);
+
+ const coinFrom = offer.coin_from;
+ const coinTo = offer.coin_to;
+ const coinFromSymbol = coinNameToSymbol[coinFrom] || coinFrom.toLowerCase();
+ const coinToSymbol = coinNameToSymbol[coinTo] || coinTo.toLowerCase();
+ const coinFromDisplay = getDisplayName(coinFrom);
+ const coinToDisplay = getDisplayName(coinTo);
+
+ const postedTime = formatTimeAgo(offer.created_at);
+ const expiresIn = formatTimeLeft(offer.expire_at);
+
+ const currentTime = Math.floor(Date.now() / 1000);
+ const isActuallyExpired = currentTime > offer.expire_at;
+
+ const isOwnOffer = offer.is_own_offer;
+ const isRevoked = Boolean(offer.is_revoked);
+
+ const { buttonClass, buttonText } = getButtonProperties(isActuallyExpired, isSentOffers, isOwnOffer, isRevoked);
+
+ const fromAmount = parseFloat(offer.amount_from) || 0;
+ const toAmount = parseFloat(offer.amount_to) || 0;
+
+ row.innerHTML = `
+ ${createTimeColumn(offer, postedTime, expiresIn)}
+ ${createDetailsColumn(offer)}
+ ${createTakerAmountColumn(offer, coinTo, coinFrom)}
+ ${createSwapColumn(offer, coinFromDisplay, coinToDisplay, coinFromSymbol, coinToSymbol)}
+ ${createOrderbookColumn(offer, coinFrom, coinTo)}
+ ${createRateColumn(offer, coinFrom, coinTo)}
+ ${createPercentageColumn(offer)}
+ ${createActionColumn(offer, buttonClass, buttonText)}
+ ${createTooltips(offer, isOwnOffer, coinFrom, coinTo, fromAmount, toAmount, postedTime, expiresIn, isActuallyExpired, isRevoked)}
+ `;
+
+ updateTooltipTargets(row, uniqueId);
+ updateProfitLoss(row, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer);
+
+ return row;
+}
+
+async function updateOffersTable(isPaginationChange = false) {
+ console.log('[Debug] Starting updateOffersTable function');
+
+ try {
+ if (!isPaginationChange) {
+ const priceData = await fetchLatestPrices();
+ if (!priceData) {
+ console.error('Failed to fetch latest prices');
+ } else {
+ console.log('Latest prices fetched successfully');
+ latestPrices = priceData;
+ }
+ }
+
+ let validOffers = getValidOffers();
+ console.log('[Debug] Valid offers:', validOffers.length);
+
+ if (validOffers.length === 0) {
+ handleNoOffersScenario();
+ return;
+ }
+
+ 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);
+
+ offersBody.innerHTML = '';
+
+ for (const offer of itemsToDisplay) {
+ const row = createTableRow(offer, isSentOffers);
+ if (row) {
+ offersBody.appendChild(row);
+ }
+ }
+
+ updateRowTimes();
+ initializeFlowbiteTooltips();
+ updatePaginationControls(totalPages);
+
+ if (tableRateModule?.initializeTable) {
+ tableRateModule.initializeTable();
+ }
+
+ if (!isPaginationChange) {
+ lastRefreshTime = Date.now();
+ if (newEntriesCountSpan) {
+ const displayCount = isSentOffers ? jsonData.length : validOffers.length;
+ newEntriesCountSpan.textContent = displayCount;
+ }
+ if (!isSentOffers) {
+ nextRefreshCountdown = getTimeUntilNextExpiration();
+ updateLastRefreshTime();
+ updateNextRefreshTime();
+ }
+ }
+
+ } catch (error) {
+ console.error('[Debug] Error in updateOffersTable:', error);
+ offersBody.innerHTML = `An error occurred while updating the offers table. Please try again later. `;
+ } finally {
+ setRefreshButtonLoading(false);
+ }
+}
+
function startRefreshCountdown() {
if (isSentOffers) return;
- console.log('Starting refresh countdown');
-
let refreshTimeout;
let countdownInterval;
function refreshCycle() {
- console.log(`Refresh cycle started. Current countdown: ${nextRefreshCountdown}`);
- checkExpiredAndFetchNew().then(() => {
- console.log(`Refresh cycle completed. Next refresh in ${nextRefreshCountdown} seconds`);
- startCountdown();
- });
+ checkExpiredAndFetchNew()
+ .then(() => {
+ startCountdown();
+ })
+ .catch(error => {
+ console.error('Error in refresh cycle:', error);
+ nextRefreshCountdown = 60;
+ startCountdown();
+ });
}
function startCountdown() {
- if (countdownInterval) {
- clearInterval(countdownInterval);
- }
- if (refreshTimeout) {
- clearTimeout(refreshTimeout);
- }
+ if (countdownInterval) clearInterval(countdownInterval);
+ if (refreshTimeout) clearTimeout(refreshTimeout);
- nextRefreshCountdown = getTimeUntilNextExpiration();
updateNextRefreshTime();
countdownInterval = setInterval(() => {
@@ -1739,118 +1587,6 @@ function startRefreshCountdown() {
});
}
-function checkExpiredAndFetchNew() {
- if (isSentOffers) return Promise.resolve();
- console.log('Starting checkExpiredAndFetchNew');
- const currentTime = Math.floor(Date.now() / 1000);
- const expiredOffers = jsonData.filter(offer => offer.expire_at <= currentTime);
-
- console.log(`Checking for expired offers. Current time: ${currentTime}`);
- console.log(`Found ${expiredOffers.length} expired offers.`);
-
- jsonData = jsonData.filter(offer => offer.expire_at > currentTime);
-
- console.log('Fetching new offers...');
-
- return fetch('/json/offers')
- .then(response => response.json())
- .then(data => {
- let newListings = Array.isArray(data) ? data : Object.values(data);
- newListings = newListings.filter(offer => !isOfferExpired(offer));
-
- const brandNewListings = newListings.filter(newOffer =>
- !jsonData.some(existingOffer => existingOffer.offer_id === newOffer.offer_id)
- );
-
- console.log(`Found ${brandNewListings.length} new listings to add.`);
- jsonData = [...jsonData, ...brandNewListings];
- newEntriesCount += brandNewListings.length;
-
- updateOffersTable();
- updateJsonView();
- updatePaginationInfo();
-
- if (jsonData.length === 0) {
- console.log('No active offers. Displaying message to user.');
- handleNoOffersScenario();
- }
-
- const timeUntilNextExpiration = getTimeUntilNextExpiration();
- nextRefreshCountdown = Math.max(MIN_REFRESH_INTERVAL, Math.min(timeUntilNextExpiration, 300)); // Between 30 seconds and 5 minutes
- console.log(`Next check scheduled in ${nextRefreshCountdown} seconds`);
- })
- .catch(error => {
- console.error('Error fetching new listings:', error);
- nextRefreshCountdown = 60;
- console.log(`Error occurred. Next check scheduled in ${nextRefreshCountdown} seconds`);
- });
-}
-
-// Event
-toggleButton.addEventListener('click', () => {
- tableView.classList.toggle('hidden');
- jsonView.classList.toggle('hidden');
- toggleButton.textContent = tableView.classList.contains('hidden') ? 'Show Table View' : 'Show JSON View';
-});
-
-filterForm.addEventListener('submit', (e) => {
- e.preventDefault();
- applyFilters();
-});
-
-filterForm.addEventListener('change', applyFilters);
-
-document.getElementById('coin_to').addEventListener('change', (event) => {
- console.log('Coin To filter changed:', event.target.value);
- applyFilters();
-});
-
-document.getElementById('coin_from').addEventListener('change', (event) => {
- console.log('Coin From filter changed:', event.target.value);
- applyFilters();
-});
-
-prevPageButton.addEventListener('click', () => {
- if (currentPage > 1) {
- currentPage--;
- updateOffersTable();
- updatePaginationInfo();
- }
-});
-
-nextPageButton.addEventListener('click', () => {
- const validOffers = getValidOffers();
- const totalPages = Math.ceil(validOffers.length / itemsPerPage);
- if (currentPage < totalPages && validOffers.length > 0) {
- currentPage++;
- updateOffersTable();
- updatePaginationInfo();
- console.log(`Moved to page ${currentPage} of ${totalPages}`);
- } else {
- console.log('No more pages or all offers have expired');
- nextPageButton.classList.add('invisible');
- nextPageButton.style.display = 'none';
- if (validOffers.length === 0) {
- handleNoOffersScenario();
- }
- }
-});
-
-document.getElementById('clearFilters').addEventListener('click', () => {
- filterForm.reset();
- jsonData = [...originalJsonData];
- currentPage = 1;
- updateOffersTable();
- updateJsonView();
- updateCoinFilterImages();
-});
-
-document.getElementById('refreshOffers').addEventListener('click', () => {
- console.log('Refresh button clicked');
- fetchOffers(true);
-});
-
-// Init
function initializeTableRateModule() {
if (typeof window.tableRateModule !== 'undefined') {
tableRateModule = window.tableRateModule;
@@ -1865,15 +1601,20 @@ function initializeTableRateModule() {
function continueInitialization() {
if (typeof volumeToggle !== 'undefined' && volumeToggle.init) {
volumeToggle.init();
- } else {
- console.warn('volumeToggle is not defined or does not have an init method');
}
updateCoinFilterImages();
fetchOffers().then(() => {
applyFilters();
- startRefreshCountdown();
+ if (!isSentOffers) {
+ startRefreshCountdown();
+ }
});
+
+ const listingLabel = document.querySelector('span[data-listing-label]');
+ if (listingLabel) {
+ listingLabel.textContent = isSentOffers ? 'Total Listings: ' : 'Network Listings: ';
+ }
function updateTimesLoop() {
updateRowTimes();
@@ -1882,13 +1623,15 @@ function continueInitialization() {
requestAnimationFrame(updateTimesLoop);
setInterval(updateRowTimes, 900000);
-
console.log('Initialization completed');
}
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM content loaded, initializing...');
+ initializeFooter();
+ updateClearFiltersButton();
+
if (initializeTableRateModule()) {
continueInitialization();
} else {
@@ -1900,7 +1643,7 @@ document.addEventListener('DOMContentLoaded', () => {
clearInterval(retryInterval);
continueInitialization();
} else if (retryCount >= maxRetries) {
- console.error('Failed to load tableRateModule after multiple attempts. Some functionality may be limited.');
+ console.error('Failed to load tableRateModule after multiple attempts');
clearInterval(retryInterval);
continueInitialization();
}
@@ -1914,16 +1657,54 @@ document.addEventListener('DOMContentLoaded', () => {
filterForm.addEventListener('change', applyFilters);
- document.getElementById('coin_to').addEventListener('change', applyFilters);
- document.getElementById('coin_from').addEventListener('change', applyFilters);
+ const coinToSelect = document.getElementById('coin_to');
+ const coinFromSelect = document.getElementById('coin_from');
+
+ coinToSelect.addEventListener('change', applyFilters);
+ coinFromSelect.addEventListener('change', applyFilters);
document.getElementById('clearFilters').addEventListener('click', () => {
filterForm.reset();
+ const statusSelect = document.getElementById('status');
+ if (statusSelect) {
+ statusSelect.value = 'any';
+ }
jsonData = [...originalJsonData];
currentPage = 1;
applyFilters();
updateCoinFilterImages();
});
+
+ document.getElementById('refreshOffers').addEventListener('click', () => {
+ console.log('Refresh button clicked');
+ fetchOffers(true);
+ });
+
+ toggleButton.addEventListener('click', () => {
+ tableView.classList.toggle('hidden');
+ jsonView.classList.toggle('hidden');
+ toggleButton.textContent = tableView.classList.contains('hidden') ? 'Show Table View' : 'Show JSON View';
+ });
+
+ prevPageButton.addEventListener('click', () => {
+ if (currentPage > 1) {
+ currentPage--;
+ const validOffers = getValidOffers();
+ const totalPages = Math.ceil(validOffers.length / itemsPerPage);
+ updateOffersTable(true);
+ updatePaginationControls(totalPages);
+ }
+ });
+
+ nextPageButton.addEventListener('click', () => {
+ const validOffers = getValidOffers();
+ const totalPages = Math.ceil(validOffers.length / itemsPerPage);
+ if (currentPage < totalPages) {
+ currentPage++;
+ updateOffersTable(true);
+ updatePaginationControls(totalPages);
+ }
+ });
});
console.log('Offers Table Module fully initialized');
diff --git a/basicswap/templates/offers.html b/basicswap/templates/offers.html
index f78ca78..d8d957d 100644
--- a/basicswap/templates/offers.html
+++ b/basicswap/templates/offers.html
@@ -266,11 +266,26 @@ function getAPIKeys() {
+ {% if sent_offers %}
+
+
+
+ {{ input_arrow_down_svg | safe }}
+
+ Filter Status
+ Active
+ Expired
+ Revoked
+
+
+
+
+ {% endif %}
-
+
Clear Filters
@@ -384,17 +399,12 @@ function getAPIKeys() {
-
-
Last refreshed:
- Never
-
-
Network Listings:
-
-
-
Next refresh:
-
-
-
+
+
Last refreshed: Never
+
Network Listings:
+
Next refresh:
+
+
{{ page_back_svg | safe }}