Explorer: add-to-wallet icon, WETH symbol/decimals fixes

- Add wallet icon (add to MetaMask) on WETH page, Tokens list, token detail
- addTokenToWallet() via EIP-747 wallet_watchAsset; toasts for success/error
- Known-token overrides: WETH9/WETH10 display name and symbol WETH; decimals 18
- Token list: show Wrapped Ether (WETH) for WETH9/WETH10 when API missing
- Token detail: force 18 decimals and name/symbol for WETH9/WETH10
- CSS for .btn-add-token-wallet

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-22 15:35:45 -08:00
parent a53c15507f
commit 43a7b88e2a
2 changed files with 136 additions and 18 deletions

View File

@@ -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 = '<strong>Explorer API temporarily unavailable</strong> (HTTP ' + status + '). Stats, blocks, and transactions cannot load until the backend is running. <a href="https://github.com/d-bis/explorer-monorepo/blob/main/docs/EXPLORER_API_ACCESS.md" target="_blank" rel="noopener" style="color: #fff; text-decoration: underline;">See docs</a>.';
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 = '<table class="table"><thead><tr><th>Token</th><th>Contract</th><th>Type</th></tr></thead><tbody>';
var knownTokens = {
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': { name: 'Wrapped Ether', symbol: 'WETH' },
'0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': { name: 'Wrapped Ether v10', symbol: 'WETH' }
};
var html = '<table class="table"><thead><tr><th>Token</th><th>Contract</th><th>Type</th><th aria-label="Add to wallet"></th></tr></thead><tbody>';
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 += '<tr style="cursor: pointer;" onclick="showTokenDetail(\'' + escapeHtml(addr) + '\')"><td>' + escapeHtml(name) + (symbol ? ' (' + escapeHtml(symbol) + ')' : '') + '</td><td class="hash">' + escapeHtml(shortenHash(addr)) + '</td><td>' + escapeHtml(type) + '</td></tr>';
var addrEsc = escapeHtml(addr).replace(/'/g, "\\'");
html += '<tr style="cursor: pointer;" onclick="showTokenDetail(\'' + escapeHtml(addr) + '\')"><td>' + escapeHtml(name) + (symbolDisplay ? ' (' + escapeHtml(symbolDisplay) + ')' : '') + '</td><td class="hash">' + escapeHtml(shortenHash(addr)) + '</td><td>' + escapeHtml(type) + '</td><td><button type="button" class="btn-add-token-wallet" onclick="event.stopPropagation(); addTokenToWallet(\'' + addrEsc + '\', \'' + symbol + '\', ' + decimals + ');" aria-label="Add to wallet" title="Add to wallet"><i class="fas fa-wallet" aria-hidden="true"></i></button></td></tr>';
});
html += '</tbody></table>';
container.innerHTML = html;
@@ -2209,6 +2266,7 @@
<li><strong>Base</strong> - Base L2</li>
<li><strong>Arbitrum</strong> - Arbitrum One</li>
<li><strong>Optimism</strong> - Optimism Mainnet</li>
<li><strong>Cronos</strong> - Cronos (25); see routing table for full list and config-ready chains (Gnosis, Celo, Wemix)</li>
</ul>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">How to Use:</h4>
@@ -3171,18 +3229,27 @@
container.innerHTML = '<p class="error">Token not found or not indexed.</p><p><a href="/address/' + encodeURIComponent(tokenAddress) + '">View as address</a></p>';
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 = '<div class="info-row"><div class="info-label">Contract</div><div class="info-value hash" onclick="showAddressDetail(\'' + escapeHtml(tokenAddress) + '\')" style="cursor: pointer;">' + escapeHtml(tokenAddress) + '</div></div>';
var addrEsc = tokenAddress.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
var symbolEsc = String(symbol).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
var html = '<div class="info-row"><div class="info-label">Contract</div><div class="info-value"><span class="hash" onclick="showAddressDetail(\'' + escapeHtml(tokenAddress) + '\')" style="cursor: pointer;">' + escapeHtml(tokenAddress) + '</span> <button type="button" class="btn-add-token-wallet" onclick="addTokenToWallet(\'' + addrEsc + '\', \'' + symbolEsc + '\', ' + decimals + ');" aria-label="Add to wallet" title="Add to wallet"><i class="fas fa-wallet" aria-hidden="true"></i></button></div></div>';
html += '<div class="info-row"><div class="info-label">Name</div><div class="info-value">' + escapeHtml(name) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Symbol</div><div class="info-value">' + escapeHtml(symbol) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Decimals</div><div class="info-value">' + decimals + '</div></div>';

View File

