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:
@@ -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>';
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user