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 = ''; + var knownTokens = { + '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': { name: 'Wrapped Ether', symbol: 'WETH' }, + '0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': { name: 'Wrapped Ether v10', symbol: 'WETH' } + }; + var html = '
TokenContractType
'; items.forEach(function(t) { var addr = (t.address && (t.address.hash || t.address)) || t.address_hash || t.token_address || t.contract_address_hash || ''; - var name = t.name || t.symbol || '-'; - var symbol = t.symbol || ''; + var known = addr ? knownTokens[addr.toLowerCase()] : null; + var name = (known && known.name) || t.name || t.symbol || (known && known.symbol) || '-'; + var symbolDisplay = (known && known.symbol) || t.symbol; + var symbol = (symbolDisplay || 'TOKEN').replace(/'/g, "\\'"); + var decimals = t.decimals != null ? Number(t.decimals) : 18; var type = t.type || 'ERC-20'; if (!addr) return; - html += ''; + var addrEsc = escapeHtml(addr).replace(/'/g, "\\'"); + html += ''; }); html += '
TokenContractType
' + escapeHtml(name) + (symbol ? ' (' + escapeHtml(symbol) + ')' : '') + '' + escapeHtml(shortenHash(addr)) + '' + escapeHtml(type) + '
' + escapeHtml(name) + (symbolDisplay ? ' (' + escapeHtml(symbolDisplay) + ')' : '') + '' + escapeHtml(shortenHash(addr)) + '' + escapeHtml(type) + '
'; container.innerHTML = html; @@ -2209,6 +2266,7 @@
  • Base - Base L2
  • Arbitrum - Arbitrum One
  • Optimism - Optimism Mainnet
  • +
  • Cronos - Cronos (25); see routing table for full list and config-ready chains (Gnosis, Celo, Wemix)
  • How to Use:

    @@ -3171,18 +3229,27 @@ container.innerHTML = '

    Token not found or not indexed.

    View as address

    '; 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 = '
    Contract
    ' + escapeHtml(tokenAddress) + '
    '; + var addrEsc = tokenAddress.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + var symbolEsc = String(symbol).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + var html = '
    Contract
    ' + escapeHtml(tokenAddress) + '
    '; html += '
    Name
    ' + escapeHtml(name) + '
    '; html += '
    Symbol
    ' + escapeHtml(symbol) + '
    '; html += '
    Decimals
    ' + decimals + '
    '; diff --git a/frontend/public/index.html b/frontend/public/index.html index d420696..b57b8e2 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -14,7 +14,9 @@ - + @@ -683,6 +685,27 @@ opacity: 0.5; cursor: not-allowed; } + .btn-add-token-wallet { + padding: 0.35rem 0.5rem; + margin-left: 0.35rem; + border: none; + border-radius: 6px; + background: var(--primary); + color: white; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + vertical-align: middle; + transition: background 0.2s; + } + .btn-add-token-wallet:hover { + background: var(--secondary); + } + .btn-add-token-wallet:focus { + outline: 2px solid var(--primary); + outline-offset: 2px; + } .balance-display { background: var(--light); padding: 1rem; @@ -799,6 +822,32 @@ +