diff --git a/frontend/public/explorer-spa.js b/frontend/public/explorer-spa.js index bb7f8ca..6d30a26 100644 --- a/frontend/public/explorer-spa.js +++ b/frontend/public/explorer-spa.js @@ -4,6 +4,19 @@ const FETCH_MAX_RETRIES = 3; const RETRY_DELAY_MS = 1000; window.DEBUG_EXPLORER = false; + let _apiUnavailableBannerShown = false; + function showAPIUnavailableBanner(status) { + if (_apiUnavailableBannerShown) return; + _apiUnavailableBannerShown = true; + var main = document.getElementById('mainContent'); + if (!main) return; + var banner = document.createElement('div'); + banner.id = 'apiUnavailableBanner'; + banner.setAttribute('role', 'alert'); + banner.style.cssText = 'background: rgba(200,80,80,0.95); color: #fff; padding: 0.75rem 1rem; margin-bottom: 1rem; border-radius: 8px; font-size: 0.9rem;'; + banner.innerHTML = 'Explorer API temporarily unavailable (HTTP ' + status + '). Stats, blocks, and transactions cannot load until the backend is running. See docs.'; + main.insertBefore(banner, main.firstChild); + } (function() { var _log = console.log, _warn = console.warn; console.log = function() { if (window.DEBUG_EXPLORER) _log.apply(console, arguments); }; @@ -44,9 +57,11 @@ return j.result; } const BLOCKSCOUT_API_ORIGIN = 'https://explorer.d-bis.org/api'; // fallback when not on explorer host - // Origins that serve the explorer (FQDN or VM IP): use explicit same-origin API URL so nginx proxy is used + // Use relative /api when on explorer host so API always hits same host (avoids CORS/origin mismatch with www, port, or proxy) + const EXPLORER_HOSTS = ['explorer.d-bis.org', '192.168.11.140']; + const isOnExplorerHost = (typeof window !== 'undefined' && window.location && window.location.hostname && EXPLORER_HOSTS.indexOf(window.location.hostname) !== -1); + const BLOCKSCOUT_API = isOnExplorerHost ? '/api' : BLOCKSCOUT_API_ORIGIN; const EXPLORER_ORIGINS = ['https://explorer.d-bis.org', 'http://explorer.d-bis.org', 'http://192.168.11.140', 'https://192.168.11.140']; - const BLOCKSCOUT_API = (typeof window !== 'undefined' && window.location && EXPLORER_ORIGINS.includes(window.location.origin)) ? (window.location.origin + '/api') : BLOCKSCOUT_API_ORIGIN; const EXPLORER_ORIGIN = (typeof window !== 'undefined' && window.location && EXPLORER_ORIGINS.includes(window.location.origin)) ? window.location.origin : 'https://explorer.d-bis.org'; var I18N = { en: { home: 'Home', blocks: 'Blocks', transactions: 'Transactions', bridge: 'Bridge', weth: 'WETH', tokens: 'Tokens', analytics: 'Analytics', operator: 'Operator', watchlist: 'Watchlist', searchPlaceholder: 'Address, tx hash, block number, or token/contract name...', connectWallet: 'Connect Wallet', darkMode: 'Dark mode', lightMode: 'Light mode', back: 'Back', exportCsv: 'Export CSV', tokenBalances: 'Token Balances', internalTxns: 'Internal Txns', readContract: 'Read contract', writeContract: 'Write contract', addToWatchlist: 'Add to watchlist', removeFromWatchlist: 'Remove from watchlist', checkApprovals: 'Check token approvals', copied: 'Copied' }, @@ -577,6 +592,38 @@ } } + async function addTokenToWallet(address, symbol, decimals, name) { + if (!address || !/^0x[a-fA-F0-9]{40}$/i.test(address)) { + if (typeof showToast === 'function') showToast('Invalid token address', 'error'); + return; + } + if (typeof window.ethereum === 'undefined') { + if (typeof showToast === 'function') showToast('No wallet detected. Install MetaMask or another Web3 wallet.', 'error'); + return; + } + try { + var added = await window.ethereum.request({ + method: 'wallet_watchAsset', + params: { + type: 'ERC20', + options: { + address: address, + symbol: symbol || 'TOKEN', + decimals: typeof decimals === 'number' ? decimals : 18, + image: undefined + } + } + }); + if (typeof showToast === 'function') { + showToast(added ? (symbol ? symbol + ' added to wallet' : 'Token added to wallet') : 'Add token was cancelled', added ? 'success' : 'info'); + } + } catch (e) { + console.error('addTokenToWallet:', e); + if (typeof showToast === 'function') showToast(e.message || 'Could not add token to wallet', 'error'); + } + } + window.addTokenToWallet = addTokenToWallet; + let connectingMetaMask = false; async function connectMetaMask() { // Prevent multiple simultaneous connections @@ -1340,7 +1387,9 @@ headers: Object.fromEntries(response.headers.entries()) }; console.error(`❌ API Error:`, errorInfo); - + if (response.status === 502 || response.status === 503) { + showAPIUnavailableBanner(response.status); + } // For 400 errors, provide more context if (response.status === 400) { console.error('🔍 HTTP 400 Bad Request Details:'); @@ -1729,7 +1778,7 @@ // For ChainID 138, use Blockscout API if (CHAIN_ID === 138) { try { - response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?filter=to&page=1&page_size=10`); + response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?page=1&page_size=10`); rawTransactions = Array.isArray(response?.items) ? response.items : (Array.isArray(response?.data) ? response.data : []); } catch (apiErr) { console.warn('Blockscout transactions API failed, trying RPC fallback:', apiErr.message); @@ -1999,14 +2048,22 @@ var resp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/tokens?page=1&page_size=100').catch(function() { return null; }); var items = (resp && (resp.items || resp.data)) || (Array.isArray(resp) ? resp : null); if (items && items.length > 0) { - var html = '
| Token | Contract | Type |
|---|
| Token | Contract | Type | |
|---|---|---|---|
| ' + escapeHtml(name) + (symbol ? ' (' + escapeHtml(symbol) + ')' : '') + ' | ' + escapeHtml(shortenHash(addr)) + ' | ' + escapeHtml(type) + ' | |
| ' + escapeHtml(name) + (symbolDisplay ? ' (' + escapeHtml(symbolDisplay) + ')' : '') + ' | ' + escapeHtml(shortenHash(addr)) + ' | ' + escapeHtml(type) + ' |
Token not found or not indexed.
'; return; } - var name = data.name || '-'; - var symbol = data.symbol || '-'; - var decimals = data.decimals != null ? data.decimals : 18; + var knownTokenDetail = { + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': { name: 'Wrapped Ether', symbol: 'WETH', decimals: 18 }, + '0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': { name: 'Wrapped Ether v10', symbol: 'WETH', decimals: 18 } + }; + var known = knownTokenDetail[tokenAddress.toLowerCase()]; + var name = (known && known.name) || data.name || '-'; + var symbol = (known && known.symbol) || data.symbol || '-'; + var decimals = (known && known.decimals != null) ? known.decimals : (data.decimals != null ? data.decimals : 18); + decimals = parseInt(decimals, 10); + if (isNaN(decimals) || decimals < 0 || decimals > 255) decimals = 18; var supply = data.total_supply != null ? data.total_supply : (data.total_supply_raw || '0'); - var supplyNum = Number(supply) / Math.pow(10, parseInt(decimals, 10)); + var supplyNum = Number(supply) / Math.pow(10, decimals); var holders = data.holders_count != null ? data.holders_count : (data.holder_count || '-'); var transfersResp = null; try { transfersResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/tokens/' + tokenAddress + '/transfers?page=1&page_size=10').catch(function() { return { items: [] }; }); } catch (e) {} var transfers = (transfersResp && transfersResp.items) ? transfersResp.items : []; - var html = '