From 4d928dc98e0c0b164965191a6b0ed747413ee98f Mon Sep 17 00:00:00 2001
From: gerlofvanek <gerlof@particl.io>
Date: Thu, 16 Jan 2025 11:38:02 +0100
Subject: [PATCH] Better error handling API / Tooltips: Rate, Market.

---
 basicswap/static/js/offerstable.js | 218 ++++++++++++++++++-----------
 1 file changed, 137 insertions(+), 81 deletions(-)

diff --git a/basicswap/static/js/offerstable.js b/basicswap/static/js/offerstable.js
index 2c15af5..ffbc532 100644
--- a/basicswap/static/js/offerstable.js
+++ b/basicswap/static/js/offerstable.js
@@ -1081,10 +1081,8 @@ function filterAndSortData() {
     return filteredData;
 }
 
-function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
+async function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer) {
     return new Promise((resolve) => {
-        // 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.');
             resolve(null);
@@ -1111,8 +1109,8 @@ function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
         const fromPriceUSD = latestPrices[fromSymbol]?.usd;
         const toPriceUSD = latestPrices[toSymbol]?.usd;
 
-        if (!fromPriceUSD || !toPriceUSD) {
-            //console.warn(`Price data missing for ${fromSymbol} (${fromPriceUSD}) or ${toSymbol} (${toPriceUSD})`);
+        if (fromPriceUSD === null || toPriceUSD === null || 
+            fromPriceUSD === undefined || toPriceUSD === undefined) {
             resolve(null);
             return;
         }
@@ -1127,7 +1125,6 @@ function calculateProfitLoss(fromCoin, toCoin, fromAmount, toAmount, isOwnOffer)
             percentDiff = ((fromValueUSD / toValueUSD) - 1) * 100;
         }
 
-        // console.log(`Percent difference: ${percentDiff.toFixed(2)}%`);
         resolve(percentDiff);
     });
 }
@@ -1168,9 +1165,86 @@ async function getMarketRate(fromCoin, toCoin) {
     });
 }
 