@@ -14,7 +14,9 @@
<meta name="author" content="SolaceScanScout">
<meta name="application-name" content="SolaceScanScout">
<meta name="theme-color" content="#667eea">
<script>
(function(){function t(){var e=document.getElementById('navLinks'),n=document.getElementById('navToggle'),r=document.getElementById('navToggleIcon');if(e&&n){var o=e.classList.toggle('nav-open');n.setAttribute('aria-expanded',o?'true':'false');if(r)r.className=o?'fas fa-times':'fas fa-bars';}}function c(){var e=document.getElementById('navLinks'),n=document.getElementById('navToggle'),r=document.getElementById('navToggleIcon');if(e)e.classList.remove('nav-open');if(n)n.setAttribute('aria-expanded','false');if(r)r.className='fas fa-bars';}window.toggleNavMenu=t;window.closeNavMenu=c;})();
</script>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://explorer.d-bis.org/">
@@ -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 @@
</style>
</head>
<body>
<script>
(function() {
function doToggleNav() {
var links = document.getElementById('navLinks');
var btn = document.getElementById('navToggle');
var icon = document.getElementById('navToggleIcon');
if (!links || !btn) return;
var isOpen = links.classList.toggle('nav-open');
btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
if (icon) icon.className = isOpen ? 'fas fa-times' : 'fas fa-bars';
}
function doCloseNav() {
var links = document.getElementById('navLinks');
var btn = document.getElementById('navToggle');
var icon = document.getElementById('navToggleIcon');
if (links) links.classList.remove('nav-open');
if (btn) btn.setAttribute('aria-expanded', 'false');
if (icon) icon.className = 'fas fa-bars';
}
window.toggleNavMenu = doToggleNav;
window.closeNavMenu = doCloseNav;
document.addEventListener('click', function(e) {
if (e.target.closest('#navToggle')) { doToggleNav(); }
});
})();
</script>
<nav class="navbar">
<div class="nav-container">
<div class="logo">
@@ -816,7 +865,7 @@
</button>
<span id="search-help-text" class="sr-only">Search by address (0x...40 hex), transaction hash (0x...64 hex), block number, or token/contract name</span>
</div>
<button type="button" class="nav-toggle" id="navToggle" aria-label="Toggle menu" aria-expanded="false" onclick="toggleNavMenu()"><i class="fas fa-bars" id="navToggleIcon"></i></button>
<button type="button" class="nav-toggle" id="navToggle" aria-label="Toggle menu" aria-expanded="false"><i class="fas fa-bars" id="navToggleIcon"></i></button>
<ul class="nav-links" id="navLinks">
<li class="nav-dropdown" id="navDropdownExplore">
<button type="button" class="nav-dropdown-trigger" aria-expanded="false" aria-haspopup="true" aria-controls="navMenuExplore" id="navTriggerExplore"><i class="fas fa-compass" aria-hidden="true"></i> <span data-i18n="explore">Explore</span> <i class="fas fa-chevron-down" aria-hidden="true"></i></button>
@@ -937,6 +986,7 @@
<div class="chain-name">WETH9 Token</div>
<div style="color: var(--text-light); margin-bottom: 1rem;">
Contract: <span class="hash" onclick="showAddressDetail('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')" style="cursor: pointer;">0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2</span>
<button type="button" class="btn-add-token-wallet" onclick="event.stopPropagation(); addTokenToWallet('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'WETH', 18, 'Wrapped Ether');" aria-label="Add WETH9 to wallet" title="Add to wallet"><i class="fas fa-wallet" aria-hidden="true"></i></button>
</div>
<div class="balance-display" id="weth9Balance">
@@ -986,6 +1036,7 @@
<div class="chain-name">WETH10 Token</div>
<div style="color: var(--text-light); margin-bottom: 1rem;">
Contract: <span class="hash" onclick="showAddressDetail('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')" style="cursor: pointer;">0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f</span>
<button type="button" class="btn-add-token-wallet" onclick="event.stopPropagation(); addTokenToWallet('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f', 'WETH', 18, 'Wrapped Ether v10');" aria-label="Add WETH10 to wallet" title="Add to wallet"><i class="fas fa-wallet" aria-hidden="true"></i></button>
</div>
<div class="balance-display" id="weth10Balance">
@@ -1041,8 +1092,8 @@
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">Contract Addresses</h4>
<ul style="margin-left: 2rem; margin-top: 0.5rem;">
<li><strong>WETH9:</strong> <span class="hash">0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2</span></li>
<li><strong>WETH10:</strong> <span class="hash">0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f</span></li>
<li><strong>WETH9:</strong> <span class="hash" onclick="showAddressDetail('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')" style="cursor: pointer;">0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2</span> <button type="button" class="btn-add-token-wallet" onclick="event.stopPropagation(); addTokenToWallet('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'WETH', 18);" aria-label="Add WETH9 to wallet" title="Add to wallet"><i class="fas fa-wallet"></i></button></li>
<li><strong>WETH10:</strong> <span class="hash" onclick="showAddressDetail('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')" style="cursor: pointer;">0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f</span> <button type="button" class="btn-add-token-wallet" onclick="event.stopPropagation(); addTokenToWallet('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f', 'WETH', 18);" aria-label="Add WETH10 to wallet" title="Add to wallet"><i class="fas fa-wallet"></i></button></li>
</ul>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">How to Use</h4>
@@ -1218,6 +1269,6 @@
</div>
</div>
<script src="/explorer-spa.js"></script>
<script src="/explorer-spa.js?v=3"></script>
</body>
</html>