const API_BASE = '/api';
const EXPLORER_API_BASE = '/explorer-api';
const EXPLORER_API_V1_BASE = EXPLORER_API_BASE + '/v1';
const TOKEN_AGGREGATION_API_BASE = '/token-aggregation/api';
const EXPLORER_AI_API_BASE = EXPLORER_API_V1_BASE + '/ai';
const FETCH_TIMEOUT_MS = 15000;
const RPC_HEALTH_TIMEOUT_MS = 5000;
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); };
console.warn = function() { if (window.DEBUG_EXPLORER) _warn.apply(console, arguments); };
})();
// RPC/WebSocket: VMID 2201 (public RPC). FQDN when HTTPS (avoids mixed content); IP when HTTP (e.g. http://192.168.11.140)
const RPC_IP = 'http://192.168.11.221:8545'; // Chain 138 - VMID 2201 besu-rpc-public-1
const RPC_WS_IP = 'ws://192.168.11.221:8546';
const RPC_FQDN = 'https://rpc-http-pub.d-bis.org'; // VMID 2201 - HTTPS
const RPC_WS_FQDN = 'wss://rpc-ws-pub.d-bis.org';
const RPC_URLS = (typeof window !== 'undefined' && window.location && window.location.protocol === 'https:')
? [RPC_FQDN] : [RPC_IP];
const RPC_URL = (typeof window !== 'undefined' && window.location && window.location.protocol === 'https:') ? RPC_FQDN : RPC_IP;
const RPC_WS_URL = (typeof window !== 'undefined' && window.location && window.location.protocol === 'https:') ? RPC_WS_FQDN : RPC_WS_IP;
let _rpcUrlIndex = 0;
let _blocksScrollAnimationId = null;
let _explorerAIState = {
open: false,
loading: false,
messages: [
{
role: 'assistant',
content: 'Explorer AI is ready for read-only ecosystem analysis. Ask about routes, liquidity, bridges, addresses, transactions, or current Chain 138 status.'
}
]
};
async function getRpcUrl() {
if (RPC_URLS.length <= 1) return RPC_URLS[0];
const ac = new AbortController();
const t = setTimeout(() => ac.abort(), RPC_HEALTH_TIMEOUT_MS);
for (let i = 0; i < RPC_URLS.length; i++) {
const url = RPC_URLS[(_rpcUrlIndex + i) % RPC_URLS.length];
try {
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_blockNumber', params: [], id: 1 }), signal: ac.signal });
clearTimeout(t);
if (r.ok) { _rpcUrlIndex = (_rpcUrlIndex + i) % RPC_URLS.length; return url; }
} catch (e) {}
}
clearTimeout(t);
return RPC_URLS[_rpcUrlIndex % RPC_URLS.length];
}
const CHAIN_ID = 138; // Hyperledger Besu ChainID 138
async function rpcCall(method, params) {
const url = await getRpcUrl();
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method, params: params || [], id: 1 }) });
const j = await r.json();
if (j.error) throw new Error(j.error.message || 'RPC error');
return j.result;
}
const BLOCKSCOUT_API_ORIGIN = 'https://explorer.d-bis.org/api'; // fallback when not on explorer host
// 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 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', addresses: 'Addresses', bridge: 'Bridge', weth: 'WETH', tokens: 'Tokens', pools: 'Pools', more: 'More', 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' },
de: { home: 'Start', blocks: 'Blöcke', transactions: 'Transaktionen', addresses: 'Adressen', bridge: 'Brücke', weth: 'WETH', tokens: 'Token', pools: 'Pools', more: 'Mehr', analytics: 'Analysen', operator: 'Operator', watchlist: 'Beobachtungsliste', searchPlaceholder: 'Adresse, Tx-Hash, Blocknummer oder Token/Vertrag…', connectWallet: 'Wallet verbinden', darkMode: 'Dunkelmodus', lightMode: 'Hellmodus', back: 'Zurück', exportCsv: 'CSV exportieren', tokenBalances: 'Token-Bestände', internalTxns: 'Interne Transaktionen', readContract: 'Vertrag lesen', writeContract: 'Vertrag schreiben', addToWatchlist: 'Zur Beobachtungsliste', removeFromWatchlist: 'Aus Beobachtungsliste entfernen', checkApprovals: 'Token-Freigaben prüfen', copied: 'Kopiert' },
fr: { home: 'Accueil', blocks: 'Blocs', transactions: 'Transactions', addresses: 'Adresses', bridge: 'Pont', weth: 'WETH', tokens: 'Jetons', pools: 'Pools', more: 'Plus', analytics: 'Analyses', operator: 'Opérateur', watchlist: 'Liste de suivi', searchPlaceholder: 'Adresse, hash de tx, numéro de bloc ou nom de token/contrat…', connectWallet: 'Connecter le portefeuille', darkMode: 'Mode sombre', lightMode: 'Mode clair', back: 'Retour', exportCsv: 'Exporter CSV', tokenBalances: 'Soldes de jetons', internalTxns: 'Transactions internes', readContract: 'Lire le contrat', writeContract: 'Écrire le contrat', addToWatchlist: 'Ajouter à la liste', removeFromWatchlist: 'Retirer de la liste', checkApprovals: 'Vérifier les approbations', copied: 'Copié' }
};
var currentLocale = (function(){ try { return localStorage.getItem('explorerLocale') || 'en'; } catch(e){ return 'en'; } })();
function t(key) { return (I18N[currentLocale] && I18N[currentLocale][key]) || I18N.en[key] || key; }
function setLocale(loc) { currentLocale = loc; try { localStorage.setItem('explorerLocale', loc); } catch(e){} if (typeof applyI18n === 'function') applyI18n(); }
function applyI18n() { document.querySelectorAll('[data-i18n]').forEach(function(el){ var k = el.getAttribute('data-i18n'); if (k) el.textContent = t(k); }); var searchIn = document.getElementById('smartSearchInput'); if (searchIn) searchIn.placeholder = t('searchPlaceholder'); var localeSel = document.getElementById('localeSelect'); if (localeSel) localeSel.value = currentLocale; var wcBtn = document.getElementById('walletConnectBtn'); if (wcBtn) wcBtn.textContent = t('connectWallet'); }
var _explorerPageFilters = {};
function normalizeExplorerFilter(value) { return String(value == null ? '' : value).trim().toLowerCase(); }
function getExplorerPageFilter(key) { return _explorerPageFilters[key] || ''; }
function setExplorerPageFilter(key, value) { _explorerPageFilters[key] = normalizeExplorerFilter(value); return _explorerPageFilters[key]; }
function clearExplorerPageFilter(key) { delete _explorerPageFilters[key]; return ''; }
function matchesExplorerFilter(haystack, filter) { if (!filter) return true; return String(haystack == null ? '' : haystack).toLowerCase().indexOf(filter) !== -1; }
function escapeAttr(value) { return escapeHtml(String(value == null ? '' : value)).replace(/"/g, '"'); }
const SMART_SEARCH_HISTORY_KEY = 'explorerSmartSearchHistory';
const SMART_SEARCH_HISTORY_LIMIT = 8;
let _smartSearchScope = 'all';
let _smartSearchPreviewTimer = null;
let _smartSearchPreviewRequestId = 0;
let _smartSearchTrendingCache = null;
function getSmartSearchHistory() {
try {
var raw = localStorage.getItem(SMART_SEARCH_HISTORY_KEY);
var parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter(Boolean) : [];
} catch (e) {
return [];
}
}
function saveSmartSearchHistory(query) {
var value = String(query || '').trim();
if (!value) return;
try {
var history = getSmartSearchHistory().filter(function(item) {
return String(item).toLowerCase() !== value.toLowerCase();
});
history.unshift(value);
history = history.slice(0, SMART_SEARCH_HISTORY_LIMIT);
localStorage.setItem(SMART_SEARCH_HISTORY_KEY, JSON.stringify(history));
} catch (e) {}
}
function detectSmartSearchType(query) {
var value = String(query || '').trim();
var normalized = value.replace(/\s/g, '');
if (!value) return { type: 'recent', label: 'Recent searches', detail: 'Start typing to narrow the explorer.' };
if (/^0x[a-fA-F0-9]{64}$/.test(normalized)) return { type: 'transaction', label: 'Transaction hash', detail: 'Enter will open the transaction detail page.' };
if (/^0x[a-fA-F0-9]{40}$/.test(normalized)) return { type: 'address', label: 'Address', detail: 'Enter will open the address detail page.' };
if (/^\d+$/.test(value)) return { type: 'block', label: 'Block number', detail: 'Enter will open the block detail page.' };
if (/\.eth$/i.test(value)) return { type: 'ens', label: 'ENS/domain', detail: 'The explorer will search or resolve this name.' };
if (/^[a-z0-9][a-z0-9._:-]{1,31}$/i.test(value)) return { type: 'token', label: 'Token / asset symbol', detail: 'The explorer will search token and contract matches.' };
return { type: 'search', label: 'Explorer search', detail: 'The explorer will search across indexed results.' };
}
function normalizeSmartSearchItemType(item) {
var type = String((item && (item.type || item.address_type || item.entity_type || item.kind)) || '').toLowerCase();
if (item && (item.tx_hash || (item.hash && String(item.hash).length === 66))) return 'transactions';
if (item && (item.block_number != null)) return 'blocks';
if (item && (item.token_address || item.token_contract_address_hash)) return 'tokens';
if (type.indexOf('tx') !== -1 || type.indexOf('transaction') !== -1) return 'transactions';
if (type.indexOf('block') !== -1) return 'blocks';
if (type.indexOf('token') !== -1 || type.indexOf('contract') !== -1) return 'tokens';
if (type.indexOf('address') !== -1) return 'addresses';
return 'all';
}
function setSmartSearchScope(scope) {
_smartSearchScope = scope || 'all';
try {
document.querySelectorAll('.smart-search-scope-btn').forEach(function(btn) {
var active = btn.getAttribute('data-scope') === _smartSearchScope;
btn.classList.toggle('btn-primary', active);
btn.classList.toggle('btn-secondary', !active);
});
} catch (e) {}
var input = document.getElementById('smartSearchInput');
updateSmartSearchPreview(input ? input.value : '');
}
window.setSmartSearchScope = setSmartSearchScope;
function renderSmartSearchHistory() {
var history = getSmartSearchHistory();
if (!history.length) {
return '
No recent searches yet.
';
}
var html = '';
history.forEach(function(item) {
var safeItem = String(item).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
html += ' ' + escapeHtml(item) + ' ';
});
html += '
';
return html;
}
function renderSmartSearchResultCard(item, action) {
var type = (item.type || item.address_type || '').toLowerCase();
var title = item.name || item.symbol || item.address_hash || item.hash || item.tx_hash || (item.block_number != null ? 'Block #' + item.block_number : '') || 'Result';
var subtitle = item.symbol && item.name ? item.symbol + ' • ' + item.name : item.symbol || item.name || item.address_hash || item.hash || item.tx_hash || '';
var meta = [];
if (item.address_hash || item.hash || item.token_address || item.token_contract_address_hash) {
meta.push(shortenHash(item.address_hash || item.hash || item.token_address || item.token_contract_address_hash));
}
if (item.block_number != null) meta.push('Block #' + item.block_number);
if (item.transaction_count != null) meta.push(String(item.transaction_count) + ' tx');
var html = 'No live suggestions yet. Press Enter to search everything, or try a more specific address, hash, block, or token symbol.
';
}
var html = '';
items.slice(0, 6).forEach(function(item) {
if (_smartSearchScope !== 'all' && normalizeSmartSearchItemType(item) !== _smartSearchScope) return;
var action = null;
if (item.token_address || item.token_contract_address_hash) {
var tokenAddr = item.token_address || item.token_contract_address_hash;
if (/^0x[a-f0-9]{40}$/i.test(tokenAddr)) action = 'showTokenDetail(\'' + escapeHtml(tokenAddr) + '\')';
}
if (!action && (item.address_hash || item.hash) && /^0x[a-f0-9]{40}$/i.test(item.address_hash || item.hash)) {
var addr = item.address_hash || item.hash;
action = 'showAddressDetail(\'' + escapeHtml(addr) + '\')';
}
if (!action && (item.tx_hash || (item.hash && item.hash.length === 66)) && /^0x[a-f0-9]{64}$/i.test(item.tx_hash || item.hash)) {
var txHash = item.tx_hash || item.hash;
action = 'showTransactionDetail(\'' + escapeHtml(txHash) + '\')';
}
if (!action && item.block_number != null) {
action = 'showBlockDetail(\'' + escapeHtml(String(item.block_number)) + '\')';
}
html += renderSmartSearchResultCard(item, action);
});
html += '
';
return html;
}
function renderSmartSearchTrendingTokens(tokens) {
if (!tokens || !tokens.length) {
return 'No trending tokens found yet.
';
}
var html = '';
tokens.slice(0, 6).forEach(function(token) {
var tokenAddr = (token.address && (token.address.hash || token.address)) || token.address_hash || token.token_address || token.contract_address_hash || '';
if (!tokenAddr) return;
var item = {
type: 'token',
name: token.name || token.symbol || 'Token',
symbol: token.symbol || '',
token_address: tokenAddr
};
html += renderSmartSearchResultCard(item, 'showTokenDetail(\'' + String(tokenAddr).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')');
});
html += '
';
return html;
}
async function fetchSmartSearchTrendingTokens() {
if (Array.isArray(_smartSearchTrendingCache)) return _smartSearchTrendingCache;
try {
var resp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/tokens?page=1&page_size=6');
var items = (resp && (resp.items || resp.data)) || [];
_smartSearchTrendingCache = Array.isArray(items) ? items : [];
} catch (e) {
_smartSearchTrendingCache = [];
}
return _smartSearchTrendingCache;
}
async function updateSmartSearchPreview(query) {
var input = document.getElementById('smartSearchInput');
var preview = document.getElementById('smartSearchPreview');
var detected = document.getElementById('smartSearchDetected');
if (!preview || !detected) return;
var info = detectSmartSearchType(query);
if (!query) {
detected.style.display = 'inline-block';
detected.textContent = _smartSearchScope === 'all' ? 'All' : (_smartSearchScope.charAt(0).toUpperCase() + _smartSearchScope.slice(1));
if (input) input.setAttribute('aria-describedby', 'smartSearchDetected');
var emptyRequestId = ++_smartSearchPreviewRequestId;
preview.innerHTML = 'Searching live suggestions...
';
var requestId = ++_smartSearchPreviewRequestId;
if (_smartSearchPreviewTimer) {
clearTimeout(_smartSearchPreviewTimer);
_smartSearchPreviewTimer = null;
}
_smartSearchPreviewTimer = setTimeout(async function() {
try {
var liveItems = [];
if (CHAIN_ID === 138) {
var resp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/search?q=' + encodeURIComponent(query));
if (requestId !== _smartSearchPreviewRequestId) return;
liveItems = (resp && resp.items) ? resp.items : [];
}
if (requestId !== _smartSearchPreviewRequestId) return;
if (_smartSearchScope !== 'all') {
liveItems = liveItems.filter(function(item) {
return normalizeSmartSearchItemType(item) === _smartSearchScope;
});
}
preview.innerHTML = 'No addresses in watchlist. Open an address and click "Add to watchlist".
'; return; }
if (filtered.length === 0) { container.innerHTML = filterBar + 'No watchlist entries match the current filter.
'; return; }
var html = filterBar + 'API temporarily unavailable. ' + escapeHtml((blockscoutError.message || 'Unknown error').substring(0, 150)) + ' Retry
';
}
return;
}
if (blocks.length === 0 && container) {
container.innerHTML = 'Could not load blocks. Retry
';
return;
}
}
} else {
// For other networks, use Etherscan-compatible API
const blockData = await fetchAPI(`${API_BASE}?module=block&action=eth_block_number`);
if (!blockData || !blockData.result) {
throw new Error('Invalid response from API');
}
const latestBlock = parseInt(blockData.result, 16);
if (isNaN(latestBlock) || latestBlock < 0) {
throw new Error('Invalid block number');
}
// Fetch blocks one by one
for (let i = 0; i < 10 && latestBlock - i >= 0; i++) {
const blockNum = latestBlock - i;
try {
const block = await fetchAPI(`${API_BASE}?module=block&action=eth_get_block_by_number&tag=0x${blockNum.toString(16)}&boolean=false`);
if (block && block.result) {
blocks.push({
number: blockNum,
hash: block.result.hash,
timestamp: block.result.timestamp,
transaction_count: block.result.transactions ? block.result.transactions.length : 0
});
}
} catch (e) {
console.warn(`Failed to load block ${blockNum}:`, e);
}
}
}
const limitedBlocks = blocks.slice(0, 10);
const blockFilter = getExplorerPageFilter('homeBlocks');
const filteredBlocks = blockFilter ? limitedBlocks.filter(function(block) {
var d = normalizeBlockDisplay(block);
return matchesExplorerFilter([d.blockNum, d.hash, d.txCount, d.timestampFormatted, d.timeAgo].join(' '), blockFilter);
}) : limitedBlocks;
const filterBar = renderPageFilterBar('homeBlocks', 'Filter blocks by number, hash, tx count, or age...', 'Filters the live block cards below.', 'loadLatestBlocks()');
if (limitedBlocks.length === 0) {
if (container) container.innerHTML = filterBar + 'No blocks found. Retry
';
} else if (filteredBlocks.length === 0) {
if (container) container.innerHTML = filterBar + 'No blocks match the current filter.
';
} else {
// Create HTML with duplicated blocks for seamless infinite loop
let html = filterBar + 'Failed to load blocks: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '. Retry
';
} finally {
loadingBlocks = false;
}
}
// Store previous transaction hashes for real-time updates
let previousTransactionHashes = new Set();
let transactionUpdateInterval = null;
async function loadLatestTransactions() {
const container = document.getElementById('latestTransactions');
if (!container) return;
try {
let response;
let rawTransactions = [];
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
try {
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);
try {
const blockNumHex = await rpcCall('eth_blockNumber');
const latest = parseInt(blockNumHex, 16);
for (let i = 0; i < Math.min(5, latest + 1) && rawTransactions.length < 10; i++) {
const b = await rpcCall('eth_getBlockByNumber', ['0x' + (latest - i).toString(16), true]);
if (b && b.transactions) {
b.transactions.forEach(tx => {
if (typeof tx === 'object' && rawTransactions.length < 10) rawTransactions.push({ hash: tx.hash, from: tx.from, to: tx.to || null, value: tx.value || '0x0', block_number: parseInt(b.number, 16), created_at: b.timestamp ? new Date(parseInt(b.timestamp, 16) * 1000).toISOString() : null });
});
}
}
response = { items: rawTransactions };
} catch (rpcErr) {
console.error('RPC transactions fallback failed:', rpcErr);
if (container) container.innerHTML = 'API temporarily unavailable. Retry
';
return;
}
}
} else {
response = await fetchAPIWithRetry(`${API_BASE}/v2/transactions?page=1&page_size=10`);
rawTransactions = Array.isArray(response?.items) ? response.items : (Array.isArray(response?.data) ? response.data : []);
}
// Normalize transactions using adapter
const transactions = rawTransactions.map(normalizeTransaction).filter(tx => tx !== null);
// Limit to 10 transactions
const limitedTransactions = transactions.slice(0, 10);
const txFilter = getExplorerPageFilter('homeTransactions');
const filteredTransactions = txFilter ? limitedTransactions.filter(function(tx) {
const hash = String(tx.hash || '');
const from = String(tx.from || '');
const to = String(tx.to || '');
const blockNumber = String(tx.block_number || '');
const value = String(tx.value || '0');
return matchesExplorerFilter([hash, from, to, blockNumber, formatEther(value)].join(' '), txFilter);
}) : limitedTransactions;
const filterBar = renderPageFilterBar('homeTransactions', 'Filter by hash, address, block, or value...', 'Filters the live transaction table below.', 'loadLatestTransactions()');
// Check for new transactions
const currentHashes = new Set(filteredTransactions.map(tx => String(tx.hash || '')));
const newTransactions = filteredTransactions.filter(tx => !previousTransactionHashes.has(String(tx.hash || '')));
// Update previous hashes
previousTransactionHashes = currentHashes;
// Show skeleton loader only on first load
if (container.innerHTML.includes('skeleton') || container.innerHTML.includes('Loading')) {
container.innerHTML = createSkeletonLoader('table');
}
let html = filterBar + 'Failed to load transactions: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '. Retry
';
}
}
// Real-time transaction updates
function startTransactionUpdates() {
// Clear any existing interval
if (transactionUpdateInterval) {
clearInterval(transactionUpdateInterval);
}
// Update transactions every 5 seconds
transactionUpdateInterval = setInterval(() => {
if (currentView === 'home') {
loadLatestTransactions();
}
}, 5000);
}
function stopTransactionUpdates() {
if (transactionUpdateInterval) {
clearInterval(transactionUpdateInterval);
transactionUpdateInterval = null;
}
}
var blocksListPage = 1;
var transactionsListPage = 1;
var addressesListPage = 1;
const LIST_PAGE_SIZE = 25;
async function loadAllBlocks(page) {
if (page != null) blocksListPage = Math.max(1, parseInt(page, 10) || 1);
const container = document.getElementById('blocksList');
if (!container) { return; }
try {
container.innerHTML = 'Failed to load blocks: ' + escapeHtml(error.message || 'Unknown error') + '. Retry
';
}
}
async function loadAllTransactions(page) {
if (page != null) transactionsListPage = Math.max(1, parseInt(page, 10) || 1);
const container = document.getElementById('transactionsList');
if (!container) { return; }
try {
container.innerHTML = 'Failed to load transactions: ' + escapeHtml(error.message || 'Unknown error') + '. Retry
';
}
}
async function loadAllAddresses(page) {
if (page != null) addressesListPage = Math.max(1, parseInt(page, 10) || 1);
const container = document.getElementById('addressesList');
if (!container) { return; }
try {
container.innerHTML = 'Failed to load addresses: ' + escapeHtml(error.message || 'Unknown error') + '. Retry
';
}
}
window._loadAllAddresses = loadAllAddresses;
window.loadAllAddresses = loadAllAddresses;
async function loadTokensList() {
var container = document.getElementById('tokensListContent');
if (!container) return;
try {
container.innerHTML = 'No token index available. Use the search bar to find tokens by name, symbol, or contract address (0x...).
';
} catch (err) {
container.innerHTML = renderPageFilterBar('tokensList', 'Filter by token name, symbol, contract, or type...', 'Filters the indexed token list below.', 'loadTokensList()') + 'Failed to load tokens. Use the search bar to find a token by address or name.
';
}
}
window._loadTokensList = loadTokensList;
function normalizeRouteStatus(status) {
return status || 'unavailable';
}
function renderRouteMetric(label, value) {
return 'No quote-token metadata gaps detected in the current indexed pool set.
';
}
var html = '';
html += '
';
html += '';
html += '
';
html += '
Priority routes
' + escapeHtml(String(priorityOkResults.length)) + ' ok' + (priorityErrors.length ? ' / ' + String(priorityErrors.length) + ' failed' : '') + '
';
html += '
Sweep probes
' + escapeHtml(String(sweepOkResults.length)) + ' ok
';
html += '
Missing quote routes
' + escapeHtml(String(allSweepMissing.length)) + '
';
html += '
';
html += '
This sweep probes explicit local token pairs against compliant and official anchor assets on Chain 138. The priority route cards above remain the bridge-path checks; this table focuses on direct-pair coverage and quote-token metadata gaps.
';
html += renderRouteSweepSummary(sweepOkResults);
if (priorityErrors.length) {
html += '
Some priority route requests failed, but the pools table is still available.
';
}
html += '
';
html += '
';
priorityOkResults.forEach(function(entry) {
html += renderPriorityRouteCard(entry);
});
html += '
';
html += '
';
html += '';
html += renderMissingQuotePools(allSweepMissing);
html += '
';
container.innerHTML = html;
updatePoolsMissingQuoteBadge(allSweepMissing.length);
} catch (err) {
container.innerHTML = '
Failed to load live route tree: ' + escapeHtml(err.message || 'Unknown error') + '
';
updatePoolsMissingQuoteBadge(0);
}
}
function summarizePoolRows(rows) {
var summary = {
liveLocal: 0,
externalMainnet: 0,
notYetCreated: 0,
missingCode: 0,
partial: 0,
};
(rows || []).forEach(function(row) {
var status = String((row && row.status) || '').toLowerCase();
if (status.indexOf('funded (live)') !== -1 || status.indexOf('deployed (live)') !== -1) {
summary.liveLocal += 1;
return;
}
if (status.indexOf('external / mainnet') !== -1 || status.indexOf('external / not on chain 138') !== -1) {
summary.externalMainnet += 1;
return;
}
if (status.indexOf('not created') !== -1) {
summary.notYetCreated += 1;
return;
}
if (status.indexOf('missing code') !== -1) {
summary.missingCode += 1;
return;
}
if (status.indexOf('partially funded') !== -1 || status.indexOf('created (unfunded)') !== -1) {
summary.partial += 1;
}
});
return summary;
}
function toCsv(rows) {
return rows.map(function(row) {
return row.map(function(cell) {
return '"' + String(cell == null ? '' : cell).replace(/"/g, '""') + '"';
}).join(',');
}).join('\n');
}
function exportPoolsCSV() {
if (!latestPoolsSnapshot || !Array.isArray(latestPoolsSnapshot.rows)) {
showToast('Pools data is not ready yet', 'error');
return;
}
var summary = latestPoolsSnapshot.summary || {};
var rows = latestPoolsSnapshot.rows || [];
var csvRows = [
['Section', 'Metric', 'Value'],
['Summary', 'Generated At', latestPoolsSnapshot.generatedAt || ''],
['Summary', 'Live local pools', summary.liveLocal || 0],
['Summary', 'External Mainnet-side', summary.externalMainnet || 0],
['Summary', 'Not yet created', summary.notYetCreated || 0],
['Summary', 'Needs attention', (summary.missingCode || 0) + (summary.partial || 0)],
[],
['Category', 'Pool Pair', 'System', 'Address', 'Status', 'Notes']
];
rows.forEach(function(row) {
csvRows.push([
row.category || '',
row.poolPair || '',
row.poolType || '',
row.address || '',
row.status || '',
row.notes || ''
]);
});
var blob = new Blob([toCsv(csvRows)], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'pools-status.csv';
a.click();
URL.revokeObjectURL(url);
showToast('CSV downloaded', 'success');
}
function exportPoolsJSON() {
if (!latestPoolsSnapshot) {
showToast('Pools data is not ready yet', 'error');
return;
}
var blob = new Blob([JSON.stringify(latestPoolsSnapshot, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'pools-status.json';
a.click();
URL.revokeObjectURL(url);
showToast('JSON downloaded', 'success');
}
window.exportPoolsCSV = exportPoolsCSV;
window.exportPoolsJSON = exportPoolsJSON;
async function renderPoolsView() {
showView('pools');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'pools') updatePath('/pools');
var container = document.getElementById('poolsContent');
if (_poolsRouteTreeRefreshTimer) {
clearInterval(_poolsRouteTreeRefreshTimer);
_poolsRouteTreeRefreshTimer = null;
}
if (!container) return;
try {
container.innerHTML = '
Loading pools...
';
var live = await getLivePoolRows();
var filter = getExplorerPageFilter('poolsList');
var filterBar = renderPageFilterBar('poolsList', 'Filter by category, pair, type, status, address, or notes...', 'Tracks live Chain 138 pool, reserve, and bridge-linked contract state.', 'openPoolsView()');
var summary = summarizePoolRows(live.rows);
latestPoolsSnapshot = {
generatedAt: new Date().toISOString(),
summary: summary,
rows: live.rows
};
var rows = live.rows.map(function(row) {
return { row: row, searchText: [row.category, row.poolPair, row.poolType, row.address, row.status, row.notes].join(' ') };
});
var filtered = filter ? rows.filter(function(entry) { return matchesExplorerFilter(entry.searchText, filter); }) : rows;
var html = filterBar + '
This table is derived from live Chain 138 contract state. Pool addresses, funding status, quote-token readiness, and private-registry registrations are refreshed from the chain each time the page renders. External or mainnet-only systems are labeled explicitly.
';
html += '
';
html += '
Live local pools
' + escapeHtml(String(summary.liveLocal)) + '
Funded or deployed directly on Chain 138
';
html += '
External Mainnet-side
' + escapeHtml(String(summary.externalMainnet)) + '
Expected to live off Chain 138 by design
';
html += '
Not yet created
' + escapeHtml(String(summary.notYetCreated)) + '
No live pool mapping currently registered
';
html += '
Needs attention
' + escapeHtml(String(summary.missingCode + summary.partial)) + '
Missing code, partial funding, or unfunded rows
';
html += '
';
html += '
Category Pool Pair System Address Status Notes ';
if (rows.length === 0) {
html += 'No pool data available yet. ';
} else if (filtered.length === 0) {
html += 'No pools match the current filter. ';
} else {
filtered.forEach(function(entry) {
var row = entry.row;
var addr = row.address || '';
html += '';
html += '' + escapeHtml(row.category) + ' ';
html += '' + escapeHtml(row.poolPair) + ' ';
html += '' + escapeHtml(row.poolType) + ' ';
html += '' + (safeAddress(addr) ? '' + escapeHtml(shortenHash(addr)) + ' ' : '— ') + ' ';
html += '' + escapeHtml(row.status) + ' ';
html += '' + escapeHtml(row.notes) + ' ';
html += ' ';
});
}
html += '
';
html += '
';
html += '';
html += '
This live panel follows the Chain 138 DEX and bridge graph end-to-end so we can see direct pool depth, bridge leg availability, destination swap branches, and any missing quote-token metadata that still needs cleanup.
';
html += '
Loading live route tree...
';
html += '
';
container.innerHTML = html;
setTimeout(function() {
loadLiveRouteTrees();
}, 0);
_poolsRouteTreeRefreshTimer = setInterval(function() {
if (currentView === 'pools') {
loadLiveRouteTrees();
}
}, ROUTE_TREE_REFRESH_MS);
} catch (err) {
container.innerHTML = '
Failed to load pools: ' + escapeHtml(err.message || 'Unknown error') + '
';
}
}
window.renderPoolsView = renderPoolsView;
function renderLiquidityAccessView() {
showView('liquidity');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'liquidity') updatePath('/liquidity');
var container = document.getElementById('liquidityContent');
if (!container) return;
var publicApiBase = TOKEN_AGGREGATION_API_BASE + '/v1';
var livePools = [
{
pair: 'cUSDT / cUSDC',
poolAddress: '0xff8d3b8fDF7B112759F076B69f4271D4209C0849',
reserves: '10,000,000 / 10,000,000'
},
{
pair: 'cUSDT / USDT',
poolAddress: '0x6fc60DEDc92a2047062294488539992710b99D71',
reserves: '10,000,000 / 10,000,000'
},
{
pair: 'cUSDC / USDC',
poolAddress: '0x0309178ae30302D83c76d6Dd402a684eF3160eec',
reserves: '10,000,000 / 10,000,000'
},
{
pair: 'cUSDT / cXAUC',
poolAddress: '0x1AA55E2001E5651349AfF5A63FD7A7Ae44f0F1b0',
reserves: '2,666,965 / 519.477000'
},
{
pair: 'cUSDC / cXAUC',
poolAddress: '0xEA9Ac6357CaCB42a83b9082B870610363B177cBa',
reserves: '1,000,000 / 194.782554'
},
{
pair: 'cEURT / cXAUC',
poolAddress: '0xbA99bc1eAAC164569d5AcA96C806934DDaF970Cf',
reserves: '1,000,000 / 225.577676'
}
];
var endpointCards = [
{
title: 'Canonical route matrix',
method: 'GET',
href: publicApiBase + '/routes/matrix',
notes: 'Full live-route inventory with optional blocked and planned route visibility.'
},
{
title: 'Live ingestion export',
method: 'GET',
href: publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap',
notes: 'Flat export for adapter discovery and route ingestion.'
},
{
title: 'Partner payload templates',
method: 'GET',
href: publicApiBase + '/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true',
notes: 'Builds request templates for 1inch, 0x, and LiFi from live routes.'
},
{
title: 'Resolve supported partner payloads',
method: 'POST',
href: publicApiBase + '/routes/partner-payloads/resolve',
notes: 'Accepts partner, amount, and addresses and returns supported payloads by default.'
},
{
title: 'Dispatch supported partner payload',
method: 'POST',
href: publicApiBase + '/routes/partner-payloads/dispatch',
notes: 'Dispatches one supported partner payload when the chain is publicly supported.'
},
{
title: 'Internal Chain 138 execution plan',
method: 'POST',
href: publicApiBase + '/routes/internal-execution-plan',
notes: 'Returns the DODO PMM fallback execution plan when public partner support is unavailable.'
}
];
var requestExamples = [
'GET ' + publicApiBase + '/routes/matrix?includeNonLive=true',
'GET ' + publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap',
'GET ' + publicApiBase + '/routes/partner-payloads?partner=LiFi&amount=1000000&includeUnsupported=true',
'POST ' + publicApiBase + '/routes/partner-payloads/resolve',
'POST ' + publicApiBase + '/routes/internal-execution-plan'
];
var html = '';
html += '
';
html += '
Live public pools
6
Verified DODO PMM pools on Chain 138.
';
html += '
Public access path
/token-aggregation/api/v1
Explorer-hosted proxy for route and execution APIs.
';
html += '
Partner status
Fallback Ready
Templates exist for 1inch, 0x, and LiFi, but Chain 138 execution still falls back internally.
';
html += '
';
html += '
';
html += '';
html += '
';
livePools.forEach(function(pool) {
html += '
';
html += '
';
html += '
' + escapeHtml(pool.pair) + '
Pool: ' + escapeHtml(pool.poolAddress) + '
';
html += '
Reserves: ' + escapeHtml(pool.reserves) + '
';
html += '
';
});
html += '
';
html += '
';
html += '';
html += '
';
html += '
Direct live routes today: cUSDT ↔ cUSDC, cUSDT ↔ USDT, cUSDC ↔ USDC, cUSDT ↔ cXAUC, cUSDC ↔ cXAUC, and cEURT ↔ cXAUC.
';
html += '
Multi-hop public paths exist through cXAUC for cEURT ↔ cUSDT, cEURT ↔ cUSDC, and an alternate cUSDT ↔ cUSDC path.
';
html += '
Mainnet bridge discovery is live for cUSDT → USDT and cUSDC → USDC through the configured UniversalCCIPBridge lane.
';
html += '
1inch, 0x, and LiFi request templates are available through the explorer API, but those partners do not publicly support Chain 138 execution today.
';
html += '
When public partner execution is unavailable, the internal DODO PMM execution plan endpoint returns the Chain 138 fallback route instead of a dead end.
';
html += '
';
html += '
';
html += '
';
html += '
';
requestExamples.forEach(function(example) {
html += '
' + escapeHtml(example) + '
';
});
html += '
';
html += '
';
html += '
Use Wallet for network onboarding and the explorer token list URL, then use Pools for live route-tree diagnostics and contract-state checks.
';
html += '
';
html += '
Pools view';
html += '
WETH tools';
html += '
Explorer docs';
html += '
';
html += '
';
container.innerHTML = html;
}
window.renderLiquidityAccessView = renderLiquidityAccessView;
function renderMoreView() {
showView('more');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'more') updatePath('/more');
var container = document.getElementById('moreContent');
if (!container) return;
var groups = [
{
key: 'tools',
title: 'Tools',
items: [
{ title: 'Input Data Decoder', icon: 'fa-file-code', status: 'Live', badgeClass: 'badge-info', desc: 'Open transaction detail pages to decode calldata, logs, and contract interactions already exposed by the explorer.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Unit Converter', icon: 'fa-scale-balanced', status: 'Live', badgeClass: 'badge-success', desc: 'Convert wei, gwei, ether, and common Chain 138 stablecoin units with a quick in-page helper.', action: 'showUnitConverterModal();', href: '/more' },
{ title: 'CSV Export', icon: 'fa-file-csv', status: 'Live', badgeClass: 'badge-success', desc: 'Export pool state and route inventory snapshots for operator review and downstream ingestion.', action: 'showPools(); updatePath(\'/pools\'); setTimeout(function(){ if (typeof exportPoolsCSV === \"function\") exportPoolsCSV(); }, 200);', href: '/pools' },
{ title: 'Account Balance Checker', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Jump into indexed addresses to inspect balances, token inventory, internal transfers, and recent activity.', action: 'showAddresses();', href: '/addresses' }
]
},
{
key: 'explore',
title: 'Explore',
items: [
{ title: 'Gas Tracker', icon: 'fa-gas-pump', status: 'Live', badgeClass: 'badge-success', desc: 'Review live gas, block time, TPS, and chain health from the analytics and home dashboards.', action: 'showAnalytics();', href: '/analytics' },
{ title: 'DEX Tracker', icon: 'fa-chart-line', status: 'Live', badgeClass: 'badge-success', desc: 'Open liquidity discovery, PMM pool status, live route trees, and partner payload access points.', action: 'showLiquidityAccess();', href: '/liquidity' },
{ title: 'Node Tracker', icon: 'fa-server', status: 'Live', badgeClass: 'badge-success', desc: 'Inspect bridge and operator infrastructure surfaces already exposed in the Bridge and Operator panels.', action: 'showOperator();', href: '/operator' },
{ title: 'Label Cloud', icon: 'fa-tags', status: 'Live', badgeClass: 'badge-success', desc: 'Browse labeled addresses, contracts, and address activity through the explorer address index.', action: 'showAddresses();', href: '/addresses' },
{ title: 'Domain Name Lookup', icon: 'fa-magnifying-glass', status: 'Live', badgeClass: 'badge-success', desc: 'Use the smart search launcher to resolve ENS-style names, domains, addresses, hashes, and token symbols.', action: 'openSmartSearchModal(\'\');', href: '/more' }
]
},
{
key: 'services',
title: 'Services',
items: [
{ title: 'Token Approvals', icon: 'fa-shield-halved', status: 'External', badgeClass: 'badge-warning', desc: 'Jump to revoke.cash for wallet approval review. Address detail pages also expose approval shortcuts directly.', action: 'openExternalMoreLink(\'https://revoke.cash/\');', href: '#' },
{ title: 'Verified Signature', icon: 'fa-signature', status: 'Live', badgeClass: 'badge-success', desc: 'Use wallet sign-in and verified address flows already built into the explorer authentication surfaces.', action: 'showWalletModal();', href: '/more' },
{ title: 'Input Data Messages', icon: 'fa-message', status: 'Live', badgeClass: 'badge-info', desc: 'Transaction detail pages already surface decoded input data, event logs, and contract interaction context.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Advanced Filter', icon: 'fa-filter', status: 'Live', badgeClass: 'badge-success', desc: 'Block, transaction, address, token, pool, bridge, and watchlist screens all support focused page-level filtering.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Blockscan Chat', icon: 'fa-comments', status: 'Soon', badgeClass: 'badge-muted', desc: 'Messaging and collaborative address discussion are not exposed yet in SolaceScanScout.', disabled: true, href: '/more' }
]
}
];
var html = '
';
html += '
';
html += '
Tools & Services
';
html += '
Discover more of SolaceScanScout's explorer tools in one place, grouped the way users expect from Etherscan-style explorers.
';
html += '
';
html += '
Now live
Route matrix, ingestion APIs, smart search, pool exports, and live Mainnet stable bridge discovery.
';
html += '
Good entry points
';
html += ' Liquidity ';
html += ' Search ';
html += ' Addresses ';
html += '
';
html += '
';
groups.forEach(function(group) {
html += '
';
html += '
' + escapeHtml(group.title) + '
';
html += '
';
});
html += '
';
container.innerHTML = html;
}
window._showMore = renderMoreView;
window.showUnitConverterModal = function() {
var existing = document.getElementById('unitConverterModal');
if (existing) existing.remove();
var modal = document.createElement('div');
modal.id = 'unitConverterModal';
modal.style.cssText = 'position:fixed; inset:0; background:rgba(8,15,32,0.68); backdrop-filter:blur(8px); z-index:12000; display:flex; align-items:center; justify-content:center; padding:1rem;';
modal.innerHTML = '' +
'
' +
'
' +
'
Unit Converter
Wei, gwei, ether, and 6-decimal stablecoin units for Chain 138.
' +
'
' +
'
' +
'
' +
'
Amount ' +
'
Unit Ether / WETH Gwei Wei Stablecoin (6 decimals) ' +
'
' +
'
' +
'
';
document.body.appendChild(modal);
function renderUnitConverterResults() {
var amountEl = document.getElementById('unitConverterAmount');
var unitEl = document.getElementById('unitConverterUnit');
var resultsEl = document.getElementById('unitConverterResults');
if (!amountEl || !unitEl || !resultsEl) return;
var amount = Number(amountEl.value || '0');
var unit = unitEl.value;
if (!isFinite(amount) || amount < 0) {
resultsEl.innerHTML = '
Enter a non-negative amount to convert.
';
return;
}
var wei = 0;
if (unit === 'ether') wei = amount * 1e18;
else if (unit === 'gwei') wei = amount * 1e9;
else if (unit === 'wei') wei = amount;
else if (unit === 'stable') wei = amount * 1e6;
var etherValue = unit === 'stable' ? 'N/A' : (wei / 1e18).toLocaleString(undefined, { maximumFractionDigits: 18 });
var gweiValue = unit === 'stable' ? 'N/A' : (wei / 1e9).toLocaleString(undefined, { maximumFractionDigits: 9 });
var stableValue = (unit === 'stable' ? amount : wei / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 });
resultsEl.innerHTML =
'
Wei: ' + escapeHtml(Math.round(wei).toString()) + '
' +
'
Gwei: ' + escapeHtml(gweiValue) + '
' +
'
Ether / WETH: ' + escapeHtml(etherValue) + '
' +
'
6-decimal stable amount: ' + escapeHtml(stableValue) + '
';
}
var closeBtn = document.getElementById('unitConverterCloseBtn');
if (closeBtn) closeBtn.addEventListener('click', function() { modal.remove(); });
modal.addEventListener('click', function(event) {
if (event.target === modal) modal.remove();
});
var amountEl = document.getElementById('unitConverterAmount');
var unitEl = document.getElementById('unitConverterUnit');
if (amountEl) amountEl.addEventListener('input', renderUnitConverterResults);
if (unitEl) unitEl.addEventListener('change', renderUnitConverterResults);
renderUnitConverterResults();
};
window.openExternalMoreLink = function(url) {
window.open(url, '_blank', 'noopener,noreferrer');
};
async function refreshBridgeData() {
const container = document.getElementById('bridgeContent');
if (!container) return;
try {
container.innerHTML = '
Loading bridge data...
';
// Chain 138 Bridge Contracts
const WETH9_BRIDGE_138 = '0x971cD9D156f193df8051E48043C476e53ECd4693';
const WETH10_BRIDGE_138 = '0xe0E93247376aa097dB308B92e6Ba36bA015535D0';
// Ethereum Mainnet Bridge Contracts
const WETH9_BRIDGE_MAINNET = '0x2A0840e5117683b11682ac46f5CF5621E67269E3';
const WETH10_BRIDGE_MAINNET = '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03';
const explorerLinks = {
'BSC (56)': { label: 'BscScan', baseUrl: 'https://bscscan.com/address/' },
'Polygon (137)': { label: 'PolygonScan', baseUrl: 'https://polygonscan.com/address/' },
'Avalanche (43114)': { label: 'Avalanche Explorer', baseUrl: 'https://subnets.avax.network/c-chain/address/' },
'Base (8453)': { label: 'BaseScan', baseUrl: 'https://basescan.org/address/' },
'Arbitrum (42161)': { label: 'Arbiscan', baseUrl: 'https://arbiscan.io/address/' },
'Optimism (10)': { label: 'Optimistic Etherscan', baseUrl: 'https://optimistic.etherscan.io/address/' },
'Ethereum Mainnet (1)': { label: 'Etherscan', baseUrl: 'https://etherscan.io/address/' }
};
function renderExplorerLink(chainLabel, address) {
const explorer = explorerLinks[chainLabel];
if (!explorer) return '
No explorer ';
const url = explorer.baseUrl + address;
return '
View on ' + escapeHtml(explorer.label) + ' ';
}
// Bridge routes configuration
const routes = {
weth9: {
'BSC (56)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
'Polygon (137)': '0xa780ef19a041745d353c9432f2a7f5a241335ffe',
'Avalanche (43114)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
'Base (8453)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
'Arbitrum (42161)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
'Optimism (10)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
'Ethereum Mainnet (1)': WETH9_BRIDGE_MAINNET
},
weth10: {
'BSC (56)': '0x105f8a15b819948a89153505762444ee9f324684',
'Polygon (137)': '0xdab0591e5e89295ffad75a71dcfc30c5625c4fa2',
'Avalanche (43114)': '0x105f8a15b819948a89153505762444ee9f324684',
'Base (8453)': '0x105f8a15b819948a89153505762444ee9f324684',
'Arbitrum (42161)': '0x105f8a15b819948a89153505762444ee9f324684',
'Optimism (10)': '0x105f8a15b819948a89153505762444ee9f324684',
'Ethereum Mainnet (1)': WETH10_BRIDGE_MAINNET
}
};
const sourceBridgeCount = 2;
const mainnetBridgeCount = 2;
const routeBridgeCount = new Set([
...Object.values(routes.weth9),
...Object.values(routes.weth10)
].map(function(addr) { return String(addr || '').toLowerCase(); })).size;
const totalBridgeCount = getActiveBridgeContractCount();
const bridgeFilter = getExplorerPageFilter('bridgeRoutes');
const filterBar = renderPageFilterBar('bridgeRoutes', 'Filter by chain name, chain ID, or bridge address...', 'Filters the route tables below.', 'refreshBridgeData()');
// Build HTML
let html = filterBar + `
CCIP Bridge Ecosystem
${sourceBridgeCount} source contracts / ${mainnetBridgeCount} mainnet contracts / ${routeBridgeCount} unique route contracts
Cross-chain interoperability powered by Chainlink CCIP
CCIPWETH9Bridge
${WETH9_BRIDGE_138}
Token: WETH9
CCIPWETH10Bridge
${WETH10_BRIDGE_138}
Token: WETH10
Destination Chain
Chain ID
Bridge Address
Explorer
`;
// Add WETH9 routes
const weth9Routes = Object.entries(routes.weth9).filter(function(entry) {
return !bridgeFilter || matchesExplorerFilter(entry[0] + ' ' + entry[1], bridgeFilter);
});
const weth10Routes = Object.entries(routes.weth10).filter(function(entry) {
return !bridgeFilter || matchesExplorerFilter(entry[0] + ' ' + entry[1], bridgeFilter);
});
if (weth9Routes.length === 0) {
html += 'No WETH9 routes match the current filter. ';
}
for (const [chain, address] of weth9Routes) {
const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
html += `
${chain.replace(/\s*\\(\\d+\\)/, '')}
${chainId}
${escapeHtml(shortenHash(address))}
${renderExplorerLink(chain, address)}
`;
}
html += `
Destination Chain
Chain ID
Bridge Address
Explorer
`;
// Add WETH10 routes
if (weth10Routes.length === 0) {
html += 'No WETH10 routes match the current filter. ';
}
for (const [chain, address] of weth10Routes) {
const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
html += `
${chain.replace(/\s*\\(\\d+\\)/, '')}
${chainId}
${escapeHtml(shortenHash(address))}
${renderExplorerLink(chain, address)}
`;
}
html += `
CCIPWETH9Bridge
${WETH9_BRIDGE_MAINNET}
CCIPWETH10Bridge
${WETH10_BRIDGE_MAINNET}
CCIP Bridge Ecosystem enables cross-chain transfers of WETH9 and WETH10 tokens using Chainlink CCIP (Cross-Chain Interoperability Protocol).
Supported Networks:
Chain 138 - Source chain with both bridge contracts
Ethereum Mainnet - Destination with dedicated bridge contracts
BSC - Binance Smart Chain
Polygon - Polygon PoS
Avalanche - Avalanche C-Chain
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:
Click on any bridge address to view detailed information and transaction history
Use the bridge contracts to transfer WETH9 or WETH10 tokens between supported chains
All transfers are secured by Chainlink CCIP infrastructure
CCIP Infrastructure:
CCIP Router (Chain 138) : 0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e
CCIP Sender (Chain 138) : 0x105F8A15b819948a89153505762444Ee9f324684
`;
container.innerHTML = html;
} catch (error) {
container.innerHTML = '
Failed to load bridge data: ' + escapeHtml(error.message) + '
';
}
}
function safeBlockNumber(v) { const n = String(v).replace(/[^0-9]/g, ''); return n ? n : null; }
function safeTxHash(v) { const s = String(v); return /^0x[a-fA-F0-9]{64}$/.test(s) ? s : null; }
function safeAddress(v) {
const s = String(v || '');
if (!/^0x[a-fA-F0-9]{40}$/i.test(s)) return null;
if (/^0x0{40}$/i.test(s)) return null;
return s;
}
async function renderBlockDetail(blockNumber) {
const bn = safeBlockNumber(blockNumber);
if (!bn) { showToast('Invalid block number', 'error'); return; }
blockNumber = bn;
currentDetailKey = 'block:' + blockNumber;
showView('blockDetail');
updatePath('/block/' + blockNumber);
const container = document.getElementById('blockDetail');
updateBreadcrumb('block', blockNumber);
container.innerHTML = createSkeletonLoader('detail');
try {
let b;
// For ChainID 138, use Blockscout API directly
var rawBlockResponse = null;
if (CHAIN_ID === 138) {
try {
var detailResult = await fetchChain138BlockDetail(blockNumber);
rawBlockResponse = detailResult.rawBlockResponse;
b = detailResult.block;
if (!b) {
throw new Error('Block not found');
}
} catch (error) {
container.innerHTML = '
Failed to load block: ' + escapeHtml(error.message || 'Unknown error') + '. Retry
';
return;
}
} else {
const block = await fetchAPIWithRetry(`${API_BASE}/v1/blocks/138/${blockNumber}`);
if (block.data) {
b = block.data;
} else {
throw new Error('Block not found');
}
}
if (b) {
const timestamp = new Date(b.timestamp).toLocaleString();
const gasUsedPercent = b.gas_limit ? ((parseInt(b.gas_used || 0) / parseInt(b.gas_limit)) * 100).toFixed(2) : '0';
const baseFeeGwei = b.base_fee_per_gas ? (parseInt(b.base_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
const burntFeesEth = b.burnt_fees ? formatEther(b.burnt_fees) : '0';
container.innerHTML = `
Block #${b.number}
Export
Hash
${escapeHtml(b.hash || '')}
Parent Hash
${escapeHtml(b.parent_hash || '')}
Timestamp
${escapeHtml(timestamp)}
Miner
${formatAddressWithLabel(b.miner || '') || 'N/A'}
Transaction Count
${b.transaction_count || 0}
Gas Used
${formatNumber(b.gas_used || 0)} / ${formatNumber(b.gas_limit || 0)} (${gasUsedPercent}%)
Gas Limit
${formatNumber(b.gas_limit || 0)}
${b.base_fee_per_gas ? `
Base Fee
${baseFeeGwei} Gwei
` : ''}
${b.burnt_fees && parseInt(b.burnt_fees) > 0 ? `
Burnt Fees
${burntFeesEth} ETH
` : ''}
Size
${formatNumber(b.size || 0)} bytes
Difficulty
${formatNumber(b.difficulty || 0)}
Nonce
${escapeHtml(String(b.nonce || '0x0'))}
${(rawBlockResponse && (rawBlockResponse.consensus !== undefined || rawBlockResponse.finality !== undefined || rawBlockResponse.validated !== undefined)) ? '
Finality / Consensus
' + escapeHtml(String(rawBlockResponse.consensus != null ? rawBlockResponse.consensus : (rawBlockResponse.finality != null ? rawBlockResponse.finality : rawBlockResponse.validated))) + '
' : ''}
`;
} else {
container.innerHTML = '
Block not found
';
}
} catch (error) {
container.innerHTML = '
Failed to load block: ' + escapeHtml(error.message) + '
';
}
}
window._showBlockDetail = renderBlockDetail;
// Keep wrapper (do not overwrite) so all calls go through setTimeout and avoid stack overflow
async function renderTransactionDetail(txHash) {
const th = safeTxHash(txHash);
if (!th) { showToast('Invalid transaction hash', 'error'); return; }
txHash = th;
currentDetailKey = 'tx:' + txHash.toLowerCase();
showView('transactionDetail');
updatePath('/tx/' + txHash);
const container = document.getElementById('transactionDetail');
updateBreadcrumb('transaction', txHash);
container.innerHTML = createSkeletonLoader('detail');
try {
let t;
let rawTx = null;
if (CHAIN_ID === 138) {
try {
var detailResult = await fetchChain138TransactionDetail(txHash);
rawTx = detailResult.rawTransaction;
t = detailResult.transaction;
if (!t) throw new Error('Transaction not found');
} catch (error) {
container.innerHTML = '
Failed to load transaction: ' + escapeHtml(error.message || 'Unknown error') + '. Retry
';
return;
}
} else {
const tx = await fetchAPIWithRetry(`${API_BASE}/v1/transactions/138/${txHash}`);
if (tx.data) t = tx.data;
else throw new Error('Transaction not found');
}
if (!t) {
container.innerHTML = '
Transaction not found
';
return;
}
const timestamp = new Date(t.created_at).toLocaleString();
const valueEth = formatEther(t.value || '0');
const gasPriceGwei = t.gas_price ? (parseInt(t.gas_price) / 1e9).toFixed(2) : 'N/A';
const maxFeeGwei = t.max_fee_per_gas ? (parseInt(t.max_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
const priorityFeeGwei = t.max_priority_fee_per_gas ? (parseInt(t.max_priority_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
const burntFeeEth = t.tx_burnt_fee ? formatEther(t.tx_burnt_fee) : '0';
const totalFee = t.gas_used && t.gas_price ? formatEther((BigInt(t.gas_used) * BigInt(t.gas_price)).toString()) : '0';
const txType = t.type === 2 ? 'EIP-1559' : t.type === 1 ? 'EIP-2930' : 'Legacy';
const revertReason = t.revert_reason || (rawTx && (rawTx.revert_reason || rawTx.error || rawTx.result));
const inputHex = (t.input && t.input !== '0x') ? t.input : null;
const decodedInput = t.decoded_input || (rawTx && rawTx.decoded_input);
const toCellContent = t.to ? formatAddressWithLabel(t.to) + '
' : 'N/A';
let mainHtml = `
Transaction Hash
${escapeHtml(t.hash)}
Status
${t.status === 1 ? 'Success' : t.status === 0 ? 'Failed' : 'Pending'}
Block Number
${escapeHtml(String(t.block_number || 'N/A'))}
Block Hash
${escapeHtml(t.block_hash || 'N/A')}
From
${formatAddressWithLabel(t.from || '')}
Gas Used
${t.gas_used ? formatNumber(t.gas_used) : 'N/A'}
Gas Limit
${t.gas_limit ? formatNumber(t.gas_limit) : 'N/A'}
${t.max_fee_per_gas ? `
Max Fee Per Gas
${maxFeeGwei} Gwei
` : ''}
${t.max_priority_fee_per_gas ? `
Max Priority Fee
${priorityFeeGwei} Gwei
` : ''}
${!t.max_fee_per_gas && t.gas_price ? `
Gas Price
${gasPriceGwei} Gwei
` : ''}
Total Fee
${totalFee} ETH
${t.tx_burnt_fee && parseInt(t.tx_burnt_fee) > 0 ? `
Burnt Fee
${burntFeeEth} ETH
` : ''}
${t.contract_address ? `
Contract Address
${escapeHtml(t.contract_address)}
` : ''}
`;
if (revertReason && t.status !== 1) {
const reasonStr = typeof revertReason === 'string' ? revertReason : (revertReason.message || JSON.stringify(revertReason));
mainHtml += `
Revert Reason
${escapeHtml(reasonStr)}
`;
}
if (inputHex || decodedInput) {
mainHtml += `
Input Data `;
if (decodedInput && (decodedInput.method || decodedInput.params)) {
const method = decodedInput.method || decodedInput.name || 'Unknown';
mainHtml += `
Method: ${escapeHtml(method)}
`;
if (decodedInput.params && Array.isArray(decodedInput.params)) {
mainHtml += '
Param Value ';
decodedInput.params.forEach(function(p) {
const name = (p.name || p.type || '');
const val = typeof p.value !== 'undefined' ? String(p.value) : (p.type || '');
mainHtml += '' + escapeHtml(name) + ' ' + escapeHtml(val) + ' ';
});
mainHtml += '
';
}
}
if (inputHex) {
mainHtml += `
Hex: ${escapeHtml(inputHex)} Copy
`;
}
mainHtml += '
';
}
container.innerHTML = mainHtml;
if (CHAIN_ID === 138) {
const internalCard = document.createElement('div');
internalCard.className = 'card';
internalCard.style.marginTop = '1rem';
internalCard.innerHTML = '
Internal TransactionsLoading...
';
container.appendChild(internalCard);
const logsCard = document.createElement('div');
logsCard.className = 'card';
logsCard.style.marginTop = '1rem';
logsCard.innerHTML = '
Event LogsLoading...
';
container.appendChild(logsCard);
Promise.all([
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/internal-transactions`).catch(function() { return { items: [] }; }),
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/internal_transactions`).catch(function() { return { items: [] }; }),
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/logs`).catch(function() { return { items: [] }; }),
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/log_entries`).catch(function() { return { items: [] }; })
]).then(function(results) {
const internalResp = results[0].items ? results[0] : results[1];
const logsResp = results[2].items ? results[2] : results[3];
const internals = internalResp.items || [];
const logs = logsResp.items || logsResp.log_entries || [];
const internalEl = document.getElementById('txInternalTxs');
if (internalEl) {
const internalFilter = getExplorerPageFilter('txInternalTxs');
const reloadInternalJs = 'showTransactionDetail(\'' + txHash.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')';
const internalFilterBar = renderPageFilterBar('txInternalTxs', 'Filter by type, from, to, or value...', 'Filters the internal transactions below.', reloadInternalJs);
if (internals.length === 0) {
internalEl.innerHTML = internalFilterBar + '
No internal transactions
';
} else {
const filteredInternals = internalFilter ? internals.filter(function(it) {
const from = it.from?.hash || it.from || 'N/A';
const to = it.to?.hash || it.to || 'N/A';
const val = it.value ? formatEther(it.value) : '0';
const type = it.type || it.call_type || 'call';
return matchesExplorerFilter([type, from, to, val].join(' '), internalFilter);
}) : internals;
let tbl = internalFilterBar + '
Type From To Value ';
filteredInternals.forEach(function(it) {
const from = it.from?.hash || it.from || 'N/A';
const to = it.to?.hash || it.to || 'N/A';
const val = it.value ? formatEther(it.value) : '0';
const type = it.type || it.call_type || 'call';
tbl += '' + escapeHtml(type) + ' ' + escapeHtml(shortenHash(from)) + ' ' + escapeHtml(shortenHash(to)) + ' ' + escapeHtml(val) + ' ETH ';
});
if (filteredInternals.length === 0) {
tbl += 'No internal transactions match the current filter. ';
}
tbl += '
';
internalEl.innerHTML = tbl;
}
}
const logsEl = document.getElementById('txLogs');
if (logsEl) {
const logsFilter = getExplorerPageFilter('txLogs');
const reloadLogsJs = 'showTransactionDetail(\'' + txHash.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')';
const logsFilterBar = renderPageFilterBar('txLogs', 'Filter by address, topics, data, or decoded text...', 'Filters the event logs below.', reloadLogsJs);
if (logs.length === 0) {
logsEl.innerHTML = logsFilterBar + '
No event logs
';
} else {
const filteredLogs = logsFilter ? logs.filter(function(log) {
const addr = log.address?.hash || log.address || 'N/A';
const topics = (log.topics && Array.isArray(log.topics)) ? log.topics : (log.topic0 ? [log.topic0] : []);
const topicsStr = topics.join(', ');
const data = log.data || log.raw_data || '0x';
const decoded = log.decoded || log.decoded_text || '';
return matchesExplorerFilter([addr, topicsStr, data, decoded].join(' '), logsFilter);
}) : logs;
let tbl = logsFilterBar + '
Address Topics Data Decoded ';
filteredLogs.forEach(function(log, idx) {
const addr = log.address?.hash || log.address || 'N/A';
const topics = (log.topics && Array.isArray(log.topics)) ? log.topics : (log.topic0 ? [log.topic0] : []);
const topicsStr = topics.join(', ');
const data = log.data || log.raw_data || '0x';
tbl += '' + escapeHtml(shortenHash(addr)) + ' ' + escapeHtml(String(topicsStr).substring(0, 80)) + (String(topicsStr).length > 80 ? '...' : '') + ' ' + escapeHtml(String(data).substring(0, 66)) + (String(data).length > 66 ? '...' : '') + ' — ';
});
if (filteredLogs.length === 0) {
tbl += 'No event logs match the current filter. ';
}
tbl += '
';
logsEl.innerHTML = tbl;
if (typeof ethers !== 'undefined' && ethers.utils) {
(function(logsList, txHash) {
var addrs = [];
logsList.forEach(function(l) { var a = l.address && (l.address.hash || l.address) || l.address; if (a && addrs.indexOf(a) === -1) addrs.push(a); });
var abiCache = {};
Promise.all(addrs.map(function(addr) {
if (!/^0x[a-f0-9]{40}$/i.test(addr)) return Promise.resolve();
return fetch(BLOCKSCOUT_API + '/v2/smart-contracts/' + addr).then(function(r) { return r.json(); }).catch(function() { return null; }).then(function(res) {
var abi = res && (res.abi || res.abi_json);
if (abi) abiCache[addr.toLowerCase()] = Array.isArray(abi) ? abi : (typeof abi === 'string' ? JSON.parse(abi) : abi);
});
})).then(function() {
logsList.forEach(function(log, idx) {
var addr = (log.address && (log.address.hash || log.address)) || log.address;
var topics = log.topics && Array.isArray(log.topics) ? log.topics : (log.topic0 ? [log.topic0] : []);
var data = log.data || log.raw_data || '0x';
var abi = addr ? abiCache[(addr + '').toLowerCase()] : null;
var decodedEl = document.getElementById('txLogDecoded' + idx);
if (!decodedEl || !abi) return;
try {
var iface = new ethers.utils.Interface(abi);
var parsed = iface.parseLog({ topics: topics, data: data });
if (parsed && parsed.name) {
var args = parsed.args && parsed.args.length ? parsed.args.map(function(a) { return String(a); }).join(', ') : '';
decodedEl.textContent = parsed.name + '(' + args + ')';
decodedEl.title = parsed.signature || '';
}
} catch (e) {}
});
});
})(logs, txHash);
}
}
}
});
}
} catch (error) {
container.innerHTML = '
Failed to load transaction: ' + escapeHtml(error.message) + '
';
}
}
function escapeHtml(str) {
if (str == null) return '';
const s = String(str);
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function exportTransactionCSV(txHash) {
fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions/' + txHash).then(function(r) {
var t = normalizeTransaction(r);
if (!t) return;
var rows = [['Field', 'Value'], ['hash', t.hash], ['from', t.from], ['to', t.to || ''], ['value', t.value || '0'], ['block_number', t.block_number || ''], ['status', t.status], ['gas_used', t.gas_used || ''], ['gas_limit', t.gas_limit || '']];
var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
var blob = new Blob([csv], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a'); a.href = url; a.download = 'transaction-' + txHash.substring(0, 10) + '.csv'; a.click();
URL.revokeObjectURL(url);
}).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
}
function exportBlocksCSV() {
Promise.resolve(CHAIN_ID === 138 ? fetchChain138BlocksPage(1, 50) : fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/blocks?page=1&page_size=50').then(function(r) {
return (r.items || r || []).map(normalizeBlock).filter(function(block) { return block !== null; });
})).then(function(items) {
var rows = [['Block', 'Hash', 'Transactions', 'Timestamp']];
items.forEach(function(b) {
var bn = b.height || b.number || b.block_number;
var h = b.hash || b.block_hash || '';
var tc = b.transaction_count || b.transactions_count || 0;
var ts = b.timestamp || '';
rows.push([String(bn), h, String(tc), String(ts)]);
});
var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
var blob = new Blob([csv], { type: 'text/csv' });
var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'blocks.csv'; a.click();
URL.revokeObjectURL(a.href);
}).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
}
function exportTransactionsListCSV() {
Promise.resolve(CHAIN_ID === 138 ? fetchChain138TransactionsPage(1, 50) : fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions?page=1&page_size=50').then(function(r) {
return (r.items || r || []).map(normalizeTransaction).filter(function(tx) { return tx !== null; });
})).then(function(items) {
var rows = [['Hash', 'From', 'To', 'Value', 'Block']];
items.forEach(function(tx) {
var t = tx && tx.hash ? tx : normalizeTransaction(tx);
if (t) rows.push([t.hash || '', t.from || '', t.to || '', t.value || '0', String(t.block_number || '')]);
});
var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
var blob = new Blob([csv], { type: 'text/csv' });
var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'transactions.csv'; a.click();
URL.revokeObjectURL(a.href);
}).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
}
function exportAddressTransactionsCSV(addr) {
if (!addr || !/^0x[a-fA-F0-9]{40}$/i.test(addr)) { showToast('Invalid address', 'error'); return; }
fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions?address=' + encodeURIComponent(addr) + '&page=1&page_size=100').then(function(r) {
var items = r.items || r || [];
var rows = [['Hash', 'From', 'To', 'Value', 'Block', 'Status']];
items.forEach(function(tx) {
var t = normalizeTransaction(tx);
if (t) rows.push([t.hash || '', t.from || '', t.to || '', t.value || '0', String(t.block_number || ''), t.status === 1 ? 'Success' : 'Failed']);
});
var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
var blob = new Blob([csv], { type: 'text/csv' });
var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'address-' + addr.substring(0, 10) + '-transactions.csv'; a.click();
URL.revokeObjectURL(a.href);
showToast('CSV downloaded', 'success');
}).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
}
window.exportAddressTransactionsCSV = exportAddressTransactionsCSV;
function exportAddressTokenBalancesCSV(addr) {
if (!addr || !/^0x[a-fA-F0-9]{40}$/i.test(addr)) { showToast('Invalid address', 'error'); return; }
fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/token-balances').catch(function() { return { items: [] }; }).then(function(r) {
var items = Array.isArray(r) ? r : (r.items || r || []);
var rows = [['Token', 'Contract', 'Balance', 'Type']];
(items || []).forEach(function(b) {
var token = b.token || b;
var contract = token.address?.hash || token.address || b.token_contract_address_hash || 'N/A';
var symbol = token.symbol || token.name || '-';
var balance = b.value || b.balance || '0';
var decimals = token.decimals != null ? token.decimals : 18;
var divisor = Math.pow(10, parseInt(decimals, 10));
var displayBalance = (Number(balance) / divisor).toLocaleString(undefined, { maximumFractionDigits: 6 });
var type = token.type || b.token_type || 'ERC-20';
rows.push([symbol, contract, displayBalance, type]);
});
var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
var blob = new Blob([csv], { type: 'text/csv' });
var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'address-' + addr.substring(0, 10) + '-token-balances.csv'; a.click();
URL.revokeObjectURL(a.href);
showToast('CSV downloaded', 'success');
}).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
}
window.exportAddressTokenBalancesCSV = exportAddressTokenBalancesCSV;
window._showTransactionDetail = renderTransactionDetail;
// Keep wrapper (do not overwrite) so all calls go through setTimeout and avoid stack overflow
async function renderAddressDetail(address) {
const addr = safeAddress(address);
if (!addr) { showToast('Invalid address', 'error'); return; }
address = addr;
currentDetailKey = 'address:' + address.toLowerCase();
showView('addressDetail');
updatePath('/address/' + address);
const container = document.getElementById('addressDetail');
updateBreadcrumb('address', address);
container.innerHTML = createSkeletonLoader('detail');
try {
// Validate address format
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
container.innerHTML = '
Invalid address format
';
return;
}
let a;
// For ChainID 138, use Blockscout API directly
if (CHAIN_ID === 138) {
try {
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/addresses/${address}`);
var raw = response && (response.data !== undefined ? response.data : response.address !== undefined ? response.address : response.items && response.items[0] !== undefined ? response.items[0] : response);
a = normalizeAddress(raw);
if (!a || !a.hash) {
throw new Error('Address not found');
}
} catch (error) {
var retryAddress = String(address || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
container.innerHTML = '
Failed to load address: ' + escapeHtml(error.message || 'Unknown error') + '. Retry
';
return;
}
} else {
const addr = await fetchAPIWithRetry(`${API_BASE}/v1/addresses/138/${address}`);
if (addr.data) {
a = addr.data;
} else {
throw new Error('Address not found');
}
}
if (a) {
const balanceEth = formatEther(a.balance || '0');
const isContract = !!a.is_contract;
const verifiedBadge = a.is_verified ? '
Verified ' : '';
const encodedAddress = encodeURIComponent(address);
const escapedAddress = escapeHtml(address);
const addressForJs = address.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const contractLink = isContract ? `
View contract on Blockscout ` : '';
const savedLabel = getAddressLabel(address);
const inWatchlist = isInWatchlist(address);
container.innerHTML = `
Address
${escapedAddress}
Watchlist
${inWatchlist ? 'Remove from watchlist' : 'Add to watchlist'}
Balance
${balanceEth} ETH
Transaction Count
${a.transaction_count || 0}
Token Count
${a.token_count || 0}
Type
${a.is_contract ? 'Contract ' + verifiedBadge + (contractLink ? ' ' + contractLink : '') : 'EOA '}
${a.creation_tx_hash ? `
Contract created in
${escapeHtml(shortenHash(a.creation_tx_hash))}
` : ''}
${a.first_seen_at ? `
First seen
${escapeHtml(typeof a.first_seen_at === 'string' ? a.first_seen_at : new Date(a.first_seen_at).toISOString())}
` : ''}
${a.last_seen_at ? `
Last seen
${escapeHtml(typeof a.last_seen_at === 'string' ? a.last_seen_at : new Date(a.last_seen_at).toISOString())}
` : ''}
Transactions
Token Balances
Internal Txns
NFTs
${isContract ? 'Contract (ABI / Bytecode) ' : ''}
Recent Transactions
Export CSV
Loading transactions...
Token Balances
Export CSV
Loading...
Internal Transactions
Loading...
${isContract ? '
Contract ABI & Bytecode Loading...
' : ''}
`;
function switchAddressTab(tabName, addr) {
document.querySelectorAll('.address-tab-content').forEach(function(el) { el.style.display = 'none'; });
document.querySelectorAll('.tabs .tab').forEach(function(t) { t.classList.remove('active'); });
if (tabName === 'transactions') {
document.getElementById('addressTabTransactions').style.display = 'block';
document.getElementById('addrTabTxs').classList.add('active');
} else if (tabName === 'tokens') {
document.getElementById('addressTabTokens').style.display = 'block';
document.getElementById('addrTabTokens').classList.add('active');
loadAddressTokenBalances(addr);
} else if (tabName === 'internal') {
document.getElementById('addressTabInternal').style.display = 'block';
document.getElementById('addrTabInternal').classList.add('active');
loadAddressInternalTxns(addr);
} else if (tabName === 'nfts') {
document.getElementById('addressTabNfts').style.display = 'block';
document.getElementById('addrTabNfts').classList.add('active');
loadAddressNftInventory(addr);
} else if (tabName === 'contract') {
var contractPanel = document.getElementById('addressTabContract');
var contractTab = document.getElementById('addrTabContract');
if (contractPanel && contractTab) {
contractPanel.style.display = 'block';
contractTab.classList.add('active');
loadAddressContractInfo(addr);
}
}
}
window.switchAddressTab = switchAddressTab;
async function loadAddressTokenBalances(addr) {
const el = document.getElementById('addressTokenBalances');
if (!el || el.dataset.loaded === '1') return;
try {
const r = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/token-balances').catch(function() { return { items: [] }; });
const r2 = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/token_balances').catch(function() { return { items: [] }; });
const items = (r.items || r).length ? (r.items || r) : (r2.items || r2);
el.dataset.loaded = '1';
const filter = getExplorerPageFilter('addressTokenBalances');
const reloadJs = 'showAddressDetail(\'' + addr.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')';
const filterBar = renderPageFilterBar('addressTokenBalances', 'Filter by token name, contract, balance, or type...', 'Filters the token balances below.', reloadJs);
if (!items || items.length === 0) {
el.innerHTML = filterBar + '
No token balances
';
return;
}
const filteredItems = filter ? items.filter(function(b) {
const token = b.token || b;
const contract = token.address?.hash || token.address || b.token_contract_address_hash || 'N/A';
const symbol = token.symbol || token.name || '-';
const balance = b.value || b.balance || '0';
const decimals = token.decimals != null ? token.decimals : 18;
const divisor = Math.pow(10, parseInt(decimals, 10));
const displayBalance = (Number(balance) / divisor).toLocaleString(undefined, { maximumFractionDigits: 6 });
const type = token.type || b.token_type || 'ERC-20';
return matchesExplorerFilter([symbol, contract, displayBalance, type].join(' '), filter);
}) : items;
let tbl = filterBar + '
Token Contract Balance Type ';
filteredItems.forEach(function(b) {
const token = b.token || b;
const contract = token.address?.hash || token.address || b.token_contract_address_hash || 'N/A';
const symbol = token.symbol || token.name || '-';
const balance = b.value || b.balance || '0';
const decimals = token.decimals != null ? token.decimals : 18;
const divisor = Math.pow(10, parseInt(decimals, 10));
const displayBalance = (Number(balance) / divisor).toLocaleString(undefined, { maximumFractionDigits: 6 });
const type = token.type || b.token_type || 'ERC-20';
tbl += '' + escapeHtml(symbol) + ' ' + escapeHtml(shortenHash(contract)) + ' ' + escapeHtml(displayBalance) + ' ' + escapeHtml(type) + ' ';
});
if (filteredItems.length === 0) {
tbl += 'No token balances match the current filter. ';
}
tbl += '
';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '
Failed to load token balances
';
}
}
async function loadAddressNftInventory(addr) {
const el = document.getElementById('addressNftInventory');
if (!el || el.dataset.loaded === '1') return;
try {
const r = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/token-balances').catch(function() { return { items: [] }; });
const r2 = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/nft-inventory').catch(function() { return { items: [] }; });
const r3 = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/nft_tokens').catch(function() { return { items: [] }; });
var items = (r2.items || r2).length ? (r2.items || r2) : (r3.items || r3);
if (!items || items.length === 0) {
var allBalances = (r.items || r) || [];
items = Array.isArray(allBalances) ? allBalances.filter(function(b) {
var t = (b.token || b).type || (b.token_type || '');
return t === 'ERC-721' || t === 'ERC-1155' || String(t).toLowerCase().indexOf('nft') !== -1;
}) : [];
}
el.dataset.loaded = '1';
const filter = getExplorerPageFilter('addressNftInventory');
const reloadJs = 'showAddressDetail(\'' + addr.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')';
const filterBar = renderPageFilterBar('addressNftInventory', 'Filter by contract, token ID, name, symbol, or balance...', 'Filters the NFT inventory below.', reloadJs);
if (!items || items.length === 0) {
el.innerHTML = filterBar + '
No NFT tokens
';
return;
}
const filteredItems = filter ? items.filter(function(b) {
var token = b.token || b;
var contract = token.address?.hash || token.address || b.token_contract_address_hash || b.contract_address_hash || 'N/A';
var tokenId = b.token_id != null ? b.token_id : (b.tokenId != null ? b.tokenId : (b.id != null ? b.id : '-'));
var name = token.name || token.symbol || '-';
var balance = b.value != null ? b.value : (b.balance != null ? b.balance : '1');
return matchesExplorerFilter([contract, tokenId, name, balance].join(' '), filter);
}) : items;
var tbl = filterBar + '
Contract Token ID Name / Symbol Balance ';
filteredItems.forEach(function(b) {
var token = b.token || b;
var contract = token.address?.hash || token.address || b.token_contract_address_hash || b.contract_address_hash || 'N/A';
var tokenId = b.token_id != null ? b.token_id : (b.tokenId != null ? b.tokenId : (b.id != null ? b.id : '-'));
var name = token.name || token.symbol || '-';
var balance = b.value != null ? b.value : (b.balance != null ? b.balance : '1');
tbl += '' + escapeHtml(shortenHash(contract)) + ' ';
tbl += '' + (tokenId !== '-' ? '' + escapeHtml(String(tokenId)) + ' ' : '-') + ' ';
tbl += '' + escapeHtml(name) + ' ' + escapeHtml(String(balance)) + ' ';
});
if (filteredItems.length === 0) {
tbl += 'No NFT inventory matches the current filter. ';
}
tbl += '
';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '
Failed to load NFT inventory
';
}
}
async function loadAddressInternalTxns(addr) {
const el = document.getElementById('addressInternalTxns');
if (!el || el.dataset.loaded === '1') return;
try {
const r = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/internal-transactions').catch(function() { return { items: [] }; });
const r2 = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/internal_transactions').catch(function() { return { items: [] }; });
const items = (r.items || r).length ? (r.items || r) : (r2.items || r2);
el.dataset.loaded = '1';
const filter = getExplorerPageFilter('addressInternalTxns');
const reloadJs = 'showAddressDetail(\'' + addr.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')';
const filterBar = renderPageFilterBar('addressInternalTxns', 'Filter by block, from, to, tx hash, or value...', 'Filters the internal transactions below.', reloadJs);
if (!items || items.length === 0) {
el.innerHTML = filterBar + '
No internal transactions
';
return;
}
const slicedItems = items.slice(0, 25);
const filteredItems = filter ? slicedItems.filter(function(it) {
const from = it.from?.hash || it.from || 'N/A';
const to = it.to?.hash || it.to || 'N/A';
const val = it.value ? formatEther(it.value) : '0';
const block = it.block_number || it.block || '-';
const txHash = it.transaction_hash || it.tx_hash || '-';
return matchesExplorerFilter([block, from, to, val, txHash].join(' '), filter);
}) : slicedItems;
let tbl = filterBar + '
Block From To Value Tx Hash ';
filteredItems.forEach(function(it) {
const from = it.from?.hash || it.from || 'N/A';
const to = it.to?.hash || it.to || 'N/A';
const val = it.value ? formatEther(it.value) : '0';
const block = it.block_number || it.block || '-';
const txHash = it.transaction_hash || it.tx_hash || '-';
tbl += '' + escapeHtml(block) + ' ' + escapeHtml(shortenHash(from)) + ' ' + escapeHtml(shortenHash(to)) + ' ' + escapeHtml(val) + ' ETH ' + (txHash !== '-' ? escapeHtml(shortenHash(txHash)) : '-') + ' ';
});
if (filteredItems.length === 0) {
tbl += 'No internal transactions match the current filter. ';
}
tbl += '
';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '
Failed to load internal transactions
';
}
}
async function loadAddressContractInfo(addr) {
const el = document.getElementById('addressContractInfo');
if (!el || el.dataset.loaded === '1') return;
try {
const urls = [
BLOCKSCOUT_API + '/v2/smart-contracts/' + addr,
BLOCKSCOUT_API + '/v2/contracts/' + addr
];
let data = null;
for (var i = 0; i < urls.length; i++) {
try {
const r = await fetchAPIWithRetry(urls[i]);
if (r && (r.abi || r.bytecode || r.deployed_bytecode)) { data = r; break; }
} catch (e) {}
}
el.dataset.loaded = '1';
if (!data) {
el.innerHTML = '
Contract source not indexed. Verify on Blockscout
';
return;
}
const abi = data.abi || data.abi_interface || [];
const abiStr = typeof abi === 'string' ? abi : JSON.stringify(abi, null, 2);
const bytecode = data.bytecode || data.deployed_bytecode || data.creation_bytecode || '-';
let html = '
Verification: ' + (data.is_verified ? 'Verified ' : 'Unverified ') + '
';
html += '
';
html += '
' + escapeHtml(abiStr) + ' ';
html += '
Bytecode Copy ';
html += '
' + escapeHtml(String(bytecode).substring(0, 500)) + (String(bytecode).length > 500 ? '...' : '') + ' ';
var viewFns = (Array.isArray(abi) ? abi : []).filter(function(x) { return x.type === 'function' && (x.stateMutability === 'view' || x.stateMutability === 'pure' || (x.constant === true)); });
if (viewFns.length > 0) {
html += '
Read contractCall view/pure functions (requires ethers.js).
';
html += '
Function: ';
viewFns.forEach(function(fn) { html += '' + escapeHtml(fn.name) + '(' + (fn.inputs || []).map(function(i) { return i.type; }).join(',') + ') '; });
html += '
';
html += '
Query ';
}
var writeFns = (Array.isArray(abi) ? abi : []).filter(function(x) { return x.type === 'function' && x.stateMutability !== 'view' && x.stateMutability !== 'pure' && !x.constant; });
if (writeFns.length > 0) {
html += '
Write contractConnect wallet to send transactions.
';
html += '
Function: ';
writeFns.forEach(function(fn) { var pay = (fn.stateMutability === 'payable'); html += '' + escapeHtml(fn.name) + '(' + (fn.inputs || []).map(function(i) { return i.type; }).join(',') + ')' + (pay ? ' payable' : '') + ' '; });
html += '
';
html += '
Value (ETH):
';
html += '
Write ';
}
html += '
Read / Write contract on Blockscout
';
el.innerHTML = html;
if (viewFns.length > 0) {
(function setupReadContract(contractAddr, abiJson, viewFunctions) {
var selectEl = document.getElementById('readContractSelect');
var inputsEl = document.getElementById('readContractInputs');
var resultEl = document.getElementById('readContractResult');
var btnEl = document.getElementById('readContractBtn');
function renderInputs() {
var name = selectEl && selectEl.value;
var fn = viewFunctions.find(function(f) { return f.name === name; });
if (!inputsEl || !fn) return;
var inputs = fn.inputs || [];
if (inputs.length === 0) { inputsEl.innerHTML = ''; return; }
var h = '
';
inputs.forEach(function(inp, i) {
h += '' + escapeHtml(inp.name || 'arg' + i) + ' (' + escapeHtml(inp.type) + '): ';
});
h += '
';
inputsEl.innerHTML = h;
}
if (selectEl) selectEl.addEventListener('change', renderInputs);
renderInputs();
if (btnEl) btnEl.addEventListener('click', function() {
if (typeof ethers === 'undefined') { showToast('Ethers.js not loaded. Refresh the page.', 'error'); return; }
var name = selectEl && selectEl.value;
var fn = viewFunctions.find(function(f) { return f.name === name; });
if (!fn || !resultEl) return;
var inputs = fn.inputs || [];
var args = [];
for (var i = 0; i < inputs.length; i++) {
var inp = inputs[i];
var val = document.getElementById('readArg' + i) && document.getElementById('readArg' + i).value;
if (val === undefined || val === '') val = '';
var t = (inp.type || '').toLowerCase();
if (t.indexOf('uint') === 0 || t === 'int256') args.push(val ? (val.trim() ? BigInt(val) : 0) : 0);
else if (t === 'bool') args.push(val === 'true' || val === '1');
else if (t.indexOf('address') === 0) args.push(val && val.trim() ? val.trim() : '0x0000000000000000000000000000000000000000');
else args.push(val);
}
resultEl.style.display = 'block';
resultEl.textContent = 'Calling...';
var provider = new ethers.providers.JsonRpcProvider(RPC_URL);
var contract = new ethers.Contract(contractAddr, abiJson, provider);
contract[name].apply(contract, args).then(function(res) {
if (Array.isArray(res)) resultEl.textContent = JSON.stringify(res, null, 2);
else if (res != null && typeof res.toString === 'function') resultEl.textContent = res.toString();
else resultEl.textContent = JSON.stringify(res, null, 2);
}).catch(function(err) {
resultEl.textContent = 'Error: ' + (err.message || String(err));
});
});
})(addr, abi, viewFns);
}
if (writeFns.length > 0) {
(function setupWriteContract(contractAddr, abiJson, writeFunctions) {
var selectEl = document.getElementById('writeContractSelect');
var inputsEl = document.getElementById('writeContractInputs');
var valueRow = document.getElementById('writeContractValueRow');
var valueEl = document.getElementById('writeContractValue');
var resultEl = document.getElementById('writeContractResult');
var btnEl = document.getElementById('writeContractBtn');
function renderWriteInputs() {
var name = selectEl && selectEl.value;
var opt = selectEl && selectEl.options[selectEl.selectedIndex];
var payable = opt && opt.getAttribute('data-payable') === 'true';
if (valueRow) valueRow.style.display = payable ? 'block' : 'none';
var fn = writeFunctions.find(function(f) { return f.name === name; });
if (!inputsEl || !fn) return;
var inputs = fn.inputs || [];
var h = '
';
inputs.forEach(function(inp, i) {
h += '' + escapeHtml(inp.name || 'arg' + i) + ' (' + escapeHtml(inp.type) + '): ';
});
h += '
';
inputsEl.innerHTML = h;
}
if (selectEl) selectEl.addEventListener('change', renderWriteInputs);
renderWriteInputs();
if (btnEl) btnEl.addEventListener('click', function() {
if (typeof ethers === 'undefined') { showToast('Ethers.js not loaded. Refresh the page.', 'error'); return; }
if (!window.ethereum) { showToast('Connect MetaMask to write.', 'error'); return; }
var name = selectEl && selectEl.value;
var fn = writeFunctions.find(function(f) { return f.name === name; });
if (!fn || !resultEl) return;
var inputs = fn.inputs || [];
var args = [];
for (var i = 0; i < inputs.length; i++) {
var inp = inputs[i];
var val = document.getElementById('writeArg' + i) && document.getElementById('writeArg' + i).value;
if (val === undefined || val === '') val = '';
var t = (inp.type || '').toLowerCase();
if (t.indexOf('uint') === 0 || t === 'int256') args.push(val ? (val.trim() ? BigInt(val) : 0) : 0);
else if (t === 'bool') args.push(val === 'true' || val === '1');
else if (t.indexOf('address') === 0) args.push(val && val.trim() ? val.trim() : '0x0000000000000000000000000000000000000000');
else args.push(val);
}
var valueWei = '0';
if (fn.stateMutability === 'payable' && valueEl && valueEl.value) {
try { valueWei = ethers.utils.parseEther(valueEl.value.trim() || '0').toString(); } catch (e) { showToast('Invalid ETH value', 'error'); return; }
}
resultEl.style.display = 'block';
resultEl.textContent = 'Confirm in wallet...';
var provider = new ethers.providers.Web3Provider(window.ethereum);
provider.send('eth_requestAccounts', []).then(function() {
var signer = provider.getSigner();
var contract = new ethers.Contract(contractAddr, abiJson, signer);
var overrides = valueWei !== '0' ? { value: valueWei } : {};
return contract[name].apply(contract, args.concat([overrides]));
}).then(function(tx) {
resultEl.textContent = 'Tx hash: ' + tx.hash + '\nWaiting for confirmation...';
return tx.wait();
}).then(function(receipt) {
resultEl.textContent = 'Success. Block: ' + receipt.blockNumber + ', Tx: ' + receipt.transactionHash;
showToast('Transaction confirmed', 'success');
}).catch(function(err) {
resultEl.textContent = 'Error: ' + (err.message || String(err));
showToast(err.message || 'Transaction failed', 'error');
});
});
})(addr, abi, writeFns);
}
} catch (e) {
el.innerHTML = '
Failed to load contract info
';
}
}
try {
let txs;
if (CHAIN_ID === 138) {
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?address=${address}&page=1&page_size=10`);
const rawTxs = response.items || [];
txs = { data: rawTxs.map(normalizeTransaction).filter(tx => tx !== null) };
} else {
txs = await fetchAPIWithRetry(`${API_BASE}/v1/transactions?from_address=${address}&page_size=10`);
}
const txContainer = document.getElementById('addressTransactions');
if (txContainer) {
const filter = getExplorerPageFilter('addressTransactions');
const reloadJs = 'showAddressDetail(\'' + address.replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')';
const filterBar = renderPageFilterBar('addressTransactions', 'Filter by hash, block, from, to, or value...', 'Filters the recent transactions below.', reloadJs);
if (txs.data && txs.data.length > 0) {
const filteredTxs = filter ? txs.data.filter(function(tx) {
return matchesExplorerFilter([tx.hash || '', tx.block_number || '', tx.from || '', tx.to || '', tx.value || '0'].join(' '), filter);
}) : txs.data;
let txHtml = filterBar + '
Hash Block From To Value ';
filteredTxs.forEach(function(tx) {
txHtml += '' + escapeHtml(shortenHash(tx.hash)) + ' ' + escapeHtml(String(tx.block_number)) + ' ' + formatAddressWithLabel(tx.from) + ' ' + (tx.to ? formatAddressWithLabel(tx.to) : 'N/A') + ' ' + escapeHtml(formatEther(tx.value || '0')) + ' ETH ';
});
if (filteredTxs.length === 0) {
txHtml += 'No transactions match the current filter. ';
}
txHtml += '
';
txContainer.innerHTML = txHtml;
} else {
txContainer.innerHTML = filterBar + '
No transactions found
';
}
}
} catch (e) {
const txContainer = document.getElementById('addressTransactions');
if (txContainer) txContainer.innerHTML = '
Failed to load transactions
';
}
} else {
container.innerHTML = '
Address not found
';
}
} catch (error) {
container.innerHTML = '
Failed to load address: ' + escapeHtml(error.message) + '
';
}
}
window._showAddressDetail = renderAddressDetail;
// Keep wrapper (do not overwrite) so all calls go through setTimeout and avoid stack overflow
async function showTokenDetail(tokenAddress) {
if (!/^0x[a-fA-F0-9]{40}$/.test(tokenAddress)) return;
currentDetailKey = 'token:' + tokenAddress.toLowerCase();
showView('tokenDetail');
updatePath('/token/' + tokenAddress);
var container = document.getElementById('tokenDetail');
updateBreadcrumb('token', tokenAddress);
container.innerHTML = createSkeletonLoader('detail');
try {
var urls = [BLOCKSCOUT_API + '/v2/tokens/' + tokenAddress, BLOCKSCOUT_API + '/v2/token/' + tokenAddress];
var data = null;
for (var i = 0; i < urls.length; i++) {
try {
var r = await fetchAPIWithRetry(urls[i]);
if (r && (r.symbol || r.name || r.total_supply != null)) { data = r; break; }
} catch (e) {}
}
if (!data) {
container.innerHTML = '
Token not found or not indexed.
View as address
';
return;
}
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, 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 addrEsc = tokenAddress.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
var symbolEsc = String(symbol).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
var html = '
Add to wallet (MetaMask)
';
html += '
Contract
' + escapeHtml(tokenAddress) + '
';
html += '
Name
' + escapeHtml(name) + '
';
html += '
Symbol
' + escapeHtml(symbol) + '
';
html += '
';
html += '
Total Supply
' + supplyNum.toLocaleString(undefined, { maximumFractionDigits: 6 }) + '
';
html += '
Holders
' + (holders !== '-' ? formatNumber(holders) : '-') + '
';
const transfersFilter = getExplorerPageFilter('tokenTransfers');
const transfersFilterBar = renderPageFilterBar('tokenTransfers', 'Filter by from, to, value, or tx hash...', 'Filters the recent transfers below.', 'showTokenDetail(\'' + addrEsc + '\')');
html += '
Recent Transfers ';
if (transfers.length === 0) {
html += transfersFilterBar + '
No transfers
';
} else {
const filteredTransfers = transfersFilter ? transfers.filter(function(tr) {
var from = tr.from?.hash || tr.from || '-';
var to = tr.to?.hash || tr.to || '-';
var val = tr.total?.value != null ? tr.total.value : (tr.value || '0');
var dec = tr.token?.decimals != null ? tr.token.decimals : decimals;
var v = Number(val) / Math.pow(10, parseInt(dec, 10));
var txHash = tr.transaction_hash || tr.tx_hash || '';
return matchesExplorerFilter([from, to, v.toLocaleString(undefined, { maximumFractionDigits: 6 }), txHash].join(' '), transfersFilter);
}) : transfers;
html += transfersFilterBar + '
From To Value Tx ';
filteredTransfers.forEach(function(tr) {
var from = tr.from?.hash || tr.from || '-';
var to = tr.to?.hash || tr.to || '-';
var val = tr.total?.value != null ? tr.total.value : (tr.value || '0');
var dec = tr.token?.decimals != null ? tr.token.decimals : decimals;
var v = Number(val) / Math.pow(10, parseInt(dec, 10));
var txHash = tr.transaction_hash || tr.tx_hash || '';
html += '' + escapeHtml(shortenHash(from)) + ' ' + escapeHtml(shortenHash(to)) + ' ' + escapeHtml(v.toLocaleString(undefined, { maximumFractionDigits: 6 })) + ' ' + (txHash ? escapeHtml(shortenHash(txHash)) : '-') + ' ';
});
if (filteredTransfers.length === 0) {
html += 'No transfers match the current filter. ';
}
html += '
';
}
html += '
';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '
Failed to load token: ' + escapeHtml(err.message || 'Unknown') + '
';
}
}
window.showTokenDetail = showTokenDetail;
async function showNftDetail(contractAddress, tokenId) {
if (!/^0x[a-fA-F0-9]{40}$/.test(contractAddress)) return;
currentDetailKey = 'nft:' + contractAddress.toLowerCase() + ':' + tokenId;
showView('nftDetail');
updatePath('/nft/' + contractAddress + '/' + tokenId);
var container = document.getElementById('nftDetail');
updateBreadcrumb('nft', contractAddress, tokenId);
container.innerHTML = createSkeletonLoader('detail');
try {
var urls = [BLOCKSCOUT_API + '/v2/tokens/' + contractAddress + '/nft/' + tokenId, BLOCKSCOUT_API + '/v2/nft/' + contractAddress + '/' + tokenId];
var data = null;
for (var i = 0; i < urls.length; i++) {
try {
var r = await fetchAPIWithRetry(urls[i]);
if (r) { data = r; break; }
} catch (e) {}
}
var html = '
Contract
' + escapeHtml(contractAddress) + '
';
html += '
Token ID
' + escapeHtml(String(tokenId)) + '
';
if (data) {
if (data.metadata && data.metadata.image) {
html += '
';
}
if (data.name) html += '
Name
' + escapeHtml(data.name) + '
';
if (data.description) html += '
Description
' + escapeHtml(data.description) + '
';
if (data.owner) { var ownerAddr = (data.owner.hash || data.owner); html += '
Owner
' + escapeHtml(ownerAddr) + '
'; }
if (data.metadata && data.metadata.attributes && Array.isArray(data.metadata.attributes)) {
html += '
Traits
';
data.metadata.attributes.forEach(function(attr) {
var traitType = (attr.trait_type || attr.traitType || ''); var val = (attr.value != null ? attr.value : '');
if (traitType || val) html += '' + escapeHtml(traitType) + ': ' + escapeHtml(String(val)) + ' ';
});
html += '
';
}
}
html += '
View on Blockscout
';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '
Failed to load NFT: ' + escapeHtml(err.message || 'Unknown') + '
';
}
}
window.showNftDetail = showNftDetail;
function showSearchResultsList(items, query) {
showView('searchResults');
var container = document.getElementById('searchResultsContent');
if (!container) return;
var html = '
Found ' + items.length + ' result(s) for "' + escapeHtml(query) + '". Click a row to open.
';
html += '
Type Value ';
items.forEach(function(item) {
var type = (item.type || item.address_type || '').toLowerCase();
var label = item.name || item.symbol || item.address_hash || item.hash || item.tx_hash || (item.block_number != null ? 'Block #' + item.block_number : '') || '-';
var addr, txHash, blockNum, tokenAddr;
if (item.token_address || item.token_contract_address_hash) {
tokenAddr = item.token_address || item.token_contract_address_hash;
if (/^0x[a-f0-9]{40}$/i.test(tokenAddr)) {
html += 'Token ' + escapeHtml(shortenHash(tokenAddr)) + ' ' + (item.name || item.symbol ? ' (' + escapeHtml(item.name || item.symbol) + ')' : '') + ' ';
return;
}
}
if (item.address_hash || item.hash) {
addr = item.address_hash || item.hash;
if (/^0x[a-f0-9]{40}$/i.test(addr)) {
html += 'Address ' + escapeHtml(shortenHash(addr)) + ' ';
return;
}
}
if (item.tx_hash || (item.hash && item.hash.length === 66)) {
txHash = item.tx_hash || item.hash;
if (/^0x[a-f0-9]{64}$/i.test(txHash)) {
html += 'Transaction ' + escapeHtml(shortenHash(txHash)) + ' ';
return;
}
}
if (item.block_number != null) {
blockNum = String(item.block_number);
html += 'Block #' + escapeHtml(blockNum) + ' ';
return;
}
html += '' + escapeHtml(type || 'Unknown') + ' ' + escapeHtml(String(label).substring(0, 80)) + ' ';
});
html += '
';
container.innerHTML = html;
}
window.showSearchResultsList = showSearchResultsList;
async function handleSearch(query) {
query = query.trim();
if (!query) {
showToast('Please enter a search query', 'info');
return;
}
saveSmartSearchHistory(query);
closeSmartSearchModal();
const normalizedQuery = query.toLowerCase().replace(/\s/g, '');
try {
if (/^0x[a-f0-9]{40}$/i.test(normalizedQuery)) {
await showAddressDetail(normalizedQuery);
return;
}
if (/^0x[a-f0-9]{64}$/i.test(normalizedQuery)) {
await showTransactionDetail(normalizedQuery);
return;
}
if (/^\d+$/.test(query)) {
await showBlockDetail(query);
return;
}
if (/^0x[a-f0-9]+$/i.test(normalizedQuery)) {
const blockNum = parseInt(normalizedQuery, 16);
if (!isNaN(blockNum)) {
await showBlockDetail(blockNum.toString());
return;
}
}
if (CHAIN_ID === 138) {
var searchResults = null;
try {
searchResults = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/search?q=' + encodeURIComponent(query));
} catch (e) {}
if (searchResults && searchResults.items && searchResults.items.length > 0) {
showSearchResultsList(searchResults.items, query);
return;
}
if (/^0x[a-f0-9]{8,64}$/i.test(normalizedQuery)) {
try {
var txResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions/' + normalizedQuery);
if (txResp && (txResp.hash || txResp.tx_hash)) {
var fullHash = txResp.hash || txResp.tx_hash;
await showTransactionDetail(fullHash);
return;
}
} catch (e) {}
showToast('No unique result for partial hash. Use at least 0x + 8 hex characters, or full tx hash (0x + 64 hex).', 'info');
return;
}
}
showToast('Invalid search. Try address (0x...40 hex), tx hash (0x...64 hex or 0x+8 hex), block number, or token/contract name.', 'error');
} catch (error) {
console.error('Search error:', error);
showToast('Failed to load search results: ' + (error.message || 'Unknown error'), 'error');
}
}
window.handleSearch = handleSearch;
function getTimeAgo(date) {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return 'N/A';
}
const now = new Date();
const diffMs = now - date;
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) {
return `${diffSecs}s ago`;
} else if (diffMins < 60) {
return `${diffMins}m ago`;
} else if (diffHours < 24) {
return `${diffHours}h ago`;
} else if (diffDays < 7) {
return `${diffDays}d ago`;
} else {
return date.toLocaleDateString();
}
}
function formatNumber(num) {
return parseInt(num || 0).toLocaleString();
}
function shortenHash(hash, length = 10) {
if (!hash) return 'N/A';
// Convert to string if it's not already
const hashStr = String(hash);
if (hashStr.length <= length * 2 + 2) return hashStr;
return hashStr.substring(0, length + 2) + '...' + hashStr.substring(hashStr.length - length);
}
function formatEther(wei, unit = 'ether') {
if (typeof wei === 'string' && wei.startsWith('0x')) {
wei = BigInt(wei);
}
const weiNum = typeof wei === 'bigint' ? Number(wei) : parseFloat(wei);
const ether = weiNum / Math.pow(10, unit === 'gwei' ? 9 : 18);
return ether.toFixed(6).replace(/\.?0+$/, '');
}
function getExplorerAIPageContext() {
return {
path: (window.location && window.location.pathname) ? window.location.pathname : '/',
view: currentView || 'home'
};
}
function renderExplorerAIMessages() {
var list = document.getElementById('explorerAIMessageList');
var status = document.getElementById('explorerAIStatus');
if (!list) return;
list.innerHTML = _explorerAIState.messages.map(function(message) {
var isAssistant = message.role === 'assistant';
var bubbleStyle = isAssistant
? 'background: rgba(37,99,235,0.10); border:1px solid rgba(37,99,235,0.18);'
: 'background: rgba(15,23,42,0.06); border:1px solid rgba(148,163,184,0.25);';
return '
' +
'
' +
'
' + (isAssistant ? 'Explorer AI' : 'You') + '
' +
'
' + escapeHtml(message.content || '') + '
' +
'
' +
'
';
}).join('');
if (_explorerAIState.loading) {
list.innerHTML += '
Thinking through indexed data, live routes, and docs...
';
}
list.scrollTop = list.scrollHeight;
if (status) {
status.textContent = _explorerAIState.loading
? 'Querying explorer data and the model...'
: 'Read-only assistant using indexed explorer data, route APIs, and curated docs.';
}
}
function setExplorerAIOpen(open) {
_explorerAIState.open = !!open;
var panel = document.getElementById('explorerAIPanel');
var button = document.getElementById('explorerAIFab');
if (panel) panel.style.display = open ? 'flex' : 'none';
if (button) button.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) {
renderExplorerAIMessages();
var input = document.getElementById('explorerAIInput');
if (input) setTimeout(function() { input.focus(); }, 30);
}
}
function toggleExplorerAIPanel(forceOpen) {
if (typeof forceOpen === 'boolean') {
setExplorerAIOpen(forceOpen);
return;
}
setExplorerAIOpen(!_explorerAIState.open);
}
window.toggleExplorerAIPanel = toggleExplorerAIPanel;
function buildExplorerAISourceSummary(context) {
if (!context || !Array.isArray(context.sources) || !context.sources.length) return '';
return context.sources.map(function(source) {
return source.label || source.type || 'source';
}).filter(Boolean).join(' | ');
}
async function submitExplorerAIMessage(prefill) {
var input = document.getElementById('explorerAIInput');
var raw = typeof prefill === 'string' ? prefill : (input ? input.value : '');
var question = String(raw || '').trim();
if (!question || _explorerAIState.loading) return;
_explorerAIState.messages.push({ role: 'user', content: question });
if (input) input.value = '';
_explorerAIState.loading = true;
renderExplorerAIMessages();
try {
var payload = {
messages: _explorerAIState.messages.slice(-8),
pageContext: getExplorerAIPageContext()
};
var response = await postJSON(EXPLORER_AI_API_BASE + '/chat', payload);
var reply = (response && response.reply) ? String(response.reply) : 'No reply returned.';
var sourceSummary = buildExplorerAISourceSummary(response && response.context);
if (sourceSummary) {
reply += '\n\nSources: ' + sourceSummary;
}
if (response && Array.isArray(response.warnings) && response.warnings.length) {
reply += '\n\nWarnings: ' + response.warnings.join(' | ');
}
_explorerAIState.messages.push({ role: 'assistant', content: reply });
} catch (error) {
_explorerAIState.messages.push({
role: 'assistant',
content: 'Explorer AI could not complete that request.\n\n' + (error && error.message ? error.message : 'Unknown error') + '\n\nIf this is production, confirm the backend has OPENAI_API_KEY and TOKEN_AGGREGATION_API_BASE configured.'
});
} finally {
_explorerAIState.loading = false;
renderExplorerAIMessages();
}
}
window.submitExplorerAIMessage = submitExplorerAIMessage;
function initExplorerAIPanel() {
if (document.getElementById('explorerAIPanel') || !document.body) return;
var style = document.createElement('style');
style.textContent = `
#explorerAIFab {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 20010;
border: 0;
border-radius: 999px;
padding: 0.9rem 1rem;
background: linear-gradient(135deg, #0f172a, #2563eb);
color: #fff;
box-shadow: 0 16px 36px rgba(15,23,42,0.28);
cursor: pointer;
font-weight: 700;
letter-spacing: 0.02em;
}
#explorerAIPanel {
position: fixed;
right: 20px;
bottom: 84px;
width: min(420px, calc(100vw - 24px));
height: min(72vh, 680px);
display: none;
flex-direction: column;
z-index: 20010;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 22px;
box-shadow: 0 24px 60px rgba(15,23,42,0.25);
overflow: hidden;
}
#explorerAIPanel textarea {
width: 100%;
min-height: 88px;
resize: vertical;
border-radius: 14px;
border: 1px solid var(--border);
background: var(--light);
color: var(--text);
padding: 0.85rem 0.9rem;
font: inherit;
}
@media (max-width: 680px) {
#explorerAIPanel {
right: 12px;
left: 12px;
bottom: 76px;
width: auto;
height: min(74vh, 720px);
}
#explorerAIFab {
right: 12px;
bottom: 12px;
}
}
`;
document.head.appendChild(style);
var button = document.createElement('button');
button.id = 'explorerAIFab';
button.type = 'button';
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-controls', 'explorerAIPanel');
button.innerHTML = '
Explorer AI';
button.addEventListener('click', function() { toggleExplorerAIPanel(); });
var panel = document.createElement('section');
panel.id = 'explorerAIPanel';
panel.setAttribute('aria-label', 'Explorer AI');
panel.innerHTML = '' +
'
' +
'
' +
'
' +
'
Explorer AI
' +
'
Read-only assistant using indexed explorer data, route APIs, and curated docs.
' +
'
' +
'
Close ' +
'
' +
'
' +
'Live routes ' +
'Route status ' +
'Current page ' +
'
' +
'
' +
'
' +
'
' +
'
Public explorer and route data only. No private key handling, no transaction execution.
' +
'
' +
'
' +
'
Shift+Enter for a new line. Enter to send.
' +
'
Ask Explorer AI ' +
'
' +
'
';
document.body.appendChild(button);
document.body.appendChild(panel);
var input = document.getElementById('explorerAIInput');
var sendButton = document.getElementById('explorerAISendBtn');
if (sendButton) {
sendButton.addEventListener('click', function() {
submitExplorerAIMessage();
});
}
if (input) {
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submitExplorerAIMessage();
}
});
}
renderExplorerAIMessages();
}
// Export functions
function exportBlockData(blockNumber) {
// Fetch block data and export as JSON
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks/${blockNumber}`)
.then(response => {
const block = normalizeBlock(response);
const dataStr = JSON.stringify(block, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `block-${blockNumber}.json`;
link.click();
URL.revokeObjectURL(url);
})
.catch(error => {
alert('Failed to export block data: ' + error.message);
});
}
function exportTransactionData(txHash) {
// Fetch transaction data and export as JSON
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}`)
.then(response => {
const tx = normalizeTransaction(response);
const dataStr = JSON.stringify(tx, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `transaction-${txHash.substring(0, 10)}.json`;
link.click();
URL.revokeObjectURL(url);
})
.catch(error => {
alert('Failed to export transaction data: ' + error.message);
});
}
// Global error handler
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
if (typeof showToast === 'function') {
showToast('An error occurred. Please refresh the page.', 'error');
}
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
if (typeof showToast === 'function') {
showToast('A network error occurred. Please try again.', 'error');
}
});
function setLiveRegion(text) {
var el = document.getElementById('explorerLiveRegion');
if (el) el.textContent = text || '';
}
// Toast notification function
function showToast(message, type = 'info', duration = 3000) {
if (type === 'error') setLiveRegion(message);
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
background: ${type === 'error' ? '#fee2e2' : type === 'success' ? '#d1fae5' : '#dbeafe'};
color: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6'};
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10000;
animation: slideIn 0.3s ease-out;
`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'slideOut 0.3s ease-out';
setTimeout(() => toast.remove(), 300);
}, duration);
}
// Add CSS for toast animations
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
// Search launcher, modal handlers, and mobile nav close on link click
document.addEventListener('DOMContentLoaded', () => {
initExplorerAIPanel();
const launchBtn = document.getElementById('searchLauncherBtn');
const modal = document.getElementById('smartSearchModal');
const backdrop = document.getElementById('smartSearchBackdrop');
const closeBtn = document.getElementById('smartSearchCloseBtn');
const input = document.getElementById('smartSearchInput');
const submitBtn = document.getElementById('smartSearchSubmitBtn');
if (launchBtn) {
launchBtn.addEventListener('click', function(e) {
e.preventDefault();
openSmartSearchModal('');
});
}
if (closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
closeSmartSearchModal();
});
}
if (backdrop) {
backdrop.addEventListener('click', closeSmartSearchModal);
}
if (input) {
input.addEventListener('input', function(e) {
updateSmartSearchPreview(e.target.value);
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
e.preventDefault();
closeSmartSearchModal();
} else if (e.key === 'Enter') {
e.preventDefault();
handleSearch(e.target.value);
}
});
}
if (submitBtn && input) {
submitBtn.addEventListener('click', function(e) {
e.preventDefault();
handleSearch(input.value);
});
}
window.addEventListener('keydown', function(e) {
var target = e.target;
var tag = target && target.tagName ? target.tagName.toLowerCase() : '';
var isEditable = !!(target && (target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select'));
if (e.key === 'Escape' && modal && modal.style.display === 'block') {
e.preventDefault();
closeSmartSearchModal();
return;
}
if ((e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k')) && !isEditable) {
e.preventDefault();
openSmartSearchModal('');
}
});
var navLinks = document.getElementById('navLinks');
if (navLinks) {
navLinks.addEventListener('click', function(e) {
if (e.target.closest('a')) closeNavMenu();
});
initNavDropdowns();
}
});