+async function fetchPricesWithApiKey(baseUrl) {
+    try {
+        console.log('Attempting price fetch with API key...');
+        const apiKeys = getAPIKeys();
+        const urlWithKey = `${baseUrl}&api_key=${offersConfig.apiKeys.coinGecko}`;
+        
+        const response = await fetch('/json/readurl', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                url: urlWithKey,
+                headers: {}
+            })
+        });
+
+        if (!response.ok) {
+            return null;
+        }
+
+        const data = await response.json();
+        if (data && !data.Error && Object.keys(data).length > 0) {
+            return data;
+        }
+        return null;
+    } catch (error) {
+        console.warn('Failed to fetch prices with API key:', error);
+        return null;
+    }
+}
+
+async function fetchPricesWithoutApiKey(baseUrl) {
+    try {
+        console.log('Attempting price fetch without API key...');
+        const response = await fetch('/json/readurl', {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                url: baseUrl,
+                headers: {}
+            })
+        });
+
+        if (!response.ok) {
+            return null;
+        }
+
+        const data = await response.json();
+        if (data && !data.Error && Object.keys(data).length > 0) {
+            return data;
+        }
+        return null;
+    } catch (error) {
+        console.warn('Failed to fetch prices without API key:', error);
+        return null;
+    }
+}
+
+function getEmptyPriceData() {
+    return {
+        'bitcoin': { usd: null, btc: null },
+        'bitcoin-cash': { usd: null, btc: null },
+        'dash': { usd: null, btc: null },
+        'dogecoin': { usd: null, btc: null },
+        'decred': { usd: null, btc: null },
+        'litecoin': { usd: null, btc: null },
+        'particl': { usd: null, btc: null },
+        'pivx': { usd: null, btc: null },
+        'monero': { usd: null, btc: null },
+        'zano': { usd: null, btc: null },
+        'wownero': { usd: null, btc: null },
+        'zcoin': { usd: null, btc: null }
+    };
+}
+
 async function fetchLatestPrices() {
     const PRICES_CACHE_KEY = 'prices_coingecko';
-    const apiKeys = getAPIKeys();
 
     const cachedData = CacheManager.get(PRICES_CACHE_KEY);
     if (cachedData && cachedData.remainingTime > 60000) {
@@ -1179,57 +1253,31 @@ async function fetchLatestPrices() {
         return cachedData.value;
     }
 
-    const url = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC&api_key=${offersConfig.apiKeys.coinGecko}`;
+    const baseUrl = `${offersConfig.apiEndpoints.coinGecko}/simple/price?ids=bitcoin,bitcoin-cash,dash,dogecoin,decred,litecoin,particl,pivx,monero,zano,wownero,zcoin&vs_currencies=USD,BTC`;
+    
+    let data = await fetchPricesWithApiKey(baseUrl);
 
-    try {
-        console.log('Initiating fresh price data fetch...');
-        const response = await fetch('/json/readurl', {
-            method: 'POST',
-            headers: {
-                'Content-Type': 'application/json'
-            },
-            body: JSON.stringify({
-                url: url,
-                headers: {}
-            })
-        });
-
-        if (!response.ok) {
-            throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
-        }
-
-        const data = await response.json();
-
-        if (data.Error) {
-            console.error('API Error:', data.Error);
-            throw new Error(data.Error);
-        }
-
-        if (data && Object.keys(data).length > 0) {
-            console.log('Processing fresh price data...');
-            latestPrices = data;
-            CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
-            const fallbackLog = {};
-            Object.entries(data).forEach(([coin, prices]) => {
-                tableRateModule.setFallbackValue(coin, prices.usd);
-                fallbackLog[coin] = prices.usd;
-            });
-            
-            //console.log('Fallback Values Set:', fallbackLog);
-            
-            return data;
-        } else {
-            console.warn('No price data received');
-            return null;
-        }
-    } catch (error) {
-        console.error('Price Fetch Error:', {
-            message: error.message,
-            name: error.name,
-            stack: error.stack
-        });
-        throw error;
+    if (!data) {
+        data = await fetchPricesWithoutApiKey(baseUrl);
     }
+
+    if (data) {
+        console.log('Processing fresh price data...');
+        latestPrices = data;
+        CacheManager.set(PRICES_CACHE_KEY, data, CACHE_DURATION);
+        
+        // Set fallback values
+        Object.entries(data).forEach(([coin, prices]) => {
+            tableRateModule.setFallbackValue(coin, prices.usd);
+        });
+        
+        return data;
+    }
+
+    console.warn('All price fetch attempts failed, using N/A values');
+    const naData = getEmptyPriceData();
+    latestPrices = naData;
+    return naData;
 }
 
 async function fetchOffers(manualRefresh = false) {
@@ -2189,7 +2237,6 @@ function createRecipientTooltip(uniqueId, identityInfo, identity, successRate, t
 
 function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmount, isOwnOffer) {
     if (!coinFrom || !coinTo) {
-        //console.error(`Invalid coin names: coinFrom=${coinFrom}, coinTo=${coinTo}`);
         return `<p class="font-bold mb-1">Unable to calculate profit/loss</p>
                 <p>Invalid coin data.</p>`;
     }
@@ -2199,7 +2246,10 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
 
     const getPriceKey = (coin) => {
         const lowerCoin = coin.toLowerCase();
-        return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' : coinNameToSymbol[coin] || lowerCoin;
+        return lowerCoin === 'firo' || lowerCoin === 'zcoin' ? 'zcoin' : 
+               lowerCoin === 'bitcoin cash' ? 'bitcoin-cash' : 
+               lowerCoin === 'particl anon' || lowerCoin === 'particl blind' ? 'particl' :
+               coinNameToSymbol[coin] || lowerCoin;
     };
 
     const fromSymbol = getPriceKey(coinFrom);
@@ -2207,9 +2257,14 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
     const fromPriceUSD = latestPrices[fromSymbol]?.usd;
     const toPriceUSD = latestPrices[toSymbol]?.usd;
 
-    if (!fromPriceUSD || !toPriceUSD) {
-        return `<p class="font-bold mb-1">Unable to calculate profit/loss</p>
-                <p>Price data is missing for one or both coins.</p>`;
+    if (fromPriceUSD === null || toPriceUSD === null || 
+        fromPriceUSD === undefined || toPriceUSD === undefined) {
+        return `<p class="font-bold mb-1">Price Information Unavailable</p>
+                <p>Current market prices are temporarily unavailable.</p>
+                <p class="mt-2">You are ${isSentOffers ? 'selling' : 'buying'} ${fromAmount.toFixed(8)} ${coinFrom} 
+                for ${toAmount.toFixed(8)} ${coinTo}.</p>
+                <p class="font-bold mt-2">Note:</p>
+                <p>Profit/loss calculations will be available when price data is restored.</p>`;
     }
 
     const fromValueUSD = fromAmount * fromPriceUSD;
@@ -2260,37 +2315,38 @@ function createTooltipContent(isSentOffers, coinFrom, coinTo, fromAmount, toAmou
     `;
 }
 
-function createCombinedRateTooltip(offer, coinFrom, coinTo, isSentOffers, treatAsSentOffer) {
-    const rate = parseFloat(offer.rate);
-    const inverseRate = 1 / rate;
+function createCombinedRateTooltip(offer, coinFrom, coinTo, treatAsSentOffer) {
+    const rate = parseFloat(offer.rate) || 0;
+    const inverseRate = rate ? (1 / rate) : 0;
+    const fromSymbol = getCoinSymbolLowercase(coinFrom);
+    const toSymbol = getCoinSymbolLowercase(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[fromSymbol]?.usd;
+    const toPriceUSD = latestPrices[toSymbol]?.usd;
 
-    const fromSymbol = getPriceKey(coinFrom);
-    const toSymbol = getPriceKey(coinTo);
+    if (fromPriceUSD === null || toPriceUSD === null || 
+        fromPriceUSD === undefined || toPriceUSD === undefined) {
+        return `
+            <p class="font-bold mb-1">Exchange Rate Information</p>
+            <p>Market price data is temporarily unavailable.</p>
+            <p class="font-bold mt-2">Current Offer Rates:</p>
+            <p>1 ${coinFrom} = ${rate.toFixed(8)} ${coinTo}</p>
+            <p>1 ${coinTo} = ${inverseRate.toFixed(8)} ${coinFrom}</p>
+            <p class="font-bold mt-2">Note:</p>
+            <p>Market comparison will be available when price data is restored.</p>
+        `;
+    }
 
-    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 percentDiff = marketRate ? ((rate - marketRate) / marketRate) * 100 : 0;
     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";
+    const action = treatAsSentOffer ? "selling" : "buying";
 
     return `
         <p class="font-bold mb-1">Exchange Rate Explanation:</p>