Files
explorer-monorepo/frontend/public/explorer-spa.js
2026-03-28 15:15:23 -07:00

6132 lines
409 KiB
JavaScript

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 = '<strong>Explorer API temporarily unavailable</strong> (HTTP ' + status + '). Stats, blocks, and transactions cannot load until the backend is running. <a href="https://gitea.d-bis.org/d-bis/explorer-monorepo/src/branch/main/docs/EXPLORER_API_ACCESS.md" target="_blank" rel="noopener noreferrer" style="color: #fff; text-decoration: underline;">See docs</a>.';
main.insertBefore(banner, main.firstChild);
}
(function() {
var _log = console.log, _warn = console.warn;
console.log = function() { if (window.DEBUG_EXPLORER) _log.apply(console, arguments); };
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, '&quot;'); }
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 '<div style="color:var(--text-light);">No recent searches yet.</div>';
}
var html = '<div style="display:grid; gap:0.5rem;">';
history.forEach(function(item) {
var safeItem = String(item).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
html += '<button type="button" class="btn btn-secondary" style="text-align:left; justify-content:flex-start; padding:0.65rem 0.8rem;" onclick="openSmartSearchModal(\'' + safeItem + '\')"><i class="fas fa-clock" aria-hidden="true" style="margin-right:0.55rem;"></i>' + escapeHtml(item) + '</button>';
});
html += '</div>';
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 = '<button type="button" class="btn btn-secondary" style="width:100%; text-align:left; justify-content:flex-start; padding:0.85rem 0.95rem; border-radius:16px; border:1px solid var(--border); background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.08));"' + (action ? ' onclick="' + action + '; closeSmartSearchModal();"' : ' disabled') + '>';
html += '<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:0.85rem; width:100%;">';
html += '<div style="min-width:0; flex:1;">';
html += '<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap; margin-bottom:0.35rem;">';
html += '<span style="font-size:0.72rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light);">' + escapeHtml(type || 'result') + '</span>';
html += '<span style="padding:0.18rem 0.45rem; border-radius:999px; background:rgba(37,99,235,0.12); color:var(--primary); font-size:0.72rem;">' + escapeHtml(type ? type.toUpperCase() : 'MATCH') + '</span>';
html += '</div>';
html += '<div style="font-size:0.98rem; font-weight:700; line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">' + escapeHtml(String(title).substring(0, 96)) + '</div>';
if (subtitle) {
html += '<div style="color:var(--text-light); font-size:0.84rem; line-height:1.35; margin-top:0.2rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">' + escapeHtml(String(subtitle).substring(0, 120)) + '</div>';
}
if (meta.length) {
html += '<div style="display:flex; flex-wrap:wrap; gap:0.35rem; margin-top:0.55rem;">';
meta.forEach(function(bit) {
html += '<span style="padding:0.18rem 0.45rem; border:1px solid var(--border); border-radius:999px; font-size:0.72rem; color:var(--text-light);">' + escapeHtml(bit) + '</span>';
});
html += '</div>';
}
html += '</div>';
html += '<div style="display:flex; align-items:center; color:var(--text-light); padding-top:0.1rem;"><i class="fas fa-arrow-right"></i></div>';
html += '</div>';
html += '</button>';
return html;
}
function renderSmartSearchPreviewItems(items, query) {
if (!items || !items.length) {
return '<div style="color:var(--text-light);">No live suggestions yet. Press Enter to search everything, or try a more specific address, hash, block, or token symbol.</div>';
}
var html = '<div style="display:grid; gap:0.45rem;">';
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 += '</div>';
return html;
}
function renderSmartSearchTrendingTokens(tokens) {
if (!tokens || !tokens.length) {
return '<div style="color:var(--text-light);">No trending tokens found yet.</div>';
}
var html = '<div style="display:grid; gap:0.45rem;">';
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 += '</div>';
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 = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Recent searches</div>' + renderSmartSearchHistory() + '</div><div><div style="font-weight:700; margin-bottom:0.45rem;">Trending tokens</div><div style="color:var(--text-light);">Loading trending token watchlist...</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Scope</div><div style="color:var(--text-light); line-height:1.6;">Current filter: <strong>' + escapeHtml(_smartSearchScope) + '</strong>. Use the left rail to narrow the search feed.</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Quick tips</div><div style="color:var(--text-light); line-height:1.6;">Try an address, transaction hash, block number, token symbol, or contract name. Press <strong>Enter</strong> to search and <strong>Esc</strong> to close.</div></div></div>';
fetchSmartSearchTrendingTokens().then(function(tokens) {
if (emptyRequestId !== _smartSearchPreviewRequestId) return;
preview.innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Recent searches</div>' + renderSmartSearchHistory() + '</div><div><div style="font-weight:700; margin-bottom:0.45rem;">Trending tokens</div>' + renderSmartSearchTrendingTokens(tokens) + '</div><div><div style="font-weight:700; margin-bottom:0.45rem;">Scope</div><div style="color:var(--text-light); line-height:1.6;">Current filter: <strong>' + escapeHtml(_smartSearchScope) + '</strong>. Use the left rail to narrow the search feed.</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Quick tips</div><div style="color:var(--text-light); line-height:1.6;">Try an address, transaction hash, block number, token symbol, or contract name. Press <strong>Enter</strong> to search and <strong>Esc</strong> to close.</div></div></div>';
});
return;
}
detected.style.display = 'inline-block';
detected.textContent = _smartSearchScope === 'all' ? info.label : (_smartSearchScope.charAt(0).toUpperCase() + _smartSearchScope.slice(1));
preview.innerHTML = '<div style="color:var(--text-light);">Searching live suggestions...</div>';
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 = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Detected: ' + escapeHtml(info.label) + '</div><div style="color:var(--text-light); line-height:1.5;">' + escapeHtml(info.detail) + '</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Live suggestions</div>' + renderSmartSearchPreviewItems(liveItems, query) + '</div></div>';
} catch (error) {
if (requestId !== _smartSearchPreviewRequestId) return;
if (_smartSearchScope !== 'all') {
preview.innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Detected: ' + escapeHtml(info.label) + '</div><div style="color:var(--text-light); line-height:1.5;">' + escapeHtml(info.detail) + '</div></div><div style="color:var(--text-light);">Live suggestions unavailable for the selected scope. Press Enter to search directly.</div></div>';
return;
}
preview.innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Detected: ' + escapeHtml(info.label) + '</div><div style="color:var(--text-light); line-height:1.5;">' + escapeHtml(info.detail) + '</div></div><div style="color:var(--text-light);">Live suggestions unavailable. Press Enter to search directly.</div></div>';
}
}, 220);
}
function openSmartSearchModal(prefill) {
var modal = document.getElementById('smartSearchModal');
var input = document.getElementById('smartSearchInput');
if (!modal || !input) return;
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
setSmartSearchScope(_smartSearchScope || 'all');
if (prefill != null) {
input.value = prefill;
}
updateSmartSearchPreview(input.value || '');
setTimeout(function() {
input.focus();
input.select();
}, 0);
}
function closeSmartSearchModal() {
var modal = document.getElementById('smartSearchModal');
if (!modal) return;
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
}
window.openSmartSearchModal = openSmartSearchModal;
window.closeSmartSearchModal = closeSmartSearchModal;
function renderPageFilterBar(key, placeholder, helperText, reloadJs) {
var inputId = key + 'FilterInput';
var value = getExplorerPageFilter(key);
var safeReload = reloadJs ? String(reloadJs) : '';
var html = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin: 0 0 0.75rem 0; padding: 0.75rem 0.9rem; border: 1px solid var(--border); border-radius: 10px; background: var(--light);">';
html += '<input id="' + inputId + '" type="search" value="' + escapeAttr(value) + '" placeholder="' + escapeAttr(placeholder || 'Filter...') + '" style="flex: 1 1 260px; min-width: 220px; padding: 0.55rem 0.7rem; border: 1px solid var(--border); border-radius: 8px; background: var(--light); color: var(--text);" onkeydown="if(event.key===\'Enter\'){event.preventDefault(); setExplorerPageFilter(\'' + key + '\', this.value); ' + safeReload + ';}">';
html += '<button type="button" class="btn btn-primary" onclick="setExplorerPageFilter(\'' + key + '\', document.getElementById(\'' + inputId + '\').value); ' + safeReload + ';">Apply</button>';
html += '<button type="button" class="btn btn-secondary" onclick="clearExplorerPageFilter(\'' + key + '\'); var el=document.getElementById(\'' + inputId + '\'); if(el) el.value=\'\'; ' + safeReload + ';">Clear</button>';
if (helperText) html += '<span style="color: var(--text-light); font-size: 0.85rem;">' + escapeHtml(helperText) + '</span>';
html += '</div>';
return html;
}
window.setExplorerPageFilter = setExplorerPageFilter;
window.clearExplorerPageFilter = clearExplorerPageFilter;
window._renderWatchlist = function() {
var container = document.getElementById('watchlistContent');
if (!container) return;
var list = getWatchlist();
var filter = getExplorerPageFilter('watchlist');
var filtered = filter ? list.filter(function(addr) {
return matchesExplorerFilter([addr, getAddressLabel(addr) || ''].join(' '), filter);
}) : list;
var filterBar = renderPageFilterBar('watchlist', 'Filter by address or label...', 'Filters your saved addresses.', 'window._renderWatchlist && window._renderWatchlist()');
if (list.length === 0) { container.innerHTML = filterBar + '<p style="color: var(--text-light);">No addresses in watchlist. Open an address and click "Add to watchlist".</p>'; return; }
if (filtered.length === 0) { container.innerHTML = filterBar + '<p style="color: var(--text-light);">No watchlist entries match the current filter.</p>'; return; }
var html = filterBar + '<table class="table"><thead><tr><th>Address</th><th>Label</th><th></th></tr></thead><tbody>';
filtered.forEach(function(addr){ var label = getAddressLabel(addr) || ''; html += '<tr><td>' + explorerAddressLink(addr, escapeHtml(shortenHash(addr)), 'color: inherit; text-decoration: none;') + '</td><td>' + escapeHtml(label) + '</td><td><button type="button" class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="event.stopPropagation(); removeFromWatchlist(\'' + escapeHtml(addr) + '\'); if(window._renderWatchlist) window._renderWatchlist();">Remove</button></td></tr>'; });
html += '</tbody></table>';
container.innerHTML = html;
};
var KNOWN_ADDRESS_LABELS = { '0x89dd12025bfcd38a168455a44b400e913ed33be2': 'CCIP WETH9 Bridge', '0xe0e93247376aa097db308b92e6ba36ba015535d0': 'CCIP WETH10 Bridge', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': 'WETH9', '0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': 'WETH10', '0x8078a09637e47fa5ed34f626046ea2094a5cde5e': 'CCIP Router', '0x105f8a15b819948a89153505762444ee9f324684': 'CCIP Sender' };
const CHAIN_138_PMM_INTEGRATION_ADDRESS = '0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d';
const CHAIN_138_PRIVATE_POOL_REGISTRY = '0xb27057B27db09e8Df353AF722c299f200519882A';
const CHAIN_138_CUSDT_ADDRESS = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22';
const CHAIN_138_CUSDC_ADDRESS = '0xf22258f57794CC8E06237084b353Ab30fFfa640b';
const CHAIN_138_CEURT_ADDRESS = '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72';
const CHAIN_138_CXAUC_ADDRESS = '0x290E52a8819A4fbD0714E517225429aA2B70EC6b';
const CHAIN_138_CXAUT_ADDRESS = '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E';
const MAINNET_USDT_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const MAINNET_USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const RESERVE_SYSTEM_ADDRESS = '0x607e97cD626f209facfE48c1464815DDE15B5093';
const RESERVE_TOKEN_INTEGRATION_ADDRESS = '0x34B73e6EDFd9f85a7c25EeD31dcB13aB6E969b96';
const BRIDGE_VAULT_ADDRESS = '0x31884f84555210FFB36a19D2471b8eBc7372d0A8';
const CHAIN_138_POOL_BALANCE_DECIMALS = 6;
const SELECTOR_POOLS = '0x901754d7';
const SELECTOR_GET_PRIVATE_POOL = '0xc427540d';
const SELECTOR_BALANCE_OF = '0x70a08231';
const SELECTOR_SYMBOL = '0x95d89b41';
const SELECTOR_OFFICIAL_USDT = '0xe015a3b8';
const SELECTOR_OFFICIAL_USDC = '0xc82ab874';
const SELECTOR_COMPLIANT_USDT = '0x15fdffdf';
const SELECTOR_COMPLIANT_USDC = '0x59916868';
const ROUTE_TREE_REFRESH_MS = 120000;
function buildRoutePriorityQueries(ctx) {
var officialUsdt = (ctx && ctx.officialUSDT) || '';
var officialUsdc = (ctx && ctx.officialUSDC) || '';
var queries = [
{
key: 'local-cusdt-cusdc',
title: 'Local direct: cUSDT / cUSDC',
tokenIn: CHAIN_138_CUSDT_ADDRESS,
tokenOut: CHAIN_138_CUSDC_ADDRESS,
destinationChainId: 138,
amountIn: '1000000',
},
{
key: 'bridge-cusdt-usdt',
title: 'Mainnet bridge path: cUSDT -> USDT',
tokenIn: CHAIN_138_CUSDT_ADDRESS,
tokenOut: MAINNET_USDT_ADDRESS,
destinationChainId: 1,
amountIn: '1000000',
},
{
key: 'bridge-cusdc-usdc',
title: 'Mainnet bridge path: cUSDC -> USDC',
tokenIn: CHAIN_138_CUSDC_ADDRESS,
tokenOut: MAINNET_USDC_ADDRESS,
destinationChainId: 1,
amountIn: '1000000',
}
];
if (safeAddress(officialUsdt)) {
queries.unshift({
key: 'local-cusdt-usdt',
title: 'Local direct: cUSDT / USDT (official mirror, Chain 138)',
tokenIn: CHAIN_138_CUSDT_ADDRESS,
tokenOut: officialUsdt,
destinationChainId: 138,
amountIn: '1000000',
});
}
if (safeAddress(officialUsdc)) {
queries.splice(2, 0, {
key: 'local-cusdc-usdc',
title: 'Local direct: cUSDC / USDC (official mirror, Chain 138)',
tokenIn: CHAIN_138_CUSDC_ADDRESS,
tokenOut: officialUsdc,
destinationChainId: 138,
amountIn: '1000000',
});
}
return queries;
}
const CHAIN_138_ROUTE_SWEEP_TOKENS = [
{ symbol: 'cUSDT', address: '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22' },
{ symbol: 'cUSDC', address: '0xf22258f57794CC8E06237084b353Ab30fFfa640b' },
{ symbol: 'cEURC', address: '0x8085961F9cF02b4d800A3c6d386D31da4B34266a' },
{ symbol: 'cEURT', address: '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72' },
{ symbol: 'cGBPC', address: '0x003960f16D9d34F2e98d62723B6721Fb92074aD2' },
{ symbol: 'cGBPT', address: '0x350f54e4D23795f86A9c03988c7135357CCaD97c' },
{ symbol: 'cAUDC', address: '0xD51482e567c03899eecE3CAe8a058161FD56069D' },
{ symbol: 'cJPYC', address: '0xEe269e1226a334182aace90056EE4ee5Cc8A6770' },
{ symbol: 'cCHFC', address: '0x873990849DDa5117d7C644f0aF24370797C03885' },
{ symbol: 'cCADC', address: '0x54dBd40cF05e15906A2C21f600937e96787f5679' },
{ symbol: 'cXAUC', address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' },
{ symbol: 'cXAUT', address: '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' }
];
function buildRouteSweepQueries(ctx) {
var officialUsdt = (ctx && ctx.officialUSDT) || '';
var officialUsdc = (ctx && ctx.officialUSDC) || '';
var queries = [];
CHAIN_138_ROUTE_SWEEP_TOKENS.forEach(function(token) {
var anchors = [];
if (token.symbol === 'cUSDT') {
anchors.push({ symbol: 'cUSDC', address: CHAIN_138_CUSDC_ADDRESS });
if (safeAddress(officialUsdt)) anchors.push({ symbol: 'USDT', address: officialUsdt });
} else if (token.symbol === 'cUSDC') {
anchors.push({ symbol: 'cUSDT', address: CHAIN_138_CUSDT_ADDRESS });
if (safeAddress(officialUsdc)) anchors.push({ symbol: 'USDC', address: officialUsdc });
} else {
anchors.push({ symbol: 'cUSDT', address: CHAIN_138_CUSDT_ADDRESS });
anchors.push({ symbol: 'cUSDC', address: CHAIN_138_CUSDC_ADDRESS });
}
anchors.forEach(function(anchor) {
if (!safeAddress(anchor.address)) return;
if (String(anchor.address).toLowerCase() === String(token.address).toLowerCase()) return;
queries.push({
key: token.symbol.toLowerCase() + '-' + anchor.symbol.toLowerCase(),
title: token.symbol + ' / ' + anchor.symbol + ' coverage probe',
symbol: token.symbol,
pairLabel: token.symbol + ' / ' + anchor.symbol,
tokenIn: token.address,
tokenOut: anchor.address,
destinationChainId: 138,
amountIn: '1000000'
});
});
});
return queries;
}
function stripHexPrefix(value) {
return String(value || '').replace(/^0x/i, '');
}
function encodeAddressWord(address) {
return stripHexPrefix(address).toLowerCase().padStart(64, '0');
}
function decodeAddressWord(data) {
var hex = stripHexPrefix(data);
if (!hex || hex.length < 64) return '';
return '0x' + hex.slice(hex.length - 40);
}
function decodeUint256Word(data, offsetWords) {
var hex = stripHexPrefix(data);
var start = (offsetWords || 0) * 64;
var word = hex.slice(start, start + 64);
if (!word) return 0n;
try { return BigInt('0x' + word); } catch (e) { return 0n; }
}
async function rpcEthCall(to, data) {
return rpcCall('eth_call', [{ to: to, data: data }, 'latest']);
}
async function rpcCodeExists(address) {
if (!safeAddress(address)) return false;
try {
var code = await rpcCall('eth_getCode', [address, 'latest']);
return !!code && code !== '0x';
} catch (e) {
return false;
}
}
async function rpcReadAddress(to, selector) {
if (!safeAddress(to)) return '';
try {
var out = await rpcEthCall(to, selector);
var addr = decodeAddressWord(out);
return safeAddress(addr) ? addr : '';
} catch (e) {
return '';
}
}
async function rpcReadMappedPool(integration, tokenA, tokenB) {
if (!safeAddress(integration) || !safeAddress(tokenA) || !safeAddress(tokenB)) return '';
try {
var out = await rpcEthCall(integration, SELECTOR_POOLS + encodeAddressWord(tokenA) + encodeAddressWord(tokenB));
var addr = decodeAddressWord(out);
return safeAddress(addr) ? addr : '';
} catch (e) {
return '';
}
}
async function rpcReadPrivatePool(registry, tokenA, tokenB) {
if (!safeAddress(registry) || !safeAddress(tokenA) || !safeAddress(tokenB)) return '';
try {
var out = await rpcEthCall(registry, SELECTOR_GET_PRIVATE_POOL + encodeAddressWord(tokenA) + encodeAddressWord(tokenB));
var addr = decodeAddressWord(out);
return safeAddress(addr) ? addr : '';
} catch (e) {
return '';
}
}
async function rpcReadBalanceOf(token, account) {
if (!safeAddress(token) || !safeAddress(account)) return 0n;
try {
var out = await rpcEthCall(token, SELECTOR_BALANCE_OF + encodeAddressWord(account));
return decodeUint256Word(out, 0);
} catch (e) {
return 0n;
}
}
async function rpcReadSymbol(token) {
if (!safeAddress(token)) return '';
try {
var out = await rpcEthCall(token, SELECTOR_SYMBOL);
var hex = stripHexPrefix(out);
if (!hex || hex.length < 128) return '';
var len = Number(BigInt('0x' + hex.slice(64, 128)));
if (!len || !Number.isFinite(len)) return '';
var dataHex = hex.slice(128, 128 + (len * 2));
var bytes = [];
for (var i = 0; i < dataHex.length; i += 2) {
bytes.push(parseInt(dataHex.slice(i, i + 2), 16));
}
return new TextDecoder().decode(new Uint8Array(bytes)).replace(/\0+$/, '');
} catch (e) {
return '';
}
}
function formatTokenUnits(value, decimals, precision) {
var amount = typeof value === 'bigint' ? value : BigInt(value || 0);
var scale = BigInt(Math.pow(10, Number(decimals || 0)));
if (scale === 0n) return '0';
var whole = amount / scale;
var fraction = amount % scale;
if (fraction === 0n) return whole.toString();
var fracStr = fraction.toString().padStart(Number(decimals || 0), '0');
var trimmed = fracStr.slice(0, Math.max(0, Number(precision == null ? 3 : precision))).replace(/0+$/, '');
return trimmed ? (whole.toString() + '.' + trimmed) : whole.toString();
}
async function fetchCurrentPmmContext() {
var integration = CHAIN_138_PMM_INTEGRATION_ADDRESS;
var officialUSDT = await rpcReadAddress(integration, SELECTOR_OFFICIAL_USDT);
var officialUSDC = await rpcReadAddress(integration, SELECTOR_OFFICIAL_USDC);
var compliantUSDT = await rpcReadAddress(integration, SELECTOR_COMPLIANT_USDT);
var compliantUSDC = await rpcReadAddress(integration, SELECTOR_COMPLIANT_USDC);
return {
integration: integration,
privateRegistry: CHAIN_138_PRIVATE_POOL_REGISTRY,
officialUSDT: officialUSDT,
officialUSDC: officialUSDC,
compliantUSDT: compliantUSDT || CHAIN_138_CUSDT_ADDRESS,
compliantUSDC: compliantUSDC || CHAIN_138_CUSDC_ADDRESS
};
}
function inferKnownTokenSymbol(address, ctx) {
var lower = String(address || '').toLowerCase();
if (lower === String((ctx && ctx.compliantUSDT) || CHAIN_138_CUSDT_ADDRESS).toLowerCase()) return 'cUSDT';
if (lower === String((ctx && ctx.compliantUSDC) || CHAIN_138_CUSDC_ADDRESS).toLowerCase()) return 'cUSDC';
if (lower === String((ctx && ctx.officialUSDT) || '').toLowerCase()) return 'USDT';
if (lower === String((ctx && ctx.officialUSDC) || '').toLowerCase()) return 'USDC';
var matched = CHAIN_138_ROUTE_SWEEP_TOKENS.find(function(token) { return String(token.address).toLowerCase() === lower; });
return matched ? matched.symbol : '';
}
async function buildLiveDirectRouteFallback(query, ctx) {
if (!query || Number(query.destinationChainId || query.chainId || 138) !== 138 || !safeAddress(query.tokenOut)) return null;
var poolAddress = await rpcReadMappedPool((ctx && ctx.integration) || CHAIN_138_PMM_INTEGRATION_ADDRESS, query.tokenIn, query.tokenOut);
if (!safeAddress(poolAddress) || !(await rpcCodeExists(poolAddress))) return null;
var reserveIn = await rpcReadBalanceOf(query.tokenIn, poolAddress);
var reserveOut = await rpcReadBalanceOf(query.tokenOut, poolAddress);
var funded = reserveIn > 0n && reserveOut > 0n;
var partial = (reserveIn > 0n || reserveOut > 0n) && !funded;
var status = funded ? 'live' : (partial ? 'partial' : 'unavailable');
var tokenInSymbol = inferKnownTokenSymbol(query.tokenIn, ctx) || await rpcReadSymbol(query.tokenIn) || shortenHash(query.tokenIn);
var tokenOutSymbol = inferKnownTokenSymbol(query.tokenOut, ctx) || await rpcReadSymbol(query.tokenOut) || shortenHash(query.tokenOut);
var reserveInFormatted = formatTokenUnits(reserveIn, CHAIN_138_POOL_BALANCE_DECIMALS, 3);
var reserveOutFormatted = formatTokenUnits(reserveOut, CHAIN_138_POOL_BALANCE_DECIMALS, 3);
return {
generatedAt: new Date().toISOString(),
source: {
chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet',
tokenIn: {
address: query.tokenIn,
symbol: tokenInSymbol,
name: tokenInSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
}
},
destination: {
chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet'
},
decision: 'direct-pool',
tree: [{
id: 'live-direct:' + String(poolAddress).toLowerCase(),
kind: 'direct-pool',
label: tokenInSymbol + '/' + tokenOutSymbol,
chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet',
status: status,
depth: {
tvlUsd: 0,
reserve0: reserveInFormatted,
reserve1: reserveOutFormatted,
estimatedTradeCapacityUsd: 0,
freshnessSeconds: 0,
status: funded ? 'live' : (partial ? 'stale' : 'unavailable')
},
tokenIn: {
address: query.tokenIn,
symbol: tokenInSymbol,
name: tokenInSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
tokenOut: {
address: query.tokenOut,
symbol: tokenOutSymbol,
name: tokenOutSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
poolAddress: poolAddress,
dexType: 'DODO PMM',
path: [query.tokenIn, query.tokenOut],
notes: [
'Live fallback from on-chain DODOPMMIntegration mapping',
'Base reserve ' + reserveInFormatted,
'Quote reserve ' + reserveOutFormatted
]
}],
pools: [{
poolAddress: poolAddress,
dexType: 'DODO PMM',
token0: {
address: query.tokenIn,
symbol: tokenInSymbol,
name: tokenInSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
token1: {
address: query.tokenOut,
symbol: tokenOutSymbol,
name: tokenOutSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
depth: {
tvlUsd: 0,
reserve0: reserveInFormatted,
reserve1: reserveOutFormatted,
estimatedTradeCapacityUsd: 0,
freshnessSeconds: 0,
status: funded ? 'live' : (partial ? 'stale' : 'unavailable')
}
}],
missingQuoteTokenPools: []
};
}
async function discoverPublicXauPool(baseToken, anchors, integration) {
for (var i = 0; i < anchors.length; i++) {
var anchor = anchors[i];
var pool = await rpcReadMappedPool(integration, baseToken, anchor.address);
if (safeAddress(pool)) {
return { pool: pool, anchor: anchor };
}
}
return { pool: '', anchor: anchors[0] };
}
async function discoverPrivateXauPool(baseToken, anchors, registry) {
for (var i = 0; i < anchors.length; i++) {
var anchor = anchors[i];
var pool = await rpcReadPrivatePool(registry, baseToken, anchor.address);
if (safeAddress(pool)) {
return { pool: pool, anchor: anchor };
}
}
return { pool: '', anchor: anchors[0] };
}
async function getLivePoolRows() {
var ctx = await fetchCurrentPmmContext();
var xauAnchors = [
{ symbol: 'cXAUC', address: CHAIN_138_CXAUC_ADDRESS },
{ symbol: 'cXAUT', address: CHAIN_138_CXAUT_ADDRESS }
];
var rows = [];
async function buildLivePoolRow(category, poolPair, poolType, poolAddress, baseToken, quoteToken, notesPrefix) {
var codeExists = await rpcCodeExists(poolAddress);
var baseBal = codeExists ? await rpcReadBalanceOf(baseToken, poolAddress) : 0n;
var quoteBal = codeExists ? await rpcReadBalanceOf(quoteToken, poolAddress) : 0n;
var funded = baseBal > 0n && quoteBal > 0n;
var partial = (baseBal > 0n || quoteBal > 0n) && !funded;
var status = !safeAddress(poolAddress) ? 'Not created' : (funded ? 'Funded (live)' : (partial ? 'Partially funded' : (codeExists ? 'Created (unfunded)' : 'Missing code')));
var notes = [];
if (notesPrefix) notes.push(notesPrefix);
if (safeAddress(poolAddress) && codeExists) {
notes.push('Base reserve: ' + formatTokenUnits(baseBal, CHAIN_138_POOL_BALANCE_DECIMALS, 3));
notes.push('Quote reserve: ' + formatTokenUnits(quoteBal, CHAIN_138_POOL_BALANCE_DECIMALS, 3));
}
return { category: category, poolPair: poolPair, poolType: poolType, address: poolAddress || '', status: status, notes: notes.join(' | ') };
}
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDT / cUSDC', 'DODO PMM',
await rpcReadMappedPool(ctx.integration, ctx.compliantUSDT, ctx.compliantUSDC),
ctx.compliantUSDT, ctx.compliantUSDC, 'Derived from live DODOPMMIntegration state'));
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDT / USDT (official mirror)', 'DODO PMM',
await rpcReadMappedPool(ctx.integration, ctx.compliantUSDT, ctx.officialUSDT),
ctx.compliantUSDT, ctx.officialUSDT, (await rpcCodeExists(ctx.officialUSDT)) ? ('Live quote token ' + shortenHash(ctx.officialUSDT)) : 'Quote-side USDT contract missing on Chain 138'));
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDC / USDC (official mirror)', 'DODO PMM',
await rpcReadMappedPool(ctx.integration, ctx.compliantUSDC, ctx.officialUSDC),
ctx.compliantUSDC, ctx.officialUSDC, (await rpcCodeExists(ctx.officialUSDC)) ? ('Live quote token ' + shortenHash(ctx.officialUSDC)) : 'Quote-side USDC contract missing on Chain 138'));
var publicCusdtXau = await discoverPublicXauPool(ctx.compliantUSDT, xauAnchors, ctx.integration);
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDT / XAU (' + publicCusdtXau.anchor.symbol + ')', 'DODO PMM',
publicCusdtXau.pool, ctx.compliantUSDT, publicCusdtXau.anchor.address,
safeAddress(publicCusdtXau.pool) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered'));
var publicCusdcXau = await discoverPublicXauPool(ctx.compliantUSDC, xauAnchors, ctx.integration);
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDC / XAU (' + publicCusdcXau.anchor.symbol + ')', 'DODO PMM',
publicCusdcXau.pool, ctx.compliantUSDC, publicCusdcXau.anchor.address,
safeAddress(publicCusdcXau.pool) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered'));
var publicCeurtXau = await discoverPublicXauPool(CHAIN_138_CEURT_ADDRESS, xauAnchors, ctx.integration);
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cEURT / XAU (' + publicCeurtXau.anchor.symbol + ')', 'DODO PMM',
publicCeurtXau.pool, CHAIN_138_CEURT_ADDRESS, publicCeurtXau.anchor.address,
safeAddress(publicCeurtXau.pool) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered'));
var privateCusdtXau = await discoverPrivateXauPool(ctx.compliantUSDT, xauAnchors, ctx.privateRegistry);
rows.push(await buildLivePoolRow('Private Stabilization Pools', 'cUSDT ↔ XAU (' + privateCusdtXau.anchor.symbol + ')', 'PrivatePoolRegistry',
privateCusdtXau.pool, ctx.compliantUSDT, privateCusdtXau.anchor.address,
safeAddress(privateCusdtXau.pool) ? ('Registered in live PrivatePoolRegistry ' + shortenHash(ctx.privateRegistry)) : 'No live private XAU pool currently registered'));
var privateCusdcXau = await discoverPrivateXauPool(ctx.compliantUSDC, xauAnchors, ctx.privateRegistry);
rows.push(await buildLivePoolRow('Private Stabilization Pools', 'cUSDC ↔ XAU (' + privateCusdcXau.anchor.symbol + ')', 'PrivatePoolRegistry',
privateCusdcXau.pool, ctx.compliantUSDC, privateCusdcXau.anchor.address,
safeAddress(privateCusdcXau.pool) ? ('Registered in live PrivatePoolRegistry ' + shortenHash(ctx.privateRegistry)) : 'No live private XAU pool currently registered'));
var privateCeurtXau = await discoverPrivateXauPool(CHAIN_138_CEURT_ADDRESS, xauAnchors, ctx.privateRegistry);
rows.push(await buildLivePoolRow('Private Stabilization Pools', 'cEURT ↔ XAU (' + privateCeurtXau.anchor.symbol + ')', 'PrivatePoolRegistry',
privateCeurtXau.pool, CHAIN_138_CEURT_ADDRESS, privateCeurtXau.anchor.address,
safeAddress(privateCeurtXau.pool) ? ('Registered in live PrivatePoolRegistry ' + shortenHash(ctx.privateRegistry)) : 'No live private XAU pool currently registered'));
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'ReserveSystem',
poolType: 'Reserve',
address: RESERVE_SYSTEM_ADDRESS,
status: (await rpcCodeExists(RESERVE_SYSTEM_ADDRESS)) ? 'Deployed (live)' : 'Missing code',
notes: 'Live code check against Chain 138'
});
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'ReserveTokenIntegration',
poolType: 'Reserve',
address: RESERVE_TOKEN_INTEGRATION_ADDRESS,
status: (await rpcCodeExists(RESERVE_TOKEN_INTEGRATION_ADDRESS)) ? 'Deployed (live)' : 'Missing code',
notes: 'Live code check against Chain 138'
});
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'StablecoinReserveVault',
poolType: 'Reserve',
address: '',
status: 'External / Mainnet-side',
notes: 'Reserve vault lives on Mainnet by design; no local Chain 138 contract is expected here'
});
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'Bridge_Vault',
poolType: 'Vault',
address: BRIDGE_VAULT_ADDRESS,
status: (await rpcCodeExists(BRIDGE_VAULT_ADDRESS)) ? 'Deployed (live)' : 'Missing code',
notes: 'Live code check against Chain 138'
});
rows.push({
category: 'Bridge Liquidity Pool',
poolPair: 'LiquidityPoolETH',
poolType: 'Bridge LP',
address: '',
status: 'External / Mainnet bridge LP',
notes: 'Trustless bridge liquidity pool is a Mainnet-side contract, not a local Chain 138 pool'
});
return { rows: rows, context: ctx };
}
function getActiveBridgeContractCount() {
return new Set([
'0x971cD9D156f193df8051E48043C476e53ECd4693',
'0xe0E93247376aa097dB308B92e6Ba36bA015535D0',
'0x2A0840e5117683b11682ac46f5CF5621E67269E3',
'0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03',
'0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
'0xa780ef19a041745d353c9432f2a7f5a241335ffe',
'0x105f8a15b819948a89153505762444ee9f324684',
'0xdab0591e5e89295ffad75a71dcfc30c5625c4fa2'
].map(function(addr) { return String(addr || '').toLowerCase(); })).size;
}
function getAddressLabel(addr) { if (!addr) return ''; var lower = addr.toLowerCase(); if (KNOWN_ADDRESS_LABELS[lower]) return KNOWN_ADDRESS_LABELS[lower]; try { var j = localStorage.getItem('explorerAddressLabels'); if (!j) return ''; var m = JSON.parse(j); return m[lower] || ''; } catch(e){ return ''; } }
function formatAddressWithLabel(addr) { if (!addr) return ''; var label = getAddressLabel(addr); return label ? escapeHtml(label) + ' (' + escapeHtml(shortenHash(addr)) + ')' : escapeHtml(shortenHash(addr)); }
function copyToClipboard(val, msg) { if (!val) return; try { navigator.clipboard.writeText(String(val)); showToast(msg || 'Copied', 'success'); } catch(e) { showToast('Copy failed', 'error'); } }
function setAddressLabel(addr, label) { try { var j = localStorage.getItem('explorerAddressLabels') || '{}'; var m = JSON.parse(j); m[addr.toLowerCase()] = (label || '').trim(); localStorage.setItem('explorerAddressLabels', JSON.stringify(m)); return true; } catch(e){ return false; } }
function getWatchlist() { try { var j = localStorage.getItem('explorerWatchlist'); if (!j) return []; var a = JSON.parse(j); return Array.isArray(a) ? a : []; } catch(e){ return []; } }
function addToWatchlist(addr) { if (!addr || !/^0x[a-fA-F0-9]{40}$/i.test(addr)) return false; var a = getWatchlist(); var lower = addr.toLowerCase(); if (a.indexOf(lower) === -1) { a.push(lower); try { localStorage.setItem('explorerWatchlist', JSON.stringify(a)); return true; } catch(e){} } return false; }
function removeFromWatchlist(addr) { var a = getWatchlist().filter(function(x){ return x !== addr.toLowerCase(); }); try { localStorage.setItem('explorerWatchlist', JSON.stringify(a)); return true; } catch(e){ return false; } }
function isInWatchlist(addr) { return getWatchlist().indexOf((addr || '').toLowerCase()) !== -1; }
let currentView = 'home';
let _poolsRouteTreeRefreshTimer = null;
let currentDetailKey = '';
let latestPoolsSnapshot = null;
var _inNavHandler = false; // re-entrancy guard: prevents hashchange -> applyHashRoute -> stub from recursing
let provider = null;
let signer = null;
let userAddress = null;
// Tiered Architecture: Track and Authentication
let userTrack = 1; // Default to Track 1 (public)
let authToken = null;
// View switch helper: works even if rest of script fails. Do NOT set location.hash here (we use path-based URLs).
function switchToView(viewName) {
if (viewName !== 'pools' && viewName !== 'routes' && _poolsRouteTreeRefreshTimer) {
clearInterval(_poolsRouteTreeRefreshTimer);
_poolsRouteTreeRefreshTimer = null;
}
if (viewName !== 'blocks' && _blocksScrollAnimationId != null) {
cancelAnimationFrame(_blocksScrollAnimationId);
_blocksScrollAnimationId = null;
}
currentView = viewName;
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','addresses','pools','routes','liquidity','more'];
if (detailViews.indexOf(viewName) === -1) currentDetailKey = '';
var homeEl = document.getElementById('homeView');
if (homeEl) homeEl.style.display = viewName === 'home' ? 'block' : 'none';
document.querySelectorAll('.detail-view').forEach(function(el) { el.classList.remove('active'); });
var target = document.getElementById(viewName + 'View');
if (target) target.classList.add('active');
}
// Compatibility wrappers for nav buttons; re-entrancy guard prevents hashchange recursion.
window.showHome = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('home'); if (window._showHome) window._showHome(); } finally { _inNavHandler = false; } };
window.showBlocks = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('blocks'); if (window._showBlocks) window._showBlocks(); } finally { _inNavHandler = false; } };
window.showTransactions = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('transactions'); if (window._showTransactions) window._showTransactions(); } finally { _inNavHandler = false; } };
window.showAddresses = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('addresses'); if (window._showAddresses) window._showAddresses(); } finally { _inNavHandler = false; } };
window.showBridgeMonitoring = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('bridge'); if (window._showBridgeMonitoring) window._showBridgeMonitoring(); } finally { _inNavHandler = false; } };
window.showWETHUtilities = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('weth'); if (window._showWETHUtilities) window._showWETHUtilities(); } finally { _inNavHandler = false; } };
window.showWETHTab = function() {};
window.showWatchlist = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('watchlist'); if (window._renderWatchlist) window._renderWatchlist(); } finally { _inNavHandler = false; } };
function openPoolsView() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('pools'); if (typeof renderPoolsView === 'function') renderPoolsView(); } finally { _inNavHandler = false; } }
window.openPoolsView = openPoolsView;
// Back-compat alias for older menu wiring; prefer openPoolsView() and renderPoolsView().
window.showPools = openPoolsView;
window.showRoutes = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('routes'); if (typeof renderRoutesView === 'function') renderRoutesView(); } finally { _inNavHandler = false; } };
window.showLiquidityAccess = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('liquidity'); if (typeof renderLiquidityAccessView === 'function') renderLiquidityAccessView(); } finally { _inNavHandler = false; } };
window.showMore = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('more'); if (window._showMore) window._showMore(); } finally { _inNavHandler = false; } };
window.showTokensList = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('tokens'); if (window._loadTokensList) window._loadTokensList(); } finally { _inNavHandler = false; } };
window.showAnalytics = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('analytics'); if (window._showAnalytics) window._showAnalytics(); } finally { _inNavHandler = false; } };
window.showOperator = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('operator'); if (window._showOperator) window._showOperator(); } finally { _inNavHandler = false; } };
// Compatibility wrappers for detail views; defer to the next tick to avoid synchronous recursion.
window.showBlockDetail = function(n) { if (window._showBlockDetail) setTimeout(function() { window._showBlockDetail(n); }, 0); };
window.showTransactionDetail = function(h) { if (window._showTransactionDetail) setTimeout(function() { window._showTransactionDetail(h); }, 0); };
window.showAddressDetail = function(a) { if (window._showAddressDetail) setTimeout(function() { window._showAddressDetail(a); }, 0); };
window.toggleDarkMode = function() { document.body.classList.toggle('dark-theme'); var icon = document.getElementById('themeIcon'); if (icon) icon.className = document.body.classList.contains('dark-theme') ? 'fas fa-sun' : 'fas fa-moon'; try { localStorage.setItem('explorerTheme', document.body.classList.contains('dark-theme') ? 'dark' : 'light'); } catch (e) {} };
// Feature flags
const FEATURE_FLAGS = {
ADDRESS_FULL_DETAIL: { track: 2 },
TOKEN_BALANCES: { track: 2 },
TX_HISTORY: { track: 2 },
INTERNAL_TXS: { track: 2 },
ENHANCED_SEARCH: { track: 2 },
ANALYTICS_DASHBOARD: { track: 3 },
FLOW_TRACKING: { track: 3 },
BRIDGE_ANALYTICS: { track: 3 },
OPERATOR_PANEL: { track: 4 },
};
function hasAccess(requiredTrack) {
return userTrack >= requiredTrack;
}
function isFeatureEnabled(featureName) {
const feature = FEATURE_FLAGS[featureName];
if (!feature) return false;
return hasAccess(feature.track);
}
// Load feature flags from API
async function loadFeatureFlags() {
try {
const response = await fetch(EXPLORER_API_V1_BASE + '/features', {
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
});
if (response.ok) {
const data = await response.json();
userTrack = data.track || 1;
updateUIForTrack();
}
} catch (error) {
console.error('Failed to load feature flags:', error);
}
}
function updateUIForTrack() {
// Show/hide navigation items based on track
const analyticsNav = document.getElementById('analyticsNav');
const operatorNav = document.getElementById('operatorNav');
if (analyticsNav) analyticsNav.style.display = hasAccess(3) ? 'block' : 'none';
if (operatorNav) operatorNav.style.display = hasAccess(4) ? 'block' : 'none';
}
// Wallet authentication
async function connectWallet() {
if (typeof ethers === 'undefined') {
alert('Ethers.js not loaded. Please refresh the page.');
return;
}
try {
if (!window.ethereum) {
alert('MetaMask not detected. Please install MetaMask.');
return;
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.send("eth_requestAccounts", []);
const address = accounts[0];
// Request nonce
const nonceResp = await fetch(EXPLORER_API_V1_BASE + '/auth/nonce', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address })
});
const nonceData = await nonceResp.json();
// Sign message
const message = `Sign this message to authenticate with SolaceScanScout Explorer.\n\nNonce: ${nonceData.nonce}`;
const signer = provider.getSigner();
const signature = await signer.signMessage(message);
// Authenticate
const authResp = await fetch(EXPLORER_API_V1_BASE + '/auth/wallet', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address, signature, nonce: nonceData.nonce })
});
if (authResp.ok) {
const authData = await authResp.json();
authToken = authData.token;
userTrack = authData.track;
userAddress = address;
localStorage.setItem('authToken', authToken);
localStorage.setItem('userAddress', userAddress);
updateUIForTrack();
const walletBtn = document.getElementById('walletConnectBtn');
const walletStatus = document.getElementById('walletStatus');
const walletAddress = document.getElementById('walletAddress');
if (walletBtn) walletBtn.style.display = 'none';
if (walletStatus) walletStatus.style.display = 'flex';
if (walletAddress) walletAddress.textContent = shortenHash(address);
await loadFeatureFlags();
showToast('Wallet connected successfully!', 'success');
} else {
const errorData = await authResp.json();
alert('Authentication failed: ' + (errorData.error?.message || 'Unknown error'));
}
} catch (error) {
var msg = (error && error.message) ? String(error.message) : '';
var friendly = (error && error.code === 4001) || /not been authorized|rejected|denied/i.test(msg)
? 'Connection was rejected. Please approve the MetaMask popup to connect.'
: ('Failed to connect wallet: ' + (msg || 'Unknown error'));
alert(friendly);
}
}
// Check for stored auth token on load
window.addEventListener('DOMContentLoaded', () => {
const storedToken = localStorage.getItem('authToken');
const storedAddress = localStorage.getItem('userAddress');
if (storedToken && storedAddress) {
authToken = storedToken;
userAddress = storedAddress;
const walletBtn = document.getElementById('walletConnectBtn');
const walletStatus = document.getElementById('walletStatus');
const walletAddress = document.getElementById('walletAddress');
if (walletBtn) walletBtn.style.display = 'none';
if (walletStatus) walletStatus.style.display = 'flex';
if (walletAddress) walletAddress.textContent = shortenHash(storedAddress);
loadFeatureFlags();
} else {
const walletBtn = document.getElementById('walletConnectBtn');
if (walletBtn) walletBtn.style.display = 'block';
}
});
// WETH Contract Addresses
const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
// WETH10 address - will be checksummed when ethers is loaded
const WETH10_ADDRESS_RAW = '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f';
let WETH10_ADDRESS = WETH10_ADDRESS_RAW; // Will be updated to checksummed version
// Data adapter functions to normalize Blockscout API responses
function normalizeBlock(blockscoutBlock) {
if (!blockscoutBlock) return null;
return {
number: blockscoutBlock.height || blockscoutBlock.number || parseInt(blockscoutBlock.block_number, 10),
hash: blockscoutBlock.hash || blockscoutBlock.block_hash,
parent_hash: blockscoutBlock.parent_hash || blockscoutBlock.parentHash,
timestamp: blockscoutBlock.timestamp,
miner: blockscoutBlock.miner?.hash || blockscoutBlock.miner || blockscoutBlock.miner_hash,
transaction_count: blockscoutBlock.transaction_count || blockscoutBlock.transactions_count || 0,
gas_used: blockscoutBlock.gas_used || '0',
gas_limit: blockscoutBlock.gas_limit || blockscoutBlock.gasLimit || '0',
size: blockscoutBlock.size || 0,
difficulty: blockscoutBlock.difficulty || '0',
base_fee_per_gas: blockscoutBlock.base_fee_per_gas,
burnt_fees: blockscoutBlock.burnt_fees || '0',
total_difficulty: blockscoutBlock.total_difficulty || '0',
nonce: blockscoutBlock.nonce || '0x0',
extra_data: blockscoutBlock.extra_data || '0x'
};
}
function normalizeTransaction(blockscoutTx) {
if (!blockscoutTx) return null;
// Map status: "ok" -> 1, "error" -> 0, others -> 0
let status = 0;
if (blockscoutTx.status === 'ok' || blockscoutTx.status === 'success') {
status = 1;
} else if (blockscoutTx.status === 1 || blockscoutTx.status === '1') {
status = 1;
}
return {
hash: blockscoutTx.hash || blockscoutTx.tx_hash,
from: blockscoutTx.from?.hash || blockscoutTx.from || blockscoutTx.from_address_hash || blockscoutTx.from_address,
to: blockscoutTx.to?.hash || blockscoutTx.to || blockscoutTx.to_address_hash || blockscoutTx.to_address,
value: blockscoutTx.value || '0',
block_number: blockscoutTx.block_number || blockscoutTx.block || null,
block_hash: blockscoutTx.block_hash || blockscoutTx.blockHash || null,
transaction_index: blockscoutTx.position || blockscoutTx.transaction_index || blockscoutTx.index || 0,
gas_price: blockscoutTx.gas_price || blockscoutTx.max_fee_per_gas || '0',
gas_used: blockscoutTx.gas_used || '0',
gas_limit: blockscoutTx.gas_limit || blockscoutTx.gas || '0',
nonce: blockscoutTx.nonce || '0',
status: status,
created_at: blockscoutTx.timestamp || blockscoutTx.created_at || blockscoutTx.block_timestamp,
input: blockscoutTx.input || blockscoutTx.raw_input || '0x',
max_fee_per_gas: blockscoutTx.max_fee_per_gas,
max_priority_fee_per_gas: blockscoutTx.max_priority_fee_per_gas,
priority_fee: blockscoutTx.priority_fee,
tx_burnt_fee: blockscoutTx.tx_burnt_fee || blockscoutTx.burnt_fees || '0',
type: blockscoutTx.type || 0,
confirmations: blockscoutTx.confirmations || 0,
contract_address: blockscoutTx.created_contract_address_hash || (blockscoutTx.contract_creation && (blockscoutTx.to?.hash || blockscoutTx.to_address_hash)) || null,
revert_reason: blockscoutTx.revert_reason || blockscoutTx.error || blockscoutTx.result || null,
decoded_input: blockscoutTx.decoded_input || null,
method_id: blockscoutTx.method_id || null
};
}
function normalizeAddress(blockscoutAddr) {
if (!blockscoutAddr || typeof blockscoutAddr !== 'object') return null;
var hash = blockscoutAddr.hash || blockscoutAddr.address || blockscoutAddr.address_hash;
if (!hash && blockscoutAddr.creator_address_hash === undefined) return null;
var creationTx = blockscoutAddr.creation_tx_hash || blockscoutAddr.creator_tx_hash || blockscoutAddr.creation_transaction_hash || null;
var firstSeen = blockscoutAddr.first_transaction_at || blockscoutAddr.first_seen_at || blockscoutAddr.first_tx_at || null;
var lastSeen = blockscoutAddr.last_transaction_at || blockscoutAddr.last_seen_at || blockscoutAddr.last_tx_at || null;
var txSent = blockscoutAddr.transactions_sent_count;
if (txSent == null) txSent = blockscoutAddr.tx_sent_count;
if (txSent == null) txSent = blockscoutAddr.transactions_count;
var txReceived = blockscoutAddr.transactions_received_count;
if (txReceived == null) txReceived = blockscoutAddr.tx_received_count;
if (txReceived == null) txReceived = 0;
return {
address: hash || null,
hash: hash || null,
balance: blockscoutAddr.balance || blockscoutAddr.coin_balance || blockscoutAddr.coin_balance_value || '0',
transaction_count: blockscoutAddr.transactions_count != null ? blockscoutAddr.transactions_count : (blockscoutAddr.transaction_count != null ? blockscoutAddr.transaction_count : (blockscoutAddr.tx_count != null ? blockscoutAddr.tx_count : 0)),
token_count: blockscoutAddr.token_count != null ? blockscoutAddr.token_count : 0,
is_contract: !!blockscoutAddr.is_contract,
is_verified: !!blockscoutAddr.is_verified,
tx_sent: txSent != null ? txSent : 0,
tx_received: txReceived != null ? txReceived : 0,
label: blockscoutAddr.name || blockscoutAddr.ens_domain_name || null,
name: blockscoutAddr.name || null,
ens_domain_name: blockscoutAddr.ens_domain_name || null,
creation_tx_hash: creationTx,
first_seen_at: firstSeen,
last_seen_at: lastSeen
};
}
function hexToDecimalString(value) {
if (value == null || value === '') return '0';
var stringValue = String(value);
if (!/^0x/i.test(stringValue)) return stringValue;
try {
return BigInt(stringValue).toString();
} catch (e) {
return '0';
}
}
function hexToNumber(value) {
if (value == null || value === '') return 0;
if (typeof value === 'number') return value;
try {
return Number(BigInt(String(value)));
} catch (e) {
return Number(value) || 0;
}
}
function rpcTimestampToIso(value) {
var timestampSeconds = hexToNumber(value);
if (!timestampSeconds) return new Date(0).toISOString();
return new Date(timestampSeconds * 1000).toISOString();
}
function normalizeRpcBlock(block) {
if (!block || !block.hash) return null;
return normalizeBlock({
number: hexToDecimalString(block.number),
hash: block.hash,
parent_hash: block.parentHash,
timestamp: rpcTimestampToIso(block.timestamp),
miner: block.miner,
transaction_count: Array.isArray(block.transactions) ? block.transactions.length : 0,
gas_used: hexToDecimalString(block.gasUsed),
gas_limit: hexToDecimalString(block.gasLimit),
size: hexToNumber(block.size),
difficulty: hexToDecimalString(block.difficulty),
base_fee_per_gas: block.baseFeePerGas ? hexToDecimalString(block.baseFeePerGas) : undefined,
nonce: block.nonce || '0x0'
});
}
function normalizeRpcTransaction(tx, receipt, block) {
if (!tx || !tx.hash) return null;
var blockTimestamp = block && block.timestamp ? rpcTimestampToIso(block.timestamp) : new Date(0).toISOString();
var txType = tx.type != null ? hexToNumber(tx.type) : 0;
var effectiveGasPrice = receipt && receipt.effectiveGasPrice ? receipt.effectiveGasPrice : tx.gasPrice;
return normalizeTransaction({
hash: tx.hash,
from: tx.from,
to: tx.to,
value: hexToDecimalString(tx.value),
block_number: tx.blockNumber ? hexToDecimalString(tx.blockNumber) : null,
block_hash: tx.blockHash || null,
transaction_index: tx.transactionIndex ? hexToNumber(tx.transactionIndex) : 0,
gas_price: effectiveGasPrice ? hexToDecimalString(effectiveGasPrice) : '0',
gas_used: receipt && receipt.gasUsed ? hexToDecimalString(receipt.gasUsed) : '0',
gas_limit: tx.gas ? hexToDecimalString(tx.gas) : '0',
nonce: tx.nonce != null ? String(hexToNumber(tx.nonce)) : '0',
status: receipt && receipt.status != null ? hexToNumber(receipt.status) : 0,
created_at: blockTimestamp,
input: tx.input || '0x',
max_fee_per_gas: tx.maxFeePerGas ? hexToDecimalString(tx.maxFeePerGas) : undefined,
max_priority_fee_per_gas: tx.maxPriorityFeePerGas ? hexToDecimalString(tx.maxPriorityFeePerGas) : undefined,
type: txType,
contract_address: receipt && receipt.contractAddress ? receipt.contractAddress : null
});
}
async function fetchChain138BlocksPage(page, pageSize) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks?page=${page}&page_size=${pageSize}`);
if (response && response.items) {
return response.items.map(normalizeBlock).filter(function(block) { return block !== null; });
}
} catch (error) {
console.warn('Falling back to RPC blocks list:', error.message || error);
}
var blocks = [];
var latestBlockHex = await rpcCall('eth_blockNumber', []);
var latestBlock = hexToNumber(latestBlockHex);
var startIndex = Math.max(0, latestBlock - ((page - 1) * pageSize));
for (var i = 0; i < pageSize && startIndex - i >= 0; i++) {
var block = await rpcCall('eth_getBlockByNumber', ['0x' + (startIndex - i).toString(16), false]).catch(function() { return null; });
var normalized = normalizeRpcBlock(block);
if (normalized) blocks.push(normalized);
}
return blocks;
}
async function fetchChain138TransactionsPage(page, pageSize) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?page=${page}&page_size=${pageSize}`);
if (response && response.items) {
return response.items.map(normalizeTransaction).filter(function(tx) { return tx !== null; });
}
} catch (error) {
console.warn('Falling back to RPC transactions list:', error.message || error);
}
var transactions = [];
var latestBlockHex = await rpcCall('eth_blockNumber', []);
var latestBlock = hexToNumber(latestBlockHex);
var blockCursor = Math.max(0, latestBlock - ((page - 1) * 5));
while (blockCursor >= 0 && transactions.length < pageSize) {
var block = await rpcCall('eth_getBlockByNumber', ['0x' + blockCursor.toString(16), true]).catch(function() { return null; });
if (block && Array.isArray(block.transactions)) {
var blockIsoTimestamp = rpcTimestampToIso(block.timestamp);
block.transactions.forEach(function(tx) {
if (transactions.length >= pageSize) return;
var normalized = normalizeTransaction({
hash: tx.hash,
from: tx.from,
to: tx.to,
value: hexToDecimalString(tx.value),
block_number: tx.blockNumber ? hexToDecimalString(tx.blockNumber) : hexToDecimalString(block.number),
block_hash: tx.blockHash || block.hash,
gas_price: tx.gasPrice ? hexToDecimalString(tx.gasPrice) : (tx.maxFeePerGas ? hexToDecimalString(tx.maxFeePerGas) : '0'),
gas_limit: tx.gas ? hexToDecimalString(tx.gas) : '0',
nonce: tx.nonce != null ? String(hexToNumber(tx.nonce)) : '0',
created_at: blockIsoTimestamp,
input: tx.input || '0x',
max_fee_per_gas: tx.maxFeePerGas ? hexToDecimalString(tx.maxFeePerGas) : undefined,
max_priority_fee_per_gas: tx.maxPriorityFeePerGas ? hexToDecimalString(tx.maxPriorityFeePerGas) : undefined,
type: tx.type != null ? hexToNumber(tx.type) : 0
});
if (normalized) transactions.push(normalized);
});
}
blockCursor -= 1;
}
return transactions;
}
async function fetchChain138BlockDetail(blockNumber) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks/${blockNumber}`);
return {
block: normalizeBlock(response),
rawBlockResponse: response
};
} catch (error) {
console.warn('Falling back to RPC block detail:', error.message || error);
}
var rpcBlock = await rpcCall('eth_getBlockByNumber', ['0x' + Number(blockNumber).toString(16), true]);
return {
block: normalizeRpcBlock(rpcBlock),
rawBlockResponse: null
};
}
async function fetchChain138TransactionDetail(txHash) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}`);
return {
transaction: normalizeTransaction(response),
rawTransaction: response
};
} catch (error) {
console.warn('Falling back to RPC transaction detail:', error.message || error);
}
var rpcTx = await rpcCall('eth_getTransactionByHash', [txHash]);
if (!rpcTx) {
return { transaction: null, rawTransaction: null };
}
var receipt = await rpcCall('eth_getTransactionReceipt', [txHash]).catch(function() { return null; });
var block = rpcTx.blockNumber ? await rpcCall('eth_getBlockByNumber', [rpcTx.blockNumber, false]).catch(function() { return null; }) : null;
return {
transaction: normalizeRpcTransaction(rpcTx, receipt, block),
rawTransaction: null
};
}
// Skeleton loader function
function createSkeletonLoader(type) {
switch(type) {
case 'stats':
return `
<div class="stats-grid">
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
</div>
`;
case 'table':
return `
<table class="table">
<thead>
<tr>
<th><div class="skeleton skeleton-text"></div></th>
<th><div class="skeleton skeleton-text"></div></th>
<th><div class="skeleton skeleton-text"></div></th>
<th><div class="skeleton skeleton-text"></div></th>
</tr>
</thead>
<tbody>
${Array(5).fill(0).map(() => `
<tr>
<td><div class="skeleton skeleton-text"></div></td>
<td><div class="skeleton skeleton-text"></div></td>
<td><div class="skeleton skeleton-text"></div></td>
<td><div class="skeleton skeleton-text"></div></td>
</tr>
`).join('')}
</tbody>
</table>
`;
case 'detail':
return `
<div class="card">
<div class="card-header">
<div class="skeleton skeleton-title" style="width: 40%;"></div>
</div>
<div class="card-body">
${Array(8).fill(0).map(() => `
<div class="info-row">
<div class="info-label">
<div class="skeleton skeleton-text" style="width: 120px;"></div>
</div>
<div class="info-value">
<div class="skeleton skeleton-text" style="width: 200px;"></div>
</div>
</div>
`).join('')}
</div>
</div>
`;
default:
return '<div class="loading"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
}
}
/**
* Parse amount from WETH wrap/unwrap field. Label is "Amount (ETH)".
* - If input looks like raw wei (integer string, 18+ digits, no decimal), use as wei.
* - Otherwise treat as ETH and convert with parseEther (e.g. "100" -> 100 ETH).
* So pasting 100000000000000000000 wraps 100 ETH; typing 100 also wraps 100 ETH.
*/
function parseWETHAmount(inputStr) {
var s = (inputStr && String(inputStr).trim()) || '';
if (!s) return null;
try {
if (/^\d+$/.test(s) && s.length >= 18) {
return ethers.BigNumber.from(s);
}
return ethers.utils.parseEther(s);
} catch (e) {
return null;
}
}
// WETH ABI (Standard ERC-20 + WETH functions)
const WETH_ABI = [
"function deposit() payable",
"function withdraw(uint256 wad)",
"function balanceOf(address account) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function totalSupply() view returns (uint256)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"event Deposit(address indexed dst, uint256 wad)",
"event Withdrawal(address indexed src, uint256 wad)"
];
// Helper function to check if ethers is loaded
function ensureEthers() {
// Check immediately - ethers might already be loaded
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
return Promise.resolve(true);
}
// Wait for ethers to load if it's still loading
return new Promise((resolve, reject) => {
let resolved = false;
// Check immediately first (double check)
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
resolve(true);
return;
}
// Wait for ethersReady event
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
// Final check before rejecting
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
resolve(true);
} else {
console.error('Ethers library failed to load after 20 seconds');
reject(new Error('Ethers library failed to load. Please refresh the page.'));
}
}
}, 20000); // 20 second timeout
const checkInterval = setInterval(() => {
if (typeof ethers !== 'undefined' && !resolved) {
resolved = true;
clearInterval(checkInterval);
clearTimeout(timeout);
window.ethersReady = true;
console.log('✅ Ethers detected via polling');
resolve(true);
}
}, 100);
// Listen for ethersReady event
const onReady = function() {
if (!resolved) {
resolved = true;
clearInterval(checkInterval);
clearTimeout(timeout);
window.removeEventListener('ethersReady', onReady);
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
console.log('✅ Ethers ready via event');
resolve(true);
} else {
console.error('ethersReady event fired but ethers is still undefined');
reject(new Error('Ethers library is not loaded. Please refresh the page.'));
}
}
};
window.addEventListener('ethersReady', onReady, { once: true });
});
}
// Helper function to get API URL based on chain ID
function getAPIUrl(endpoint) {
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
return `${BLOCKSCOUT_API}${endpoint}`;
}
// For other networks, use v2 Etherscan/Blockscan APIs
return `${API_BASE}/v2${endpoint}`;
}
// Initialize - only run once
let initialized = false;
document.addEventListener('DOMContentLoaded', async () => {
if (initialized) {
console.warn('Initialization already completed, skipping...');
return;
}
initialized = true;
applyStoredTheme();
var localeSel = document.getElementById('localeSelect'); if (localeSel) localeSel.value = currentLocale;
if (typeof applyI18n === 'function') applyI18n();
var initialRoute = (window.location.pathname || '/').replace(/^\//, '').replace(/\/$/, '').replace(/^index\.html$/i, '');
applyHashRoute();
window.addEventListener('popstate', function() { applyHashRoute(); });
window.addEventListener('hashchange', function() { applyHashRoute(); });
window.addEventListener('load', function() { applyHashRoute(); });
var shouldLoadHomeData = !initialRoute || initialRoute === 'home';
if (shouldLoadHomeData) {
console.log('Loading stats, blocks, and transactions...');
loadStats();
loadLatestBlocks();
loadLatestTransactions();
startTransactionUpdates();
} else {
setTimeout(function() { applyHashRoute(); }, 0);
}
// Ethers is only needed for MetaMask/WETH; don't block feeds on it
try {
await ensureEthers();
console.log('Ethers ready.');
} catch (error) {
console.warn('Ethers not ready, continuing without MetaMask features:', error);
}
setTimeout(() => {
if (typeof ethers !== 'undefined' && typeof window.ethereum !== 'undefined') {
checkMetaMaskConnection();
}
}, 500);
});
// MetaMask Connection
let checkingMetaMask = false;
async function checkMetaMaskConnection() {
// Prevent multiple simultaneous checks
if (checkingMetaMask) {
console.log('checkMetaMaskConnection already in progress, skipping...');
return;
}
checkingMetaMask = true;
try {
// Ensure ethers is loaded before checking MetaMask
if (typeof ethers === 'undefined') {
try {
await ensureEthers();
// Wait a bit more to ensure ethers is fully initialized
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.warn('Ethers not available, skipping MetaMask check:', error);
return;
}
}
// Double-check ethers is available
if (typeof ethers === 'undefined') {
console.warn('Ethers still not available after ensureEthers(), skipping MetaMask check');
return;
}
if (typeof window.ethereum !== 'undefined') {
try {
const accounts = await window.ethereum.request({ method: 'eth_accounts' });
if (accounts.length > 0) {
await connectMetaMask();
}
} catch (_) {
// User has not authorized the site yet; skip auto-connect silently
}
}
} finally {
checkingMetaMask = false;
}
}
var ERC20_META_ABI = ['function symbol() view returns (string)', 'function name() view returns (string)', 'function decimals() view returns (uint8)'];
var SYMBOL_SELECTOR = '0x95d89b41';
var NAME_SELECTOR = '0x06fdde03';
var DECIMALS_SELECTOR = '0x313ce567';
function decodeBytes32OrString(hex) {
if (!hex || typeof hex !== 'string' || hex.length < 66) return '';
var data = hex.slice(2);
if (data.length >= 64) {
var offset = parseInt(data.slice(0, 64), 16);
var len = parseInt(data.slice(64, 128), 16);
if (offset === 32 && len > 0 && data.length >= 128 + len * 2) {
return ethers.utils.toUtf8String('0x' + data.slice(128, 128 + len * 2)).replace(/\0+$/g, '');
}
var fixed = '0x' + data.slice(0, 64);
try {
return ethers.utils.parseBytes32String(fixed).replace(/\0+$/g, '');
} catch (_) {
return ethers.utils.toUtf8String(fixed).replace(/\0+$/g, '');
}
}
return '';
}
async function fetchTokenMetadataFromChain(address) {
if (typeof window.ethereum === 'undefined' || typeof ethers === 'undefined') return null;
var prov = new ethers.providers.Web3Provider(window.ethereum);
try {
var contract = new ethers.Contract(address, ERC20_META_ABI, prov);
var sym = await contract.symbol();
var nam = await contract.name();
var dec = await contract.decimals();
var decimalsNum = (typeof dec === 'number') ? dec : (dec && dec.toNumber ? dec.toNumber() : 18);
var symbolStr = (sym != null && sym !== undefined) ? String(sym) : '';
var nameStr = (nam != null && nam !== undefined) ? String(nam) : '';
return { symbol: symbolStr, name: nameStr, decimals: decimalsNum };
} catch (e) {
try {
var outSymbol = await window.ethereum.request({ method: 'eth_call', params: [{ to: address, data: SYMBOL_SELECTOR }] });
var outName = await window.ethereum.request({ method: 'eth_call', params: [{ to: address, data: NAME_SELECTOR }] });
var outDecimals = await window.ethereum.request({ method: 'eth_call', params: [{ to: address, data: DECIMALS_SELECTOR }] });
if (outSymbol && outSymbol !== '0x') {
var symbolStr = decodeBytes32OrString(outSymbol);
var nameStr = outName && outName !== '0x' ? decodeBytes32OrString(outName) : '';
var decimalsNum = 18;
if (outDecimals && outDecimals.length >= 66) {
decimalsNum = parseInt(outDecimals.slice(2, 66), 16);
if (isNaN(decimalsNum)) decimalsNum = 18;
}
return { symbol: symbolStr, name: nameStr, decimals: decimalsNum };
}
} catch (_) {}
return null;
}
}
async function addTokenToWallet(address, symbol, decimals, name) {
if (!address || !/^0x[a-fA-F0-9]{40}$/i.test(address)) {
if (typeof showToast === 'function') showToast('Invalid token address', 'error');
return;
}
if (typeof window.ethereum === 'undefined') {
if (typeof showToast === 'function') showToast('No wallet detected. Install MetaMask or another Web3 wallet.', 'error');
return;
}
try {
var meta = await fetchTokenMetadataFromChain(address);
if (!meta) {
if (typeof showToast === 'function') showToast('Could not read token from chain. Switch to the correct network and try again.', 'error');
return;
}
var useSymbol = (meta.symbol !== undefined && meta.symbol !== null) ? meta.symbol : 'TOKEN';
var useName = (meta.name !== undefined && meta.name !== null) ? meta.name : (name || '');
var useDecimals = (typeof meta.decimals === 'number') ? meta.decimals : (typeof decimals === 'number' ? decimals : 18);
if (useSymbol === '' || (typeof useSymbol === 'string' && useSymbol.trim() === '')) {
if (typeof showToast === 'function') showToast('This token has no symbol on-chain. Add it manually in MetaMask: use this contract address and set symbol to WETH.', 'info');
return;
}
var added = await window.ethereum.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20',
options: {
address: address,
symbol: (useSymbol !== undefined && useSymbol !== null) ? useSymbol : 'TOKEN',
decimals: useDecimals,
name: useName || undefined,
image: undefined
}
}
});
if (typeof showToast === 'function') {
var displaySym = useSymbol || symbol || 'Token';
showToast(added ? (displaySym ? displaySym + ' added to wallet' : 'Token added to wallet') : 'Add token was cancelled', added ? 'success' : 'info');
}
} catch (e) {
var msg = (e && e.message) ? String(e.message) : '';
var friendly = (e && e.code === 4001) || /not been authorized|rejected|denied/i.test(msg)
? 'Please approve the MetaMask popup to add the token.'
: (msg || 'Could not add token to wallet.');
if (typeof showToast === 'function') showToast(friendly, 'error');
}
}
window.addTokenToWallet = addTokenToWallet;
let connectingMetaMask = false;
async function connectMetaMask() {
// Prevent multiple simultaneous connections
if (connectingMetaMask) {
console.log('connectMetaMask already in progress, skipping...');
return;
}
connectingMetaMask = true;
try {
if (typeof window.ethereum === 'undefined') {
alert('MetaMask is not installed! Please install MetaMask to use WETH utilities.');
return;
}
// Wait for ethers to be loaded
if (typeof ethers === 'undefined') {
try {
await ensureEthers();
// Wait a bit more to ensure ethers is fully initialized
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
alert('Ethers library is not loaded. Please refresh the page and try again.');
console.error('ethers loading error:', error);
return;
}
}
// Double-check ethers is available
if (typeof ethers === 'undefined') {
alert('Ethers library is not loaded. Please refresh the page and try again.');
console.error('Ethers still not available after ensureEthers()');
return;
}
try {
// Request account access
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
userAddress = accounts[0];
// Connect to Chain 138
await switchToChain138();
// Setup provider and signer
provider = new ethers.providers.Web3Provider(window.ethereum);
signer = provider.getSigner();
// Update UI
const statusEl = document.getElementById('metamaskStatus');
statusEl.className = 'metamask-status connected';
statusEl.innerHTML = `
<i class="fas fa-check-circle"></i>
<span>Connected: ${escapeHtml(shortenHash(userAddress))}</span>
<button class="btn btn-warning" onclick="disconnectMetaMask()" style="margin-left: auto;" aria-label="Disconnect MetaMask wallet">Disconnect</button>
`;
// Enable buttons
document.getElementById('weth9WrapBtn').disabled = false;
document.getElementById('weth9UnwrapBtn').disabled = false;
document.getElementById('weth10WrapBtn').disabled = false;
document.getElementById('weth10UnwrapBtn').disabled = false;
// Load balances
await refreshWETHBalances();
// Listen for account changes
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
disconnectMetaMask();
} else {
connectMetaMask();
}
});
// Listen for chain changes
window.ethereum.on('chainChanged', () => {
switchToChain138();
});
} catch (error) {
var errMsg = (error && error.message) ? String(error.message) : '';
var friendly = (error && error.code === 4001) || /not been authorized|rejected|denied/i.test(errMsg)
? 'Connection was rejected. Click Connect Wallet and approve access when MetaMask asks.'
: (errMsg.includes('ethers is not defined') || typeof ethers === 'undefined')
? 'Ethers library failed to load. Please refresh the page.'
: ('Failed to connect MetaMask: ' + (errMsg || 'Unknown error'));
alert(friendly);
}
} finally {
connectingMetaMask = false;
}
}
async function switchToChain138() {
const chainId = '0x8A'; // 138 in hex
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId }],
});
} catch (switchError) {
// If chain doesn't exist, add it
if (switchError.code === 4902) {
try {
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId,
chainName: 'Chain 138',
nativeCurrency: {
name: 'ETH',
symbol: 'ETH',
decimals: 18
},
rpcUrls: RPC_URLS.length > 0 ? RPC_URLS : [RPC_URL],
blockExplorerUrls: [window.location.origin || 'https://explorer.d-bis.org']
}],
});
} catch (addError) {
throw addError;
}
} else {
throw switchError;
}
}
}
function disconnectMetaMask() {
provider = null;
signer = null;
userAddress = null;
const statusEl = document.getElementById('metamaskStatus');
statusEl.className = 'metamask-status disconnected';
statusEl.innerHTML = `
<i class="fas fa-wallet"></i>
<span>MetaMask not connected</span>
<button class="btn btn-success" onclick="connectMetaMask()" style="margin-left: auto;" aria-label="Connect MetaMask wallet">Connect MetaMask</button>
`;
document.getElementById('weth9WrapBtn').disabled = true;
document.getElementById('weth9UnwrapBtn').disabled = true;
document.getElementById('weth10WrapBtn').disabled = true;
document.getElementById('weth10UnwrapBtn').disabled = true;
}
async function refreshWETHBalances() {
if (!userAddress) return;
try {
await ensureEthers();
// Checksum addresses when ethers is available
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
// Convert to lowercase first, then checksum
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
// Fallback to lowercase version
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
// Fallback to lowercase if ethers not available
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
// Get ETH balance
const ethBalance = await provider.getBalance(userAddress);
const ethBalanceFormatted = formatEther(ethBalance);
// Get WETH9 balance
const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, provider);
const weth9Balance = await weth9Contract.balanceOf(userAddress);
const weth9BalanceFormatted = formatEther(weth9Balance);
// Get WETH10 balance - use checksummed address
const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, provider);
const weth10Balance = await weth10Contract.balanceOf(userAddress);
const weth10BalanceFormatted = formatEther(weth10Balance);
// Update UI
document.getElementById('weth9EthBalance').textContent = ethBalanceFormatted + ' ETH';
document.getElementById('weth9TokenBalance').textContent = weth9BalanceFormatted + ' WETH9';
document.getElementById('weth10EthBalance').textContent = ethBalanceFormatted + ' ETH';
document.getElementById('weth10TokenBalance').textContent = weth10BalanceFormatted + ' WETH10';
} catch (error) {
console.error('Error refreshing balances:', error);
}
}
function wrapUnwrapErrorMessage(op, error) {
if (error && (error.code === 4001 || error.code === 'ACTION_REJECTED' || (error.message && /user rejected|user denied/i.test(error.message)))) return 'Transaction cancelled.';
if (error && error.reason) return error.reason;
return (error && error.message) ? error.message : 'Unknown error';
}
function setMaxWETH9(type) {
if (type === 'wrap') {
const ethBalance = document.getElementById('weth9EthBalance').textContent.replace(' ETH', '');
document.getElementById('weth9WrapAmount').value = parseFloat(ethBalance).toFixed(6);
} else {
const wethBalance = document.getElementById('weth9TokenBalance').textContent.replace(' WETH9', '');
document.getElementById('weth9UnwrapAmount').value = parseFloat(wethBalance).toFixed(6);
}
}
function setMaxWETH10(type) {
if (type === 'wrap') {
const ethBalance = document.getElementById('weth10EthBalance').textContent.replace(' ETH', '');
document.getElementById('weth10WrapAmount').value = parseFloat(ethBalance).toFixed(6);
} else {
const wethBalance = document.getElementById('weth10TokenBalance').textContent.replace(' WETH10', '');
document.getElementById('weth10UnwrapAmount').value = parseFloat(wethBalance).toFixed(6);
}
}
async function wrapWETH9() {
const amount = document.getElementById('weth9WrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const ethBalance = await provider.getBalance(userAddress);
if (ethBalance.lt(amountWei)) {
alert('Insufficient ETH balance. You have ' + formatEther(ethBalance) + ' ETH.');
return;
}
const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, signer);
try {
await weth9Contract.callStatic.deposit({ value: amountWei });
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth9WrapBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth9Contract.deposit({ value: amountWei });
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth9WrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH9';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error wrapping WETH9:', error);
alert('Wrap WETH9: ' + wrapUnwrapErrorMessage('wrap', error));
document.getElementById('weth9WrapBtn').innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH9';
document.getElementById('weth9WrapBtn').disabled = false;
}
}
async function unwrapWETH9() {
const amount = document.getElementById('weth9UnwrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, signer);
const wethBalance = await weth9Contract.balanceOf(userAddress);
if (wethBalance.lt(amountWei)) {
alert('Insufficient WETH9 balance. You have ' + formatEther(wethBalance) + ' WETH9.');
return;
}
try {
await weth9Contract.callStatic.withdraw(amountWei);
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth9UnwrapBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth9Contract.withdraw(amountWei);
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth9UnwrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH9 to ETH';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error unwrapping WETH9:', error);
alert('Unwrap WETH9: ' + wrapUnwrapErrorMessage('unwrap', error));
document.getElementById('weth9UnwrapBtn').innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH9 to ETH';
document.getElementById('weth9UnwrapBtn').disabled = false;
}
}
async function wrapWETH10() {
const amount = document.getElementById('weth10WrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
// Ensure address is checksummed
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const ethBalance = await provider.getBalance(userAddress);
if (ethBalance.lt(amountWei)) {
alert('Insufficient ETH balance. You have ' + formatEther(ethBalance) + ' ETH.');
return;
}
const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, signer);
try {
await weth10Contract.callStatic.deposit({ value: amountWei });
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth10WrapBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth10Contract.deposit({ value: amountWei });
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth10WrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH10';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error wrapping WETH10:', error);
alert('Wrap WETH10: ' + wrapUnwrapErrorMessage('wrap', error));
document.getElementById('weth10WrapBtn').innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH10';
document.getElementById('weth10WrapBtn').disabled = false;
}
}
async function unwrapWETH10() {
const amount = document.getElementById('weth10UnwrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
// Ensure address is checksummed
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, signer);
const wethBalance = await weth10Contract.balanceOf(userAddress);
if (wethBalance.lt(amountWei)) {
alert('Insufficient WETH10 balance. You have ' + formatEther(wethBalance) + ' WETH10.');
return;
}
try {
await weth10Contract.callStatic.withdraw(amountWei);
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth10UnwrapBtn');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth10Contract.withdraw(amountWei);
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth10UnwrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH10 to ETH';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error unwrapping WETH10:', error);
alert('Unwrap WETH10: ' + wrapUnwrapErrorMessage('unwrap', error));
document.getElementById('weth10UnwrapBtn').innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH10 to ETH';
document.getElementById('weth10UnwrapBtn').disabled = false;
}
}
function showWETHTab(tab, clickedElement) {
document.querySelectorAll('.weth-tab-content').forEach(el => el.style.display = 'none');
document.querySelectorAll('.weth-tab').forEach(el => el.classList.remove('active'));
const tabElement = document.getElementById(`${tab}Tab`);
if (tabElement) {
tabElement.style.display = 'block';
}
// Update active tab - use clickedElement if provided, otherwise find by tab name
if (clickedElement) {
clickedElement.classList.add('active');
} else {
// Find the button that corresponds to this tab
const tabButtons = document.querySelectorAll('.weth-tab');
tabButtons.forEach(btn => {
if (btn.getAttribute('onclick')?.includes(`'${tab}'`)) {
btn.classList.add('active');
}
});
}
}
window.showWETHTab = showWETHTab;
async function renderWETHUtilitiesView() {
showView('weth');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'weth') updatePath('/weth');
if (userAddress) {
await refreshWETHBalances();
}
}
window._showWETHUtilities = renderWETHUtilitiesView;
async function renderBridgeView() {
showView('bridge');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'bridge') updatePath('/bridge');
await refreshBridgeData();
}
window._showBridgeMonitoring = renderBridgeView;
async function renderHomeView() {
showView('home');
if ((window.location.pathname || '').replace(/\/$/, '') !== '') updatePath('/');
await loadStats();
await loadLatestBlocks();
await loadLatestTransactions();
// Start real-time transaction updates
startTransactionUpdates();
}
window._showHome = renderHomeView;
async function renderBlocksView() {
showView('blocks');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'blocks') updatePath('/blocks');
await loadAllBlocks();
}
window._showBlocks = renderBlocksView;
async function renderTransactionsView() {
showView('transactions');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'transactions') updatePath('/transactions');
await loadAllTransactions();
}
window._showTransactions = renderTransactionsView;
async function renderAddressesView() {
showView('addresses');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'addresses') updatePath('/addresses');
await loadAllAddresses();
}
window._showAddresses = renderAddressesView;
function buildAnalyticsViewHtml() {
var html = '';
html += '<div style="display:grid; gap:1rem;">';
html += '<div class="card" style="margin:0; box-shadow:none;">';
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">';
html += '<div>';
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-chart-line"></i> Live Network Analytics</h3>';
html += '<div style="color:var(--text-light); line-height:1.55;">Analytics surfaces are consolidated into the live explorer dashboards instead of a separate unfinished panel. Use this page as a hub to the active gas, block, bridge, and route monitoring views.</div>';
html += '</div>';
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">';
html += '<button type="button" class="btn btn-primary" onclick="showHome(); updatePath(\'/\')" aria-label="Open network dashboard"><i class="fas fa-gauge-high"></i> Network dashboard</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showBridgeMonitoring(); updatePath(\'/bridge\')" aria-label="Open bridge monitoring"><i class="fas fa-bridge"></i> Bridge monitoring</button>';
html += '</div>';
html += '</div>';
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem;">';
html += '<a href="/" onclick="event.preventDefault(); showHome(); updatePath(\'/\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Gas &amp; Network</div><div style="color:var(--text-light); line-height:1.5;">Open the live home dashboard for gas price, TPS, block time, validator count, and latest-chain activity.</div></a>';
html += '<a href="/blocks" onclick="event.preventDefault(); showBlocks(); updatePath(\'/blocks\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Block cadence</div><div style="color:var(--text-light); line-height:1.5;">Inspect live block production, miner attribution, gas usage, and exportable block history.</div></a>';
html += '<a href="/transactions" onclick="event.preventDefault(); showTransactions(); updatePath(\'/transactions\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Transaction flow</div><div style="color:var(--text-light); line-height:1.5;">Review the recent transaction stream and drill into decoded execution details and internal calls.</div></a>';
html += '<a href="/routes" onclick="event.preventDefault(); showRoutes(); updatePath(\'/routes\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Route coverage</div><div style="color:var(--text-light); line-height:1.5;">Open the dedicated route-decision tree for swap-path coverage, bridge branches, and missing quote-token diagnostics.</div></a>';
html += '</div>';
html += '</div>';
return html;
}
// Analytics view
function renderAnalyticsView() {
showView('analytics');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'analytics') updatePath('/analytics');
var container = document.getElementById('analyticsContent');
if (!container) return;
container.innerHTML = buildAnalyticsViewHtml();
}
window._showAnalytics = renderAnalyticsView;
function buildOperatorViewHtml() {
var html = '';
html += '<div style="display:grid; gap:1rem;">';
html += '<div class="card" style="margin:0; box-shadow:none;">';
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">';
html += '<div>';
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-server"></i> Operator Access Hub</h3>';
html += '<div style="color:var(--text-light); line-height:1.55;">The explorer does not expose raw privileged controls here. Instead, this page collects the live operator-facing observability and execution surfaces that are safe to browse from the public UI.</div>';
html += '</div>';
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">';
html += '<button type="button" class="btn btn-primary" onclick="showBridgeMonitoring(); updatePath(\'/bridge\')" aria-label="Open bridge monitoring"><i class="fas fa-bridge"></i> Bridge status</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\')" aria-label="Open liquidity access"><i class="fas fa-wave-square"></i> Liquidity APIs</button>';
html += '</div>';
html += '</div>';
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem;">';
html += '<a href="/bridge" onclick="event.preventDefault(); showBridgeMonitoring(); updatePath(\'/bridge\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Bridge monitoring</div><div style="color:var(--text-light); line-height:1.5;">Inspect bridge balances, fee-token posture, destination configuration, and live contract references.</div></a>';
html += '<a href="/liquidity" onclick="event.preventDefault(); showLiquidityAccess(); updatePath(\'/liquidity\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Liquidity access</div><div style="color:var(--text-light); line-height:1.5;">Jump to partner payload routes, ingestion APIs, and public execution-plan endpoints without leaving the explorer.</div></a>';
html += '<a href="/pools" onclick="event.preventDefault(); showPools(); updatePath(\'/pools\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Pool inventory</div><div style="color:var(--text-light); line-height:1.5;">Review canonical PMM addresses, funding state, registry status, and exportable pool snapshots.</div></a>';
html += '<a href="/weth" onclick="event.preventDefault(); showWETHUtilities(); updatePath(\'/weth\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">WETH utilities</div><div style="color:var(--text-light); line-height:1.5;">Open the WETH9/WETH10 utilities, bridge contract references, and balance tools that operators often need during support.</div></a>';
html += '</div>';
html += '</div>';
return html;
}
// Operator view
function renderOperatorView() {
showView('operator');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'operator') updatePath('/operator');
var container = document.getElementById('operatorContent');
if (!container) return;
container.innerHTML = buildOperatorViewHtml();
}
window._showOperator = renderOperatorView;
function showView(viewName) {
currentView = viewName;
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','addresses','pools','routes','liquidity','more','analytics','operator'];
if (detailViews.indexOf(viewName) === -1) currentDetailKey = '';
document.querySelectorAll('.detail-view').forEach(v => v.classList.remove('active'));
const homeView = document.getElementById('homeView');
if (homeView) homeView.style.display = viewName === 'home' ? 'block' : 'none';
if (viewName !== 'home') {
const targetView = document.getElementById(`${viewName}View`);
if (targetView) targetView.classList.add('active');
stopTransactionUpdates();
}
}
window.showView = showView;
function toggleDarkMode() {
document.body.classList.toggle('dark-theme');
var isDark = document.body.classList.contains('dark-theme');
try { localStorage.setItem('explorerTheme', isDark ? 'dark' : 'light'); } catch (e) {}
var icon = document.getElementById('themeIcon');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
}
}
function applyStoredTheme() {
try {
var theme = localStorage.getItem('explorerTheme');
if (theme === 'dark') {
document.body.classList.add('dark-theme');
var icon = document.getElementById('themeIcon');
if (icon) icon.className = 'fas fa-sun';
}
} catch (e) {}
}
function updatePath(path) {
if (typeof history !== 'undefined' && history.pushState) {
history.pushState(null, '', path);
}
}
window.updatePath = updatePath;
function applyHashRoute() {
var route = '';
var fromPath = (window.location.pathname || '/').replace(/^\//, '').replace(/\/$/, '').replace(/^index\.html$/i, '');
var fromHash = (window.location.hash || '').replace(/^#/, '');
if (fromPath && fromPath !== '') {
route = fromPath;
} else if (fromHash) {
route = fromHash;
}
if (!route || route === 'home') { if (currentView !== 'home') showHome(); return; }
var parts = route.split('/').filter(Boolean);
var decode = function(s) { try { return decodeURIComponent(s); } catch (e) { return s; } };
if (parts[0] === 'block' && parts[1]) { var p1 = decode(parts[1]); var key = 'block:' + p1; if (currentDetailKey === key) return; currentDetailKey = key; setTimeout(function() { showBlockDetail(p1); }, 0); return; }
if (parts[0] === 'tx' && parts[1]) { var p1 = decode(parts[1]); var txKey = 'tx:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)); if (currentDetailKey === txKey) return; currentDetailKey = txKey; setTimeout(function() { showTransactionDetail(p1); }, 0); return; }
if (parts[0] === 'address' && parts[1]) { var p1 = decode(parts[1]); var addrKey = 'address:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)); if (currentDetailKey === addrKey) return; currentDetailKey = addrKey; setTimeout(function() { showAddressDetail(p1); }, 0); return; }
if (parts[0] === 'token' && parts[1]) { var p1 = decode(parts[1]); var tokKey = 'token:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)); if (currentDetailKey === tokKey) return; currentDetailKey = tokKey; setTimeout(function() { showTokenDetail(p1); }, 0); return; }
if (parts[0] === 'nft' && parts[1] && parts[2]) { var p1 = decode(parts[1]), p2 = decode(parts[2]); var nftKey = 'nft:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)) + ':' + p2; if (currentDetailKey === nftKey) return; currentDetailKey = nftKey; setTimeout(function() { showNftDetail(p1, p2); }, 0); return; }
if (parts[0] === 'home') { if (currentView !== 'home') showHome(); return; }
if (parts[0] === 'blocks') { if (currentView !== 'blocks') showBlocks(); return; }
if (parts[0] === 'transactions') { if (currentView !== 'transactions') showTransactions(); return; }
if (parts[0] === 'addresses') { if (currentView !== 'addresses') showAddresses(); return; }
if (parts[0] === 'bridge') { if (currentView !== 'bridge') showBridgeMonitoring(); return; }
if (parts[0] === 'weth') { if (currentView !== 'weth') showWETHUtilities(); return; }
if (parts[0] === 'watchlist') { if (currentView !== 'watchlist') showWatchlist(); return; }
if (parts[0] === 'pools') { if (currentView !== 'pools') openPoolsView(); return; }
if (parts[0] === 'routes') { if (currentView !== 'routes') showRoutes(); return; }
if (parts[0] === 'liquidity') { if (currentView !== 'liquidity') showLiquidityAccess(); return; }
if (parts[0] === 'more') { if (currentView !== 'more') showMore(); return; }
if (parts[0] === 'tokens') { if (typeof showTokensList === 'function') showTokensList(); else focusSearchWithHint('token'); return; }
if (parts[0] === 'analytics') { if (currentView !== 'analytics') showAnalytics(); return; }
if (parts[0] === 'operator') { if (currentView !== 'operator') showOperator(); return; }
}
window.applyHashRoute = applyHashRoute;
var hasRouteOnReady = window.location.hash || ((window.location.pathname || '').replace(/^\//, '').replace(/\/$/, ''));
if (document.readyState !== 'loading' && hasRouteOnReady) { applyHashRoute(); }
window.toggleDarkMode = toggleDarkMode;
function focusSearchWithHint(kind) {
var prefill = kind === 'token' ? 'token' : '';
if (kind === 'token') {
showToast('Enter token contract address (0x...) or search by name/symbol', 'info', 4000);
}
openSmartSearchModal(prefill);
}
window.focusSearchWithHint = focusSearchWithHint;
function toggleNavMenu() {
var links = document.getElementById('navLinks');
var btn = document.getElementById('navToggle');
var icon = document.getElementById('navToggleIcon');
if (!links || !btn) return;
var isOpen = links.classList.toggle('nav-open');
btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
if (icon) icon.className = isOpen ? 'fas fa-times' : 'fas fa-bars';
}
function closeNavMenu() {
var links = document.getElementById('navLinks');
var btn = document.getElementById('navToggle');
var icon = document.getElementById('navToggleIcon');
if (links) links.classList.remove('nav-open');
if (btn) btn.setAttribute('aria-expanded', 'false');
if (icon) icon.className = 'fas fa-bars';
closeAllNavDropdowns();
}
function closeAllNavDropdowns() {
document.querySelectorAll('.nav-dropdown.open').forEach(function (el) {
el.classList.remove('open');
var trigger = el.querySelector('.nav-dropdown-trigger');
if (trigger) trigger.setAttribute('aria-expanded', 'false');
});
}
function initNavDropdowns() {
document.querySelectorAll('.nav-dropdown-trigger').forEach(function (trigger) {
trigger.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
var dropdown = trigger.closest('.nav-dropdown');
var wasOpen = dropdown && dropdown.classList.contains('open');
closeAllNavDropdowns();
if (dropdown && !wasOpen) {
dropdown.classList.add('open');
trigger.setAttribute('aria-expanded', 'true');
}
});
});
document.addEventListener('click', function () {
closeAllNavDropdowns();
});
document.getElementById('navLinks').addEventListener('click', function (e) {
if (e.target.closest('.nav-dropdown-menu')) e.stopPropagation();
});
}
window.toggleNavMenu = toggleNavMenu;
window.closeNavMenu = closeNavMenu;
window.closeAllNavDropdowns = closeAllNavDropdowns;
// Update breadcrumb navigation
function updateBreadcrumb(type, identifier, identifierExtra) {
let breadcrumbContainer;
let breadcrumbHTML = '<a href="/">Home</a>';
switch (type) {
case 'block':
breadcrumbContainer = document.getElementById('blockDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="/blocks">Blocks</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Block #' + escapeHtml(String(identifier)) + '</span>';
break;
case 'transaction':
breadcrumbContainer = document.getElementById('transactionDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="/transactions">Transactions</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'address':
breadcrumbContainer = document.getElementById('addressDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><a href="/addresses">Addresses</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'token':
breadcrumbContainer = document.getElementById('tokenDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="/tokens">Tokens</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Token ' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'pools':
breadcrumbContainer = document.getElementById('poolsBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Pools</span>';
break;
case 'routes':
breadcrumbContainer = document.getElementById('routesBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Routes</span>';
break;
case 'more':
breadcrumbContainer = document.getElementById('moreBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">More</span>';
break;
case 'nft':
breadcrumbContainer = document.getElementById('nftDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="/address/' + encodeURIComponent(identifier) + '">' + escapeHtml(shortenHash(identifier)) + '</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Token ID ' + (identifierExtra != null ? escapeHtml(String(identifierExtra)) : '') + '</span>';
break;
default:
return;
}
if (breadcrumbContainer) {
breadcrumbContainer.innerHTML = breadcrumbHTML;
}
}
// Retry logic with exponential backoff
async function fetchAPIWithRetry(url, maxRetries = FETCH_MAX_RETRIES, retryDelay = RETRY_DELAY_MS) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetchAPI(url);
} catch (error) {
const isLastAttempt = attempt === maxRetries - 1;
const isRetryable = error.name === 'AbortError' ||
(error.message && (error.message.includes('timeout') ||
error.message.includes('500') ||
error.message.includes('502') ||
error.message.includes('503') ||
error.message.includes('504') ||
error.message.includes('NetworkError')));
if (isLastAttempt || !isRetryable) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delay = retryDelay * Math.pow(2, attempt);
console.warn(`⚠️ API call failed (attempt ${attempt + 1}/${maxRetries}), retrying in ${delay}ms...`, error.message);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
async function fetchAPI(url) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Accept': 'application/json' },
credentials: 'omit',
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
let errorText = '';
try {
errorText = await response.text();
} catch (e) {
errorText = response.statusText;
}
// Log detailed error for debugging
const errorInfo = {
status: response.status,
statusText: response.statusText,
errorText: errorText.substring(0, 500),
url: url,
headers: Object.fromEntries(response.headers.entries())
};
console.error(`❌ API Error:`, errorInfo);
if (response.status === 502 || response.status === 503) {
showAPIUnavailableBanner(response.status);
}
// For 400 errors, provide more context
if (response.status === 400) {
console.error('🔍 HTTP 400 Bad Request Details:');
console.error('URL:', url);
console.error('Response Headers:', errorInfo.headers);
console.error('Error Body:', errorText);
console.error('Possible causes:');
console.error('1. Invalid query parameters');
console.error('2. Missing required parameters');
console.error('3. API endpoint format incorrect');
console.error('4. CORS preflight failed');
console.error('5. Request method not allowed');
// Try to parse error if it's JSON
try {
const errorJson = JSON.parse(errorText);
console.error('Parsed Error:', errorJson);
} catch (e) {
// Not JSON, that's fine
}
}
throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`);
}
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
const text = await response.text();
try {
return JSON.parse(text);
} catch (e) {
throw new Error(`Invalid JSON response: ${text.substring(0, 100)}`);
}
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout. Please try again.');
}
throw error;
}
}
async function postJSON(url, payload) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS * 2);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
credentials: 'omit',
signal: controller.signal,
body: JSON.stringify(payload || {})
});
clearTimeout(timeoutId);
const text = await response.text();
let parsed = {};
if (text) {
try {
parsed = JSON.parse(text);
} catch (e) {
parsed = { reply: text };
}
}
if (!response.ok) {
var message = (parsed && parsed.error && (parsed.error.message || parsed.error.code)) || text || response.statusText || 'Request failed';
throw new Error('HTTP ' + response.status + ': ' + message);
}
return parsed;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout. Please try again.');
}
throw error;
}
}
async function loadStats() {
const statsGrid = document.getElementById('statsGrid');
if (!statsGrid) return;
// Show skeleton loader
statsGrid.innerHTML = createSkeletonLoader('stats');
try {
let stats;
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
// Blockscout doesn't have a single stats endpoint, so we'll fetch from our API
// or use Blockscout's individual endpoints
try {
// Try our API first
stats = await fetchAPIWithRetry(`${API_BASE}/v2/stats`);
} catch (e) {
// Fallback: fetch from Blockscout and calculate
const [blocksRes, txsRes] = await Promise.all([
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks?page=1&page_size=1`).catch(() => null),
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?page=1&page_size=1`).catch(() => null)
]);
stats = {
total_blocks: blocksRes?.items?.[0]?.number || 0,
total_transactions: txsRes?.items?.length ? 'N/A' : 0,
total_addresses: 0
};
}
} else {
// For other networks, use v2 API
stats = await fetchAPIWithRetry(`${API_BASE}/v2/stats`);
}
const activeBridgeContracts = getActiveBridgeContractCount();
statsGrid.innerHTML = `
<div class="stat-card">
<div class="stat-label">Total Blocks</div>
<div class="stat-value">${formatNumber(stats.total_blocks || 0)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Transactions</div>
<div class="stat-value">${formatNumber(stats.total_transactions || 0)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Addresses</div>
<div class="stat-value">${formatNumber(stats.total_addresses || 0)}</div>
</div>
<div class="stat-card bridge-card">
<div class="stat-label">Bridge Contracts</div>
<div class="stat-value bridge-value">${activeBridgeContracts} Active</div>
</div>
<div class="stat-card" id="networkStatCard">
<div class="stat-label">Network</div>
<div class="stat-value" id="networkStatValue" style="font-size: 1rem;">Loading...</div>
</div>
`;
if (CHAIN_ID === 138) loadGasAndNetworkStats();
} catch (error) {
console.error('Failed to load stats:', error);
statsGrid.innerHTML = `
<div class="stat-card">
<div class="stat-label">Total Blocks</div>
<div class="stat-value">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Transactions</div>
<div class="stat-value">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Addresses</div>
<div class="stat-value">-</div>
</div>
<div class="stat-card bridge-card">
<div class="stat-label">Bridge Contracts</div>
<div class="stat-value bridge-value">${activeBridgeContracts} Active</div>
</div>
<div class="stat-card"><div class="stat-label">Network</div><div class="stat-value" style="font-size: 1rem;">-</div></div>
`;
}
}
async function loadGasAndNetworkStats() {
var el = document.getElementById('networkStatValue');
var gasCard = document.getElementById('gasNetworkCard');
var summaryEl = document.getElementById('gasNetworkSummary');
try {
var blocksResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/blocks?page=1&page_size=20');
var blocks = blocksResp.items || blocksResp || [];
var statsResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/stats').catch(function() { return {}; });
var gasGwei = '-';
var blockTimeSec = '-';
var tps = '-';
if (blocks.length > 0) {
var b = blocks[0];
var baseFee = b.base_fee_per_gas || b.base_fee;
if (baseFee != null) gasGwei = (Number(baseFee) / 1e9).toFixed(2) + ' Gwei';
}
if (blocks.length >= 2) {
var t0 = blocks[0].timestamp;
var t1 = blocks[1].timestamp;
if (t0 && t1) {
var d = (new Date(t0) - new Date(t1)) / 1000;
if (d > 0) blockTimeSec = d.toFixed(1) + 's';
}
}
if (statsResp.average_block_time != null) blockTimeSec = Number(statsResp.average_block_time).toFixed(1) + 's';
if (statsResp.transactions_per_second != null) tps = Number(statsResp.transactions_per_second).toFixed(2);
if (el) el.innerHTML = (gasGwei !== '-' ? 'Gas: ' + escapeHtml(gasGwei) + '<br/>' : '') + (blockTimeSec !== '-' ? 'Block: ' + escapeHtml(blockTimeSec) + '<br/>' : '') + (tps !== '-' ? 'TPS: ' + escapeHtml(tps) : '') || 'Gas / TPS';
if (summaryEl) {
summaryEl.textContent = 'Live chain health for Chain 138 updated ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + '.';
}
if (gasCard && CHAIN_ID === 138) {
var curEl = document.getElementById('gasCurrentValue');
var tpsEl = document.getElementById('gasTpsValue');
var btEl = document.getElementById('gasBlockTimeValue');
var failedEl = document.getElementById('gasFailedRateValue');
var barsEl = document.getElementById('gasHistoryBars');
if (curEl) curEl.textContent = gasGwei !== '-' ? gasGwei : '—';
if (tpsEl) tpsEl.textContent = tps !== '-' ? tps : '—';
if (btEl) btEl.textContent = blockTimeSec !== '-' ? blockTimeSec : '—';
if (failedEl) {
var txsResp = await fetch(BLOCKSCOUT_API + '/v2/transactions?page=1&page_size=100').then(function(r) { return r.json(); }).catch(function() { return { items: [] }; });
var txs = txsResp.items || txsResp || [];
var failed = txs.filter(function(t) { var s = t.status; return s === 0 || s === '0' || (t.block && t.block.success === false); }).length;
failedEl.textContent = txs.length > 0 ? (100 * failed / txs.length).toFixed(2) + '%' : '—';
}
if (barsEl) {
var recent = blocks.slice(0, 10);
var fees = recent.map(function(bl) { var f = bl.base_fee_per_gas || bl.base_fee; return f != null ? Number(f) / 1e9 : 0; });
var maxFee = Math.max.apply(null, fees) || 1;
barsEl.innerHTML = fees.map(function(g, i) {
var pct = maxFee > 0 ? (g / maxFee * 100) : 0;
return '<span title="Block ' + (recent[i] && (recent[i].height != null ? recent[i].height : recent[i].number) || '') + ': ' + g.toFixed(2) + ' Gwei" style="width: 8px; height: ' + (pct + 5) + '%; min-height: 4px; background: var(--primary); border-radius: 4px 4px 0 0;"></span>';
}).join('');
}
}
} catch (e) {
if (el) el.textContent = '-';
if (summaryEl) summaryEl.textContent = 'Live chain health unavailable right now.';
}
}
function normalizeBlockDisplay(block) {
var blockNum = block.number;
var hash = block.hash || '';
var txCount = block.transaction_count || 0;
var date = null;
if (block.timestamp) {
if (typeof block.timestamp === 'string' && block.timestamp.indexOf('0x') === 0) {
date = new Date(parseInt(block.timestamp, 16) * 1000);
} else {
date = new Date(block.timestamp);
}
}
var timestampFormatted = date && !isNaN(date.getTime()) ? date.toLocaleString() : 'N/A';
var timeAgo = date && !isNaN(date.getTime()) ? getTimeAgo(date) : 'N/A';
return { blockNum: blockNum, hash: hash, txCount: txCount, timestampFormatted: timestampFormatted, timeAgo: timeAgo };
}
function createBlockCardHtml(block, options) {
options = options || {};
var d = normalizeBlockDisplay(block);
var animationClass = options.animationClass || '';
return '<div class="block-card ' + escapeHtml(animationClass) + '" onclick="showBlockDetail(\'' + escapeHtml(String(d.blockNum)) + '\')">' +
'<div class="block-number">#' + escapeHtml(String(d.blockNum)) + '</div>' +
'<div class="block-hash">' + escapeHtml(shortenHash(d.hash)) + '</div>' +
'<div class="block-info">' +
'<div class="block-info-item"><span class="block-info-label">Transactions</span><span class="block-info-value">' + escapeHtml(String(d.txCount)) + '</span></div>' +
'<div class="block-info-item"><span class="block-info-label">Time</span><span class="block-info-value">' + escapeHtml(d.timeAgo) + '</span></div>' +
'</div></div>';
}
// Prevent multiple simultaneous calls
let loadingBlocks = false;
async function loadLatestBlocks() {
const container = document.getElementById('latestBlocks');
if (!container) return;
// Prevent multiple simultaneous calls
if (loadingBlocks) {
console.log('loadLatestBlocks already in progress, skipping...');
return;
}
loadingBlocks = true;
try {
let blocks = [];
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
try {
const blockscoutUrl = `${BLOCKSCOUT_API}/v2/blocks?page=1&page_size=10`;
console.log('Fetching blocks from Blockscout:', blockscoutUrl);
const response = await fetchAPIWithRetry(blockscoutUrl);
const raw = (response && (response.items || response.data || response.blocks)) || (Array.isArray(response) ? response : null);
if (raw && Array.isArray(raw)) {
blocks = raw.slice(0, 10).map(normalizeBlock).filter(b => b !== null);
console.log(`✅ Loaded ${blocks.length} blocks from Blockscout`);
} else if (response && typeof response === 'object') {
blocks = [];
console.warn('Blockscout blocks response empty or unexpected shape:', Object.keys(response || {}));
}
} catch (blockscoutError) {
console.warn('Blockscout API failed, trying RPC fallback:', blockscoutError.message);
try {
const blockNumHex = await rpcCall('eth_blockNumber');
const latestBlock = parseInt(blockNumHex, 16);
if (!isNaN(latestBlock) && latestBlock >= 0) {
for (let i = 0; i < Math.min(10, latestBlock + 1); i++) {
const bn = latestBlock - i;
const b = await rpcCall('eth_getBlockByNumber', ['0x' + bn.toString(16), false]);
if (b && b.number) blocks.push({ number: parseInt(b.number, 16), hash: b.hash, timestamp: b.timestamp ? (typeof b.timestamp === 'string' ? parseInt(b.timestamp, 16) * 1000 : b.timestamp) : null, transaction_count: b.transactions ? b.transactions.length : 0 });
}
if (blocks.length > 0) console.log('Loaded ' + blocks.length + ' blocks via RPC fallback');
}
} catch (rpcErr) {
console.error('RPC fallback also failed:', rpcErr);
if (container) {
container.innerHTML = '<div class="error">API temporarily unavailable. ' + escapeHtml((blockscoutError.message || 'Unknown error').substring(0, 150)) + ' <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
}
return;
}
if (blocks.length === 0 && container) {
container.innerHTML = '<div class="error">Could not load blocks. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
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 + '<div style="text-align: center; padding: 2rem; color: var(--text-light);">No blocks found. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
} else if (filteredBlocks.length === 0) {
if (container) container.innerHTML = filterBar + '<div style="text-align: center; padding: 2rem; color: var(--text-light);">No blocks match the current filter.</div>';
} else {
// Create HTML with duplicated blocks for seamless infinite loop
let html = filterBar + '<div class="blocks-scroll-container" id="blocksScrollContainer">';
html += '<div class="blocks-scroll-content">';
// First set of blocks (with animations for first 3)
filteredBlocks.forEach(function(block, index) {
var animationClass = index < 3 ? 'new-block' : '';
html += createBlockCardHtml(block, { animationClass: animationClass });
});
// Duplicate blocks for seamless infinite loop
filteredBlocks.forEach(function(block) {
html += createBlockCardHtml(block, {});
});
html += '</div></div>';
if (container) container.innerHTML = html;
// Setup auto-scroll animation
const scrollContainer = document.getElementById('blocksScrollContainer');
const scrollContent = scrollContainer?.querySelector('.blocks-scroll-content');
if (scrollContainer && scrollContent) {
const cardWidth = 200 + 16; // card width (200px) + gap (16px = 1rem)
const singleSetWidth = filteredBlocks.length * cardWidth;
// Use CSS transform for smooth animation
let scrollPosition = 0;
let isPaused = false;
const scrollSpeed = 0.4; // pixels per frame (adjust for speed)
scrollContainer.addEventListener('mouseenter', () => {
isPaused = true;
});
scrollContainer.addEventListener('mouseleave', () => {
isPaused = false;
});
if (_blocksScrollAnimationId != null) {
cancelAnimationFrame(_blocksScrollAnimationId);
_blocksScrollAnimationId = null;
}
function animateScroll() {
if (!isPaused && scrollContent) {
scrollPosition += scrollSpeed;
if (scrollPosition >= singleSetWidth) scrollPosition = 0;
scrollContent.style.transform = 'translateX(-' + scrollPosition + 'px)';
}
_blocksScrollAnimationId = requestAnimationFrame(animateScroll);
}
setTimeout(function() {
_blocksScrollAnimationId = requestAnimationFrame(animateScroll);
}, 500);
}
// Remove animation classes after animation completes
setTimeout(() => {
container.querySelectorAll('.new-block').forEach(card => {
card.classList.remove('new-block');
});
}, 1000);
}
} catch (error) {
console.error('Failed to load latest blocks:', error);
if (container) container.innerHTML = '<div class="error">Failed to load blocks: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
} 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 = '<div class="error">API temporarily unavailable. <button type="button" class="btn btn-primary" onclick="loadLatestTransactions()" style="margin-top: 0.5rem;">Retry</button></div>';
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 + '<table class="table"><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Value</th><th>Block</th></tr></thead><tbody>';
if (limitedTransactions.length === 0) {
html += '<tr><td colspan="5" style="text-align: center; padding: 1rem;">No transactions found</td></tr>';
} else if (filteredTransactions.length === 0) {
html += '<tr><td colspan="5" style="text-align: center; padding: 1rem;">No transactions match the current filter.</td></tr>';
} else {
filteredTransactions.forEach((tx, index) => {
// Transaction is already normalized by adapter
const hash = String(tx.hash || 'N/A');
const from = String(tx.from || 'N/A');
const to = String(tx.to || 'N/A');
const value = tx.value || '0';
const blockNumber = tx.block_number || 'N/A';
const valueFormatted = formatEther(value);
// Add animation class for new transactions
const isNew = newTransactions.some(ntx => String(ntx.hash || '') === hash);
const animationClass = isNew ? 'new-transaction' : '';
var fromClick = safeAddress(from) ? ' onclick="event.stopPropagation(); showAddressDetail(\'' + escapeHtml(from) + '\')" style="cursor: pointer;"' : '';
var toClick = safeAddress(to) ? ' onclick="event.stopPropagation(); showAddressDetail(\'' + escapeHtml(to) + '\')" style="cursor: pointer;"' : '';
html += '<tr class="' + animationClass + '" onclick="showTransactionDetail(\'' + escapeHtml(hash) + '\')" style="cursor: pointer;"><td class="hash">' + escapeHtml(shortenHash(hash)) + '</td><td class="hash"' + fromClick + '>' + formatAddressWithLabel(from) + '</td><td class="hash"' + toClick + '>' + (to ? formatAddressWithLabel(to) : '-') + '</td><td>' + escapeHtml(valueFormatted) + ' ETH</td><td>' + escapeHtml(String(blockNumber)) + '</td></tr>';
});
}
html += '</tbody></table>';
if (container) container.innerHTML = html;
if (container) {
setTimeout(() => {
container.querySelectorAll('.new-transaction').forEach(row => {
row.classList.remove('new-transaction');
});
}, 500);
}
} catch (error) {
console.error('Failed to load latest transactions:', error);
if (container) container.innerHTML = '<div class="error">Failed to load transactions: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '. <button type="button" class="btn btn-primary" onclick="loadLatestTransactions()" style="margin-top: 0.5rem;">Retry</button></div>';
}
}
// 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 = '<div class="loading"><i class="fas fa-spinner"></i> Loading blocks...</div>';
let blocks = [];
if (CHAIN_ID === 138) {
blocks = await fetchChain138BlocksPage(blocksListPage, LIST_PAGE_SIZE);
} else {
// For other networks, use Etherscan-compatible API
const blockData = await fetchAPI(`${API_BASE}?module=block&action=eth_block_number`);
const latestBlock = parseInt(blockData.result, 16);
// Load last 50 blocks
for (let i = 0; i < 50 && 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.result) {
blocks.push({
number: blockNum,
hash: block.result.hash,
timestamp: new Date(parseInt(block.result.timestamp, 16) * 1000).toISOString(),
transaction_count: block.result.transactions ? block.result.transactions.length : 0
});
}
} catch (e) {
// Skip failed blocks
}
}
}
const filter = getExplorerPageFilter('blocksList');
const filteredBlocks = filter ? blocks.filter(function(block) {
var d = normalizeBlockDisplay(block);
return matchesExplorerFilter([d.blockNum, d.hash, d.txCount, d.timestampFormatted, d.timeAgo].join(' '), filter);
}) : blocks;
const filterBar = renderPageFilterBar('blocksList', 'Filter blocks by number, hash, tx count, or age...', 'Filters the current page of blocks.', 'loadAllBlocks(' + blocksListPage + ')');
let html = filterBar + '<table class="table"><thead><tr><th>Block</th><th>Hash</th><th>Transactions</th><th>Timestamp</th></tr></thead><tbody>';
if (blocks.length === 0) {
html += '<tr><td colspan="4" style="text-align: center; padding: 2rem;">No blocks found</td></tr>';
} else if (filteredBlocks.length === 0) {
html += '<tr><td colspan="4" style="text-align: center; padding: 2rem;">No blocks match the current filter</td></tr>';
} else {
filteredBlocks.forEach(function(block) {
var d = normalizeBlockDisplay(block);
var blockNumber = escapeHtml(String(d.blockNum));
var blockHref = '/block/' + encodeURIComponent(String(d.blockNum));
var blockLink = '<a href="' + blockHref + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + blockNumber + '\')" style="color: inherit; text-decoration: none; font-weight: 600;">' + blockNumber + '</a>';
var hashLink = safeBlockNumber(d.blockNum) ? '<a class="hash" href="' + blockHref + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + blockNumber + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(shortenHash(d.hash)) + '</a>' : '<span class="hash">' + escapeHtml(shortenHash(d.hash)) + '</span>';
html += '<tr onclick="showBlockDetail(\'' + blockNumber + '\')" style="cursor: pointer;"><td>' + blockLink + '</td><td>' + hashLink + '</td><td>' + escapeHtml(String(d.txCount)) + '</td><td>' + escapeHtml(d.timestampFormatted) + '</td></tr>';
});
}
var pagination = '<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; flex-wrap: wrap; gap: 0.5rem;">';
pagination += '<span style="color: var(--text-light);">Page ' + blocksListPage + '</span>';
pagination += '<div style="display: flex; gap: 0.5rem;"><button type="button" class="btn btn-secondary" ' + (blocksListPage <= 1 ? 'disabled' : '') + ' onclick="loadAllBlocks(' + (blocksListPage - 1) + ')">Prev</button><button type="button" class="btn btn-secondary" ' + (blocks.length < LIST_PAGE_SIZE ? 'disabled' : '') + ' onclick="loadAllBlocks(' + (blocksListPage + 1) + ')">Next</button></div></div>';
html += '</tbody></table>' + pagination;
container.innerHTML = html;
} catch (error) {
container.innerHTML = '<div class="error">Failed to load blocks: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showBlocks()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
}
}
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 = '<div class="loading"><i class="fas fa-spinner"></i> Loading transactions...</div>';
let transactions = [];
if (CHAIN_ID === 138) {
transactions = await fetchChain138TransactionsPage(transactionsListPage, LIST_PAGE_SIZE);
} 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('Failed to get latest block number');
}
const latestBlock = parseInt(blockData.result, 16);
if (isNaN(latestBlock) || latestBlock < 0) {
throw new Error('Invalid block number');
}
const maxTxs = 50;
// Get transactions from recent blocks
for (let blockNum = latestBlock; blockNum >= 0 && transactions.length < maxTxs; blockNum--) {
try {
const block = await fetchAPI(`${API_BASE}?module=block&action=eth_get_block_by_number&tag=0x${blockNum.toString(16)}&boolean=true`);
if (block.result && block.result.transactions) {
for (const tx of block.result.transactions) {
if (transactions.length >= maxTxs) break;
if (typeof tx === 'object') {
transactions.push({
hash: tx.hash,
from: tx.from,
to: tx.to,
value: tx.value || '0',
block_number: blockNum
});
}
}
}
} catch (e) {
// Skip failed blocks
}
}
}
const filter = getExplorerPageFilter('transactionsList');
const filteredTransactions = filter ? transactions.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(' '), filter);
}) : transactions;
const filterBar = renderPageFilterBar('transactionsList', 'Filter transactions by hash, address, block, or value...', 'Filters the current page of transactions.', 'loadAllTransactions(' + transactionsListPage + ')');
let html = filterBar + '<table class="table"><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Value</th><th>Block</th></tr></thead><tbody>';
if (transactions.length === 0) {
html += '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No transactions found</td></tr>';
} else if (filteredTransactions.length === 0) {
html += '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No transactions match the current filter</td></tr>';
} else {
filteredTransactions.forEach(tx => {
const hash = String(tx.hash || 'N/A');
const from = String(tx.from || 'N/A');
const to = String(tx.to || 'N/A');
const value = tx.value || '0';
const blockNumber = tx.block_number || 'N/A';
const valueFormatted = formatEther(value);
var safeHash = escapeHtml(hash);
var txHref = '/tx/' + encodeURIComponent(hash);
var hashLink = '<a class="hash" href="' + txHref + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + safeHash + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(shortenHash(hash)) + '</a>';
var fromLink = safeAddress(from) ? '<a class="hash" href="/address/' + encodeURIComponent(from) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(from) + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel(from) + '</a>' : formatAddressWithLabel(from);
var toLink = safeAddress(to) ? '<a class="hash" href="/address/' + encodeURIComponent(to) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(to || '') + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel(to) + '</a>' : (to ? formatAddressWithLabel(to) : '-');
var blockLink = safeBlockNumber(blockNumber) ? '<a href="/block/' + encodeURIComponent(String(blockNumber)) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeHtml(String(blockNumber)) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(String(blockNumber)) + '</a>' : escapeHtml(String(blockNumber));
html += '<tr onclick="showTransactionDetail(\'' + safeHash + '\')" style="cursor: pointer;"><td>' + hashLink + '</td><td>' + fromLink + '</td><td>' + toLink + '</td><td>' + escapeHtml(valueFormatted) + ' ETH</td><td>' + blockLink + '</td></tr>';
});
}
var pagination = '<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; flex-wrap: wrap; gap: 0.5rem;">';
pagination += '<span style="color: var(--text-light);">Page ' + transactionsListPage + '</span>';
pagination += '<div style="display: flex; gap: 0.5rem;"><button type="button" class="btn btn-secondary" ' + (transactionsListPage <= 1 ? 'disabled' : '') + ' onclick="loadAllTransactions(' + (transactionsListPage - 1) + ')">Prev</button><button type="button" class="btn btn-secondary" ' + (transactions.length < LIST_PAGE_SIZE ? 'disabled' : '') + ' onclick="loadAllTransactions(' + (transactionsListPage + 1) + ')">Next</button></div></div>';
html += '</tbody></table>' + pagination;
container.innerHTML = html;
} catch (error) {
container.innerHTML = '<div class="error">Failed to load transactions: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showTransactions()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
}
}
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 = '<div class="loading"><i class="fas fa-spinner"></i> Loading addresses...</div>';
const params = new URLSearchParams({
page: String(addressesListPage),
page_size: String(LIST_PAGE_SIZE)
});
const q = getExplorerPageFilter('addressesList');
if (q) params.set('q', q);
let addresses = [];
if (CHAIN_ID === 138) {
try {
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/addresses?${params.toString()}`);
const items = response && (response.items || response.data);
if (Array.isArray(items) && items.length > 0) {
addresses = items.map(normalizeAddress).filter(function(addr) { return addr !== null; });
}
} catch (blockscoutError) {
console.warn('Blockscout address list failed, trying explorer API:', blockscoutError.message || blockscoutError);
}
}
if (addresses.length === 0) {
const response = await fetchAPIWithRetry(`${API_BASE}/v1/addresses?${params.toString()}`);
const items = response && (response.data || response.items || []);
addresses = Array.isArray(items) ? items.map(normalizeAddress).filter(function(addr) { return addr !== null; }) : [];
}
const filter = getExplorerPageFilter('addressesList');
const filteredAddresses = filter ? addresses.filter(function(item) {
return matchesExplorerFilter([
item.address,
item.label || '',
item.is_contract ? 'contract' : 'externally owned',
item.tx_sent,
item.tx_received,
item.transaction_count,
item.token_count,
item.first_seen_at || '',
item.last_seen_at || ''
].join(' '), filter);
}) : addresses;
const filterBar = renderPageFilterBar('addressesList', 'Filter addresses by address, label, type, or activity...', 'Filters the indexed address list below.', 'loadAllAddresses(' + addressesListPage + ')');
let html = filterBar + '<table class="table"><thead><tr><th>Address</th><th>Label</th><th>Type</th><th>Tx Sent</th><th>Tx Received</th><th>Tokens</th><th>Last Seen</th></tr></thead><tbody>';
if (addresses.length === 0) {
html += '<tr><td colspan="7" style="text-align: center; padding: 2rem;">No addresses found</td></tr>';
} else if (filteredAddresses.length === 0) {
html += '<tr><td colspan="7" style="text-align: center; padding: 2rem;">No addresses match the current filter</td></tr>';
} else {
filteredAddresses.forEach(function(item) {
var addr = String(item.address || '');
var label = String(item.label || '');
var isContract = !!item.is_contract;
var type = isContract ? 'Contract' : 'EOA';
var txSent = Number(item.tx_sent || 0);
var txReceived = Number(item.tx_received || 0);
var tokenCount = Number(item.token_count || 0);
var lastSeen = String(item.last_seen_at || '—');
html += '<tr style="cursor: pointer;" onclick="showAddressDetail(\'' + escapeHtml(addr) + '\')">';
html += '<td><a class="hash" href="/address/' + encodeURIComponent(addr) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(addr) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(shortenHash(addr)) + '</a></td>';
html += '<td>' + escapeHtml(label || '—') + '</td>';
html += '<td>' + escapeHtml(type) + '</td>';
html += '<td>' + escapeHtml(String(txSent)) + '</td>';
html += '<td>' + escapeHtml(String(txReceived)) + '</td>';
html += '<td>' + escapeHtml(String(tokenCount)) + '</td>';
html += '<td>' + escapeHtml(lastSeen) + '</td>';
html += '</tr>';
});
}
var pagination = '<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; flex-wrap: wrap; gap: 0.5rem;">';
pagination += '<span style="color: var(--text-light);">Page ' + addressesListPage + '</span>';
pagination += '<div style="display: flex; gap: 0.5rem;"><button type="button" class="btn btn-secondary" ' + (addressesListPage <= 1 ? 'disabled' : '') + ' onclick="loadAllAddresses(' + (addressesListPage - 1) + ')">Prev</button><button type="button" class="btn btn-secondary" ' + (addresses.length < LIST_PAGE_SIZE ? 'disabled' : '') + ' onclick="loadAllAddresses(' + (addressesListPage + 1) + ')">Next</button></div></div>';
html += '</tbody></table>' + pagination;
container.innerHTML = html;
} catch (error) {
container.innerHTML = '<div class="error">Failed to load addresses: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showAddresses()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
}
}
window._loadAllAddresses = loadAllAddresses;
window.loadAllAddresses = loadAllAddresses;
async function loadTokensList() {
var container = document.getElementById('tokensListContent');
if (!container) return;
try {
container.innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading tokens...</div>';
if (CHAIN_ID === 138) {
try {
var resp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/tokens?page=1&page_size=100').catch(function() { return null; });
var items = (resp && (resp.items || resp.data)) || (Array.isArray(resp) ? resp : null);
if (items && items.length > 0) {
var knownTokens = {
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': { name: 'Wrapped Ether', symbol: 'WETH' },
'0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': { name: 'Wrapped Ether v10', symbol: 'WETH' }
};
var filter = getExplorerPageFilter('tokensList');
var filteredItems = filter ? items.filter(function(t) {
var addr = (t.address && (t.address.hash || t.address)) || t.address_hash || t.token_address || t.contract_address_hash || '';
var known = addr ? knownTokens[addr.toLowerCase()] : null;
var name = (known && known.name) || t.name || t.symbol || (known && known.symbol) || '-';
var symbolDisplay = (known && known.symbol) || t.symbol || '';
var type = t.type || 'ERC-20';
return matchesExplorerFilter([addr, name, symbolDisplay, type].join(' '), filter);
}) : items;
var filterBar = renderPageFilterBar('tokensList', 'Filter by token name, symbol, contract, or type...', 'Filters the indexed token list below.', 'loadTokensList()');
var html = filterBar + '<table class="table"><thead><tr><th>Token</th><th>Contract</th><th>Type</th><th aria-label="Add to wallet"></th></tr></thead><tbody>';
filteredItems.forEach(function(t) {
var addr = (t.address && (t.address.hash || t.address)) || t.address_hash || t.token_address || t.contract_address_hash || '';
var known = addr ? knownTokens[addr.toLowerCase()] : null;
var name = (known && known.name) || t.name || t.symbol || (known && known.symbol) || '-';
var symbolDisplay = (known && known.symbol) || t.symbol;
var symbol = (symbolDisplay || 'TOKEN').replace(/'/g, "\\'");
var decimals = t.decimals != null ? Number(t.decimals) : 18;
var type = t.type || 'ERC-20';
if (!addr) return;
var addrEsc = escapeHtml(addr).replace(/'/g, "\\'");
html += '<tr style="cursor: pointer;" onclick="showTokenDetail(\'' + escapeHtml(addr) + '\')"><td>' + escapeHtml(name) + (symbolDisplay ? ' (' + escapeHtml(symbolDisplay) + ')' : '') + '</td><td class="hash">' + escapeHtml(shortenHash(addr)) + '</td><td>' + escapeHtml(type) + '</td><td><button type="button" class="btn-add-token-wallet" onclick="event.stopPropagation(); window.addTokenToWallet && window.addTokenToWallet(\'' + addrEsc + '\', \'' + symbol + '\', ' + decimals + ');" aria-label="Add to wallet" title="Add to wallet"><i class="fas fa-wallet" aria-hidden="true"></i></button></td></tr>';
});
if (filteredItems.length === 0) {
html += '<tr><td colspan="4" style="text-align: center; padding: 1.5rem;">No tokens match the current filter.</td></tr>';
}
html += '</tbody></table>';
container.innerHTML = html;
return;
}
} catch (e) {}
}
container.innerHTML = renderPageFilterBar('tokensList', 'Filter by token name, symbol, contract, or type...', 'Filters the indexed token list below.', 'loadTokensList()') + '<p style="color: var(--text-light);">No token index available. Use the search bar to find tokens by name, symbol, or contract address (0x...).</p>';
} catch (err) {
container.innerHTML = renderPageFilterBar('tokensList', 'Filter by token name, symbol, contract, or type...', 'Filters the indexed token list below.', 'loadTokensList()') + '<div class="error">Failed to load tokens. Use the search bar to find a token by address or name.</div>';
}
}
window._loadTokensList = loadTokensList;
function normalizeRouteStatus(status) {
return status || 'unavailable';
}
function renderRouteMetric(label, value) {
return '<div style="display:flex; justify-content:space-between; gap:0.75rem; padding:0.35rem 0; border-bottom:1px solid var(--border);"><span style="color:var(--text-light);">' + escapeHtml(label) + '</span><strong>' + escapeHtml(value) + '</strong></div>';
}
function renderRouteNode(node, depthLevel) {
var indent = Math.max(0, depthLevel || 0) * 1.05;
var status = normalizeRouteStatus(node.status);
var statusColor = status === 'live' ? '#16a34a' : status === 'partial' ? '#f59e0b' : status === 'stale' ? '#d97706' : '#dc2626';
var html = '<div style="margin-left:' + indent.toFixed(2) + 'rem; padding:1rem; border:1px solid var(--border); border-left:4px solid ' + statusColor + '; border-radius:12px; background:var(--light);">';
html += '<div style="display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:0.75rem; margin-bottom:0.75rem;">';
html += '<div>';
html += '<div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-light);">' + escapeHtml(node.kind || 'route') + '</div>';
html += '<div style="font-size:1rem; font-weight:700; margin-top:0.1rem;">' + escapeHtml(node.label || 'Untitled route') + '</div>';
html += '</div>';
html += '<span class="badge" style="background:' + statusColor + '; color:#fff; text-transform:uppercase;">' + escapeHtml(status) + '</span>';
html += '</div>';
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:0.5rem; margin-bottom:0.75rem;">';
html += renderRouteMetric('Chain', (node.chainName || 'Chain') + ' (' + (node.chainId || '-') + ')');
if (node.dexType) html += renderRouteMetric('DEX', node.dexType);
if (node.poolAddress) html += renderRouteMetric('Pool', shortenHash(node.poolAddress));
if (node.depth) {
html += renderRouteMetric('TVL', '$' + Number(node.depth.tvlUsd || 0).toFixed(2));
html += renderRouteMetric('Capacity', '$' + Number(node.depth.estimatedTradeCapacityUsd || 0).toFixed(2));
html += renderRouteMetric('Freshness', node.depth.freshnessSeconds == null ? 'n/a' : node.depth.freshnessSeconds + 's');
}
html += '</div>';
if (node.notes && node.notes.length) {
html += '<ul style="margin:0 0 0.75rem 1.1rem; color:var(--text-light);">';
node.notes.forEach(function(note) {
html += '<li>' + escapeHtml(note) + '</li>';
});
html += '</ul>';
}
if (node.children && node.children.length) {
html += '<div style="display:grid; gap:0.75rem; margin-top:0.75rem; padding-left:0.35rem; border-left:1px dashed var(--border);">';
node.children.forEach(function(child) {
html += renderRouteNode(child, (depthLevel || 0) + 1);
});
html += '</div>';
}
html += '</div>';
return html;
}
function renderMissingQuotePools(missingPools) {
if (!missingPools || !missingPools.length) {
return '<div style="color:var(--text-light);">No quote-token metadata gaps detected in the current indexed pool set.</div>';
}
var html = '<div style="overflow-x:auto;"><table class="table"><thead><tr><th>Pool</th><th>Chain</th><th>Token 0</th><th>Token 1</th><th>Reason</th></tr></thead><tbody>';
missingPools.forEach(function(pool) {
html += '<tr>';
html += '<td class="hash">' + escapeHtml(shortenHash(pool.poolAddress)) + '</td>';
html += '<td>' + escapeHtml(String(pool.chainId)) + '</td>';
html += '<td>' + escapeHtml((pool.token0Symbol || '') + ' ' + shortenHash(pool.token0Address)) + '</td>';
html += '<td>' + escapeHtml((pool.token1Symbol || '') + ' ' + shortenHash(pool.token1Address)) + '</td>';
html += '<td>' + escapeHtml(pool.reason || 'Missing quote token metadata') + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
return html;
}
function routeTreeQueryParams(query) {
var params = new URLSearchParams({
chainId: '138',
tokenIn: query.tokenIn,
amountIn: query.amountIn || '1000000'
});
if (query.tokenOut) params.set('tokenOut', query.tokenOut);
if (query.destinationChainId != null) params.set('destinationChainId', String(query.destinationChainId));
return params.toString();
}
async function fetchRouteTree(query) {
var response = await fetchAPIWithRetry(`${TOKEN_AGGREGATION_API_BASE}/v1/routes/tree?${routeTreeQueryParams(query)}`);
if ((!response || response.decision === 'unresolved' || !Array.isArray(response.tree) || response.tree.length === 0) &&
Number(query.destinationChainId || query.chainId || 138) === 138 &&
safeAddress(query.tokenOut)) {
try {
var ctx = await fetchCurrentPmmContext();
var liveFallback = await buildLiveDirectRouteFallback(query, ctx);
if (liveFallback) response = liveFallback;
} catch (e) {}
}
return { query: query, response: response };
}
function uniqueMissingQuotePools(results) {
var map = {};
results.forEach(function(entry) {
var pools = (entry.response && entry.response.missingQuoteTokenPools) || [];
pools.forEach(function(pool) {
var key = String(pool.chainId) + ':' + String(pool.poolAddress || '').toLowerCase();
if (!map[key]) map[key] = pool;
});
});
return Object.keys(map).sort().map(function(key) { return map[key]; });
}
function renderRouteSweepSummary(results) {
var html = '<div style="overflow-x:auto;"><table class="table"><thead><tr><th>Probe</th><th>Direct Pools</th><th>Missing Quote Pools</th><th>Decision</th><th>Freshest Status</th></tr></thead><tbody>';
results.forEach(function(entry) {
var response = entry.response || {};
var pools = Array.isArray(response.pools) ? response.pools : [];
var missing = Array.isArray(response.missingQuoteTokenPools) ? response.missingQuoteTokenPools : [];
var freshest = pools.length ? pools[0].depth && pools[0].depth.status ? pools[0].depth.status : 'unknown' : 'none';
html += '<tr>';
html += '<td>' + escapeHtml((entry.query.pairLabel || entry.query.title || entry.query.symbol || 'probe') + ' ' + shortenHash(entry.query.tokenIn)) + '</td>';
html += '<td>' + escapeHtml(String(pools.length)) + '</td>';
html += '<td>' + escapeHtml(String(missing.length)) + '</td>';
html += '<td>' + escapeHtml(response.decision || 'unresolved') + '</td>';
html += '<td>' + escapeHtml(freshest) + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
return html;
}
function renderPriorityRouteCard(entry) {
var response = entry.response || {};
var rootNodes = Array.isArray(response.tree) ? response.tree : [];
var decisionLabel = escapeHtml(response.decision || 'unresolved');
if (response.decision === 'bridge-only' && response.destination && Number(response.destination.chainId) === 1) {
decisionLabel = 'bridge-only (destination completion on Mainnet)';
}
var html = '<div class="card" style="margin:0; box-shadow:none;">';
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">';
html += '<div>';
html += '<h3 class="card-title" style="margin-bottom:0.3rem;"><i class="fas fa-diagram-project"></i> ' + escapeHtml(entry.query.title) + '</h3>';
html += '<div style="color:var(--text-light); font-size:0.9rem;">Decision: <strong>' + decisionLabel + '</strong> | Generated: ' + escapeHtml((response.generatedAt || '').replace('T', ' ').replace('Z', ' UTC')) + '</div>';
html += '</div>';
html += '<button type="button" class="btn btn-secondary" onclick="loadLiveRouteTrees()" aria-label="Refresh live route tree"><i class="fas fa-sync-alt"></i> Refresh</button>';
html += '</div>';
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem; margin-bottom:1rem;">';
if (response.source) {
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);">';
html += '<div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Source</div>';
html += '<div style="font-weight:700;">' + escapeHtml(response.source.chainName || 'Chain 138') + '</div>';
html += '<div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">' + escapeHtml(response.source.tokenIn ? (response.source.tokenIn.symbol + ' ' + shortenHash(response.source.tokenIn.address)) : entry.query.title) + '</div>';
html += '</div>';
}
if (response.destination) {
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);">';
html += '<div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Destination</div>';
html += '<div style="font-weight:700;">' + escapeHtml(response.destination.chainName || 'Destination') + '</div>';
html += '<div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Chain ' + escapeHtml(String(response.destination.chainId || '-')) + '</div>';
html += '</div>';
}
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);">';
html += '<div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Coverage</div>';
html += '<div style="font-weight:700;">' + escapeHtml(String((response.pools || []).length)) + ' direct pool(s)</div>';
html += '<div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">' + escapeHtml(String((response.missingQuoteTokenPools || []).length)) + ' missing quote-token pool(s)</div>';
html += '</div>';
html += '</div>';
if (rootNodes.length === 0) {
html += '<div style="color:var(--text-light);">No live route nodes available for this query yet.</div>';
} else {
rootNodes.slice(0, 4).forEach(function(node) {
html += renderRouteNode(node, 0);
});
}
html += '</div>';
return html;
}
function updatePoolsMissingQuoteBadge(count) {
var badge = document.getElementById('poolsMissingQuoteBadge');
if (!badge) return;
var n = Math.max(0, Number(count || 0));
badge.textContent = 'Missing quote routes ' + String(n);
badge.style.display = n > 0 ? 'inline-block' : 'none';
}
async function loadLiveRouteTrees(targetId) {
var containerId = targetId || (currentView === 'routes' ? 'routesRouteTreeContent' : 'poolRouteTreeContent');
var container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading live route tree...</div>';
try {
var ctx = await fetchCurrentPmmContext();
var priorityQueries = buildRoutePriorityQueries(ctx);
var sweepQueries = buildRouteSweepQueries(ctx);
var priorityResults = await Promise.allSettled(priorityQueries.map(fetchRouteTree));
var sweepResults = await Promise.allSettled(sweepQueries.map(fetchRouteTree));
var priorityOkResults = priorityResults.filter(function(result) { return result.status === 'fulfilled'; }).map(function(result) { return result.value; });
var priorityErrors = priorityResults.filter(function(result) { return result.status === 'rejected'; });
var sweepOkResults = sweepResults.filter(function(result) { return result.status === 'fulfilled'; }).map(function(result) { return result.value; });
var allSweepMissing = uniqueMissingQuotePools(sweepOkResults);
var html = '<div style="display:grid; gap:1rem;">';
html += '<div class="card" style="margin:0; box-shadow:none;">';
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">';
html += '<h3 class="card-title" style="margin-bottom:0;"><i class="fas fa-signal"></i> Route Coverage Sweep</h3>';
html += '<button type="button" class="btn btn-secondary" onclick="loadLiveRouteTrees()" aria-label="Refresh live route tree"><i class="fas fa-sync-alt"></i> Refresh all routes</button>';
html += '</div>';
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:0.75rem; margin-bottom:1rem;">';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Priority routes</div><div style="font-weight:700;">' + escapeHtml(String(priorityOkResults.length)) + ' ok' + (priorityErrors.length ? ' / ' + String(priorityErrors.length) + ' failed' : '') + '</div></div>';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Sweep probes</div><div style="font-weight:700;">' + escapeHtml(String(sweepOkResults.length)) + ' ok</div></div>';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Missing quote routes</div><div style="font-weight:700;">' + escapeHtml(String(allSweepMissing.length)) + '</div></div>';
html += '</div>';
html += '<div style="color:var(--text-light); margin-bottom:0.85rem; line-height:1.5;">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.</div>';
html += renderRouteSweepSummary(sweepOkResults);
if (priorityErrors.length) {
html += '<div style="margin-top:0.75rem; color:var(--text-light); font-size:0.9rem;">Some priority route requests failed, but the pools table is still available.</div>';
}
html += '</div>';
html += '<div style="display:grid; gap:1rem;">';
priorityOkResults.forEach(function(entry) {
html += renderPriorityRouteCard(entry);
});
html += '</div>';
html += '<div class="card" style="margin:0; box-shadow:none;">';
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">';
html += '<h3 class="card-title" style="margin-bottom:0;"><i class="fas fa-bug"></i> Missing Quote-Token Pools</h3>';
html += '</div>';
html += renderMissingQuotePools(allSweepMissing);
html += '</div>';
container.innerHTML = html;
updatePoolsMissingQuoteBadge(allSweepMissing.length);
} catch (err) {
container.innerHTML = '<div class="error">Failed to load live route tree: ' + escapeHtml(err.message || 'Unknown error') + '</div>';
updatePoolsMissingQuoteBadge(0);
}
}
function buildRoutesLandingHtml() {
var html = '';
html += '<div class="card" style="margin:0 0 1rem 0; box-shadow:none;">';
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">';
html += '<div>';
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-diagram-project"></i> Live Route Decision Tree</h3>';
html += '<div style="color:var(--text-light); line-height:1.5;">This dedicated view follows the Chain 138 routing graph end-to-end. It keeps the live coverage sweep, direct-pair diagnostics, and bridge-path branches together in one place so route investigations do not get buried inside the pools inventory.</div>';
html += '</div>';
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">';
html += '<button type="button" class="btn btn-primary" onclick="loadLiveRouteTrees(\'routesRouteTreeContent\')" aria-label="Refresh live routes"><i class="fas fa-sync-alt"></i> Refresh</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showPools(); updatePath(\'/pools\')" aria-label="Open pools inventory"><i class="fas fa-water"></i> Pools inventory</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\')" aria-label="Open liquidity access"><i class="fas fa-wave-square"></i> Liquidity access</button>';
html += '</div>';
html += '</div>';
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem;">';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Best for</div><div style="font-weight:700;">Route debugging and operator review</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Use this page when a user route, destination branch, or quote-token path looks wrong.</div></div>';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Includes</div><div style="font-weight:700;">Coverage sweep + priority route cards</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">The pools page now links here instead of embedding the full route tree inline.</div></div>';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Data source</div><div style="font-weight:700;">Live token-aggregation route tree API</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Every refresh re-reads current Chain 138 PMM and bridge state.</div></div>';
html += '</div>';
html += '</div>';
html += '<div id="routesRouteTreeContent"><div class="loading"><i class="fas fa-spinner"></i> Loading live route tree...</div></div>';
return html;
}
function renderRoutesView() {
showView('routes');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'routes') updatePath('/routes');
updateBreadcrumb('routes');
var container = document.getElementById('routesContent');
if (!container) return;
container.innerHTML = buildRoutesLandingHtml();
setTimeout(function() {
loadLiveRouteTrees('routesRouteTreeContent');
}, 0);
_poolsRouteTreeRefreshTimer = setInterval(function() {
if (currentView === 'routes') {
loadLiveRouteTrees('routesRouteTreeContent');
}
}, ROUTE_TREE_REFRESH_MS);
}
window.renderRoutesView = renderRoutesView;
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 = '<div class="loading"><i class="fas fa-spinner"></i> Loading pools...</div>';
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 + '<div style="margin-bottom: 0.15rem; color: var(--text-light); font-size: 0.92rem; line-height: 1.4;">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.</div>';
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:0.75rem; margin:0.9rem 0 1rem;">';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Live local pools</div><div style="font-weight:700;">' + escapeHtml(String(summary.liveLocal)) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Funded or deployed directly on Chain 138</div></div>';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">External Mainnet-side</div><div style="font-weight:700;">' + escapeHtml(String(summary.externalMainnet)) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Expected to live off Chain 138 by design</div></div>';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Not yet created</div><div style="font-weight:700;">' + escapeHtml(String(summary.notYetCreated)) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">No live pool mapping currently registered</div></div>';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Needs attention</div><div style="font-weight:700;">' + escapeHtml(String(summary.missingCode + summary.partial)) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Missing code, partial funding, or unfunded rows</div></div>';
html += '</div>';
html += '<div style="margin-top: 0.02rem;"><table class="table" style="margin-top: 0;"><thead><tr><th>Category</th><th>Pool Pair</th><th>System</th><th>Address</th><th>Status</th><th>Notes</th></tr></thead><tbody>';
if (rows.length === 0) {
html += '<tr><td colspan="6" style="text-align:center; padding: 1.5rem;">No pool data available yet.</td></tr>';
} else if (filtered.length === 0) {
html += '<tr><td colspan="6" style="text-align:center; padding: 1.5rem;">No pools match the current filter.</td></tr>';
} else {
filtered.forEach(function(entry) {
var row = entry.row;
var addr = row.address || '';
html += '<tr>';
html += '<td>' + escapeHtml(row.category) + '</td>';
html += '<td>' + escapeHtml(row.poolPair) + '</td>';
html += '<td>' + escapeHtml(row.poolType) + '</td>';
html += '<td>' + (safeAddress(addr) ? explorerAddressLink(addr, escapeHtml(shortenHash(addr)), 'color: inherit; text-decoration: none;') : '<span style="color: var(--text-light);">—</span>') + '</td>';
html += '<td>' + escapeHtml(row.status) + '</td>';
html += '<td>' + escapeHtml(row.notes) + '</td>';
html += '</tr>';
});
}
html += '</tbody></table></div>';
html += '<div class="card" style="margin-top:1.2rem; box-shadow:none;">';
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">';
html += '<div>';
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-diagram-project"></i> Live Route Decision Tree</h3>';
html += '<div style="color:var(--text-light); line-height:1.5;">The full route sweep and priority route cards now live on their own dedicated page so investigations can open directly into the routing graph.</div>';
html += '</div>';
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">';
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\')" aria-label="Open dedicated routes page"><i class="fas fa-arrow-right"></i> Open routes page</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\')" aria-label="Open liquidity access page"><i class="fas fa-wave-square"></i> Liquidity</button>';
html += '</div>';
html += '</div>';
html += '<div style="display:grid; gap:0.75rem;">';
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light); color:var(--text-light); line-height:1.5;">Use <strong>Routes</strong> for the live route coverage sweep, bridge-path diagnostics, and missing quote-token review. The pools table above stays focused on pool inventory and funding state.</div>';
html += '<div style="display:flex; flex-wrap:wrap; gap:0.6rem;">';
html += '<a class="btn btn-secondary" href="/routes" onclick="event.preventDefault(); showRoutes(); updatePath(\'/routes\');" style="text-decoration:none;"><i class="fas fa-diagram-project"></i> Routes</a>';
html += '<a class="btn btn-secondary" href="/liquidity" onclick="event.preventDefault(); showLiquidityAccess(); updatePath(\'/liquidity\');" style="text-decoration:none;"><i class="fas fa-plug"></i> Public access points</a>';
html += '</div>';
html += '</div>';
html += '</div>';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '<div class="error">Failed to load pools: ' + escapeHtml(err.message || 'Unknown error') + '</div>';
}
}
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 += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:0.9rem; margin-bottom:1rem;">';
html += '<div class="stat-card"><div class="stat-label">Live public pools</div><div class="stat-value">6</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.4rem;">Verified DODO PMM pools on Chain 138.</div></div>';
html += '<div class="stat-card"><div class="stat-label">Public access path</div><div class="stat-value" style="font-size:1.2rem;">/token-aggregation/api/v1</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.4rem;">Explorer-hosted proxy for route and execution APIs.</div></div>';
html += '<div class="stat-card"><div class="stat-label">Partner status</div><div class="stat-value">Fallback Ready</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.4rem;">Templates exist for 1inch, 0x, and LiFi, but Chain 138 execution still falls back internally.</div></div>';
html += '</div>';
html += '<div class="card" style="margin-bottom:1rem; box-shadow:none;">';
html += '<div class="card-header"><h3 class="card-title"><i class="fas fa-water"></i> Live Pool Snapshot</h3><button type="button" class="btn btn-secondary" onclick="openPoolsView()" aria-label="Open pool operations view"><i class="fas fa-diagram-project"></i> Open pools view</button></div>';
html += '<div style="display:grid; gap:0.75rem;">';
livePools.forEach(function(pool) {
html += '<div style="padding:0.95rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);">';
html += '<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:0.75rem; flex-wrap:wrap;">';
html += '<div><div style="font-size:1rem; font-weight:700;">' + escapeHtml(pool.pair) + '</div><div style="margin-top:0.35rem; color:var(--text-light); font-size:0.85rem;">Pool: ' + escapeHtml(pool.poolAddress) + '</div></div>';
html += '<div style="font-size:0.92rem; font-weight:600;">Reserves: ' + escapeHtml(pool.reserves) + '</div>';
html += '</div></div>';
});
html += '</div></div>';
html += '<div class="card" style="margin-bottom:1rem; box-shadow:none;">';
html += '<div class="card-header"><h3 class="card-title"><i class="fas fa-network-wired"></i> Route and Execution Notes</h3></div>';
html += '<div style="display:grid; gap:0.75rem; color:var(--text-light); line-height:1.6;">';
html += '<div>Direct live routes today: cUSDT ↔ cUSDC, cUSDT ↔ USDT, cUSDC ↔ USDC, cUSDT ↔ cXAUC, cUSDC ↔ cXAUC, and cEURT ↔ cXAUC.</div>';
html += '<div>Multi-hop public paths exist through cXAUC for cEURT ↔ cUSDT, cEURT ↔ cUSDC, and an alternate cUSDT ↔ cUSDC path.</div>';
html += '<div>Mainnet bridge discovery is live for cUSDT → USDT and cUSDC → USDC through the configured UniversalCCIPBridge lane.</div>';
html += '<div>1inch, 0x, and LiFi request templates are available through the explorer API, but those partners do not publicly support Chain 138 execution today.</div>';
html += '<div>When public partner execution is unavailable, the internal DODO PMM execution plan endpoint returns the Chain 138 fallback route instead of a dead end.</div>';
html += '</div></div>';
html += '<div class="card" style="margin-bottom:1rem; box-shadow:none;">';
html += '<div class="card-header"><h3 class="card-title"><i class="fas fa-plug"></i> Public Explorer Access Points</h3></div>';
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); gap:0.85rem;">';
endpointCards.forEach(function(card) {
html += '<a href="' + escapeAttr(card.href) + '" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:16px; padding:1rem; background:var(--muted-surface);">';
html += '<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:0.75rem; margin-bottom:0.5rem;">';
html += '<div style="font-weight:700; line-height:1.35;">' + escapeHtml(card.title) + '</div>';
html += '<span class="badge badge-info" style="white-space:nowrap;">' + escapeHtml(card.method) + '</span>';
html += '</div>';
html += '<div style="font-size:0.82rem; color:var(--text-light); word-break:break-all; margin-bottom:0.5rem;">' + escapeHtml(card.href) + '</div>';
html += '<div style="font-size:0.9rem; color:var(--text-light); line-height:1.5;">' + escapeHtml(card.notes) + '</div>';
html += '</a>';
});
html += '</div></div>';
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); gap:1rem;">';
html += '<div class="card" style="margin:0; box-shadow:none;"><div class="card-header"><h3 class="card-title"><i class="fas fa-terminal"></i> Quick Request Examples</h3></div><div style="display:grid; gap:0.75rem;">';
requestExamples.forEach(function(example) {
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><code style="display:block; font-size:0.82rem; line-height:1.6; word-break:break-all;">' + escapeHtml(example) + '</code></div>';
});
html += '</div></div>';
html += '<div class="card" style="margin:0; box-shadow:none;"><div class="card-header"><h3 class="card-title"><i class="fas fa-compass"></i> Related Explorer Tools</h3></div><div style="display:grid; gap:0.75rem; color:var(--text-light); line-height:1.6;">';
html += '<div>Use Wallet for network onboarding and the explorer token list URL, then open Routes for live route-tree diagnostics and Pools for contract-state inventory checks.</div>';
html += '<div style="display:flex; flex-wrap:wrap; gap:0.6rem; margin-top:0.2rem;">';
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\')" aria-label="Open routes view"><i class="fas fa-diagram-project"></i> Routes view</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showPools(); updatePath(\'/pools\')" aria-label="Open pools view"><i class="fas fa-water"></i> Pools view</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showWETHUtilities(); updatePath(\'/weth\')" aria-label="Open WETH tools"><i class="fas fa-coins"></i> WETH tools</button>';
html += '<a class="btn btn-secondary" href="/docs.html" style="text-decoration:none;"><i class="fas fa-book"></i> Explorer docs</a>';
html += '</div></div></div>';
html += '</div>';
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 home network dashboard.', action: 'showHome();', href: '/' },
{ 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: 'showRoutes();', href: '/routes' },
{ title: 'Node Tracker', icon: 'fa-server', status: 'Live', badgeClass: 'badge-success', desc: 'Inspect bridge balances, destination configuration, and operator-facing chain references from the live bridge monitoring panel.', action: 'showBridgeMonitoring();', href: '/bridge' },
{ 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: 'MetaMask Snap', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Open the Chain 138 MetaMask Snap companion for network setup, token list access, and wallet integration guidance.', action: 'window.location.href=\'/snap/\';', href: '/snap/' }
]
}
];
var html = '<div style="display:grid; grid-template-columns:minmax(240px, 0.9fr) repeat(3, minmax(220px, 1fr)); gap:1rem; align-items:start;">';
html += '<div style="border:1px solid var(--border); border-radius:18px; padding:1.25rem; background:linear-gradient(180deg, rgba(59,130,246,0.08), rgba(15,23,42,0.02)); min-height:100%;">';
html += '<div style="font-size:1.25rem; font-weight:800; margin-bottom:0.75rem;">Tools &amp; Services</div>';
html += '<div style="color:var(--text-light); line-height:1.7; margin-bottom:1rem;">Discover more of SolaceScanScout&apos;s explorer tools in one place, grouped the way users expect from Etherscan-style explorers.</div>';
html += '<div style="display:grid; gap:0.75rem;">';
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Now live</div><div style="font-weight:700;">Route matrix, ingestion APIs, smart search, pool exports, and live Mainnet stable bridge discovery.</div></div>';
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Good entry points</div><div style="display:flex; flex-wrap:wrap; gap:0.5rem;">';
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\');"><i class="fas fa-diagram-project"></i> Routes</button>';
html += '<button type="button" class="btn btn-primary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\');"><i class="fas fa-wave-square"></i> Liquidity</button>';
html += '<button type="button" class="btn btn-secondary" onclick="openSmartSearchModal(\'\');"><i class="fas fa-magnifying-glass"></i> Search</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showAddresses(); updatePath(\'/addresses\');"><i class="fas fa-address-book"></i> Addresses</button>';
html += '</div></div>';
html += '</div></div>';
groups.forEach(function(group) {
html += '<div style="border:1px solid var(--border); border-radius:18px; padding:1.1rem; background:var(--light); min-height:100%;">';
html += '<div style="font-size:1.1rem; font-weight:800; margin-bottom:0.85rem;">' + escapeHtml(group.title) + '</div>';
html += '<div style="display:grid; gap:0.7rem;">';
group.items.forEach(function(item) {
var disabled = !!item.disabled;
var disabledTitle = String(item.title) + ' is not exposed in the explorer yet.';
var onclick = disabled
? ('event.preventDefault(); showToast(' + JSON.stringify(disabledTitle) + ', "info");')
: (item.href === '#'
? ('event.preventDefault(); ' + item.action + ' closeNavMenu();')
: ('event.preventDefault(); ' + item.action + ' updatePath(' + JSON.stringify(item.href) + '); closeNavMenu();'));
var href = disabled ? '/more' : item.href;
html += '<a href="' + escapeAttr(href) + '" onclick="' + onclick + '" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:0.9rem; background:' + (disabled ? 'rgba(148,163,184,0.08)' : 'var(--muted-surface)') + '; opacity:' + (disabled ? '0.78' : '1') + ';">';
html += '<div style="display:flex; justify-content:space-between; gap:0.75rem; align-items:flex-start; margin-bottom:0.45rem;">';
html += '<div style="display:flex; align-items:center; gap:0.65rem; min-width:0;">';
html += '<span style="width:2rem; height:2rem; border-radius:999px; display:inline-flex; align-items:center; justify-content:center; background:rgba(59,130,246,0.12); color:var(--primary); flex:0 0 auto;"><i class="fas ' + escapeHtml(item.icon) + '"></i></span>';
html += '<strong style="line-height:1.35;">' + escapeHtml(item.title) + '</strong>';
html += '</div>';
html += '<span class="badge ' + escapeHtml(item.badgeClass || 'badge-info') + '" style="white-space:nowrap;">' + escapeHtml(item.status) + '</span>';
html += '</div>';
html += '<div style="color:var(--text-light); font-size:0.9rem; line-height:1.55;">' + escapeHtml(item.desc) + '</div>';
html += '</a>';
});
html += '</div></div>';
});
html += '</div>';
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 = '' +
'<div style="width:min(560px, 100%); border-radius:18px; border:1px solid var(--border); background:var(--background); box-shadow:0 24px 90px rgba(0,0,0,0.35); overflow:hidden;">' +
'<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.1rem; border-bottom:1px solid var(--border);">' +
'<div><div style="font-size:1.1rem; font-weight:800;">Unit Converter</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.2rem;">Wei, gwei, ether, and 6-decimal stablecoin units for Chain 138.</div></div>' +
'<button type="button" class="btn btn-secondary" id="unitConverterCloseBtn"><i class="fas fa-times"></i></button>' +
'</div>' +
'<div style="padding:1rem 1.1rem; display:grid; gap:0.9rem;">' +
'<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Amount</span><input id="unitConverterAmount" type="number" min="0" step="any" placeholder="1.0" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"></label>' +
'<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Unit</span><select id="unitConverterUnit" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"><option value="ether">Ether / WETH</option><option value="gwei">Gwei</option><option value="wei">Wei</option><option value="stable">Stablecoin (6 decimals)</option></select></label>' +
'<div id="unitConverterResults" style="display:grid; gap:0.55rem;"></div>' +
'</div>' +
'</div>';
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 = '<div style="color:var(--text-light);">Enter a non-negative amount to convert.</div>';
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 =
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>Wei:</strong> ' + escapeHtml(Math.round(wei).toString()) + '</div>' +
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>Gwei:</strong> ' + escapeHtml(gweiValue) + '</div>' +
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>Ether / WETH:</strong> ' + escapeHtml(etherValue) + '</div>' +
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>6-decimal stable amount:</strong> ' + escapeHtml(stableValue) + '</div>';
}
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 = '<div class="loading"><i class="fas fa-spinner"></i> Loading bridge data...</div>';
// 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 '<span style="color: var(--text-light);">No explorer</span>';
const url = explorer.baseUrl + address;
return '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" style="color: var(--primary); white-space: nowrap;">View on ' + escapeHtml(explorer.label) + '</a>';
}
// 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 + `
<div class="bridge-chain-card">
<div class="chain-name"><i class="fas fa-network-wired"></i> CCIP Bridge Ecosystem</div>
<div class="badge badge-info" style="display:inline-flex; margin-top:0.65rem; white-space:nowrap;">${sourceBridgeCount} source contracts / ${mainnetBridgeCount} mainnet contracts / ${routeBridgeCount} unique route contracts</div>
<div style="color: var(--text-light); margin-bottom: 1rem;">
Cross-chain interoperability powered by Chainlink CCIP
</div>
</div>
<!-- Chain 138 Bridges -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-home"></i> Chain 138 (Source Chain)</h3>
<span class="badge badge-info" style="white-space: nowrap;">${sourceBridgeCount} source contracts / ${mainnetBridgeCount} mainnet contracts / ${routeBridgeCount} unique route contracts</span>
</div>
<div class="chain-info" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH9Bridge</div>
<div class="chain-stat-value">
${explorerAddressLink(WETH9_BRIDGE_138, escapeHtml(WETH9_BRIDGE_138), 'color: inherit; text-decoration: none; font-size: 0.9rem;')}
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
Token: ${explorerAddressLink('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 'WETH9', 'color: inherit; text-decoration: none;')}
</div>
</div>
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH10Bridge</div>
<div class="chain-stat-value">
${explorerAddressLink(WETH10_BRIDGE_138, escapeHtml(WETH10_BRIDGE_138), 'color: inherit; text-decoration: none; font-size: 0.9rem;')}
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
Token: ${explorerAddressLink('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f', 'WETH10', 'color: inherit; text-decoration: none;')}
</div>
</div>
</div>
</div>
<!-- WETH9 Bridge Routes -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-route"></i> CCIPWETH9Bridge Routes</h3>
<span class="badge badge-success">${Object.keys(routes.weth9).length} Destinations</span>
</div>
<div style="overflow-x: auto;">
<table class="table">
<thead>
<tr>
<th>Destination Chain</th>
<th>Chain ID</th>
<th>Bridge Address</th>
<th>Explorer</th>
</tr>
</thead>
<tbody>
`;
// 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 += '<tr><td colspan="4" style="text-align:center; padding: 1rem; color: var(--text-light);">No WETH9 routes match the current filter.</td></tr>';
}
for (const [chain, address] of weth9Routes) {
const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
html += `
<tr>
<td><strong>${chain.replace(/\s*\\(\\d+\\)/, '')}</strong></td>
<td>${chainId}</td>
<td><span class="hash" onclick="showAddressDetail('${escapeHtml(address)}')" style="cursor: pointer;">${escapeHtml(shortenHash(address))}</span></td>
<td>${renderExplorerLink(chain, address)}</td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
</div>
<!-- WETH10 Bridge Routes -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-route"></i> CCIPWETH10Bridge Routes</h3>
<span class="badge badge-success">${Object.keys(routes.weth10).length} Destinations</span>
</div>
<div style="overflow-x: auto;">
<table class="table">
<thead>
<tr>
<th>Destination Chain</th>
<th>Chain ID</th>
<th>Bridge Address</th>
<th>Explorer</th>
</tr>
</thead>
<tbody>
`;
// Add WETH10 routes
if (weth10Routes.length === 0) {
html += '<tr><td colspan="4" style="text-align:center; padding: 1rem; color: var(--text-light);">No WETH10 routes match the current filter.</td></tr>';
}
for (const [chain, address] of weth10Routes) {
const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
html += `
<tr>
<td><strong>${chain.replace(/\s*\\(\\d+\\)/, '')}</strong></td>
<td>${chainId}</td>
<td><span class="hash" onclick="showAddressDetail('${escapeHtml(address)}')" style="cursor: pointer;">${escapeHtml(shortenHash(address))}</span></td>
<td>${renderExplorerLink(chain, address)}</td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
</div>
<!-- Ethereum Mainnet Bridges -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-ethereum"></i> Ethereum Mainnet Bridges</h3>
<span class="badge badge-info" style="white-space: nowrap;">${sourceBridgeCount} source contracts / ${mainnetBridgeCount} mainnet contracts / ${routeBridgeCount} unique route contracts</span>
</div>
<div class="chain-info" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH9Bridge</div>
<div class="chain-stat-value">
<span class="hash" onclick="window.open('https://etherscan.io/address/${WETH9_BRIDGE_MAINNET}', '_blank', 'noopener,noreferrer')" style="cursor: pointer; font-size: 0.9rem;">${WETH9_BRIDGE_MAINNET}</span>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
<a href="https://etherscan.io/address/${WETH9_BRIDGE_MAINNET}" target="_blank" rel="noopener noreferrer" style="color: var(--primary);">View on Etherscan</a>
</div>
</div>
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH10Bridge</div>
<div class="chain-stat-value">
<span class="hash" onclick="window.open('https://etherscan.io/address/${WETH10_BRIDGE_MAINNET}', '_blank', 'noopener,noreferrer')" style="cursor: pointer; font-size: 0.9rem;">${WETH10_BRIDGE_MAINNET}</span>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
<a href="https://etherscan.io/address/${WETH10_BRIDGE_MAINNET}" target="_blank" rel="noopener noreferrer" style="color: var(--primary);">View on Etherscan</a>
</div>
</div>
</div>
</div>
<!-- Bridge Information -->
<div class="card">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-info-circle"></i> Bridge Information</h3>
<span class="badge badge-info" style="white-space: nowrap;">${totalBridgeCount} Active Bridge Contracts</span>
</div>
<div style="line-height: 1.8;">
<p><strong>CCIP Bridge Ecosystem</strong> enables cross-chain transfers of WETH9 and WETH10 tokens using Chainlink CCIP (Cross-Chain Interoperability Protocol).</p>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">Supported Networks:</h4>
<ul style="margin-left: 2rem; margin-top: 0.5rem;">
<li><strong>Chain 138</strong> - Source chain with both bridge contracts</li>
<li><strong>Ethereum Mainnet</strong> - Destination with dedicated bridge contracts</li>
<li><strong>BSC</strong> - Binance Smart Chain</li>
<li><strong>Polygon</strong> - Polygon PoS</li>
<li><strong>Avalanche</strong> - Avalanche C-Chain</li>
<li><strong>Base</strong> - Base L2</li>
<li><strong>Arbitrum</strong> - Arbitrum One</li>
<li><strong>Optimism</strong> - Optimism Mainnet</li>
<li><strong>Cronos</strong> - Cronos (25); see routing table for full list and config-ready chains (Gnosis, Celo, Wemix)</li>
</ul>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">How to Use:</h4>
<ol style="margin-left: 2rem; margin-top: 0.5rem;">
<li>Click on any bridge address to view detailed information and transaction history</li>
<li>Use the bridge contracts to transfer WETH9 or WETH10 tokens between supported chains</li>
<li>All transfers are secured by Chainlink CCIP infrastructure</li>
</ol>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">CCIP Infrastructure:</h4>
<ul style="margin-left: 2rem; margin-top: 0.5rem;">
<li><strong>CCIP Router (Chain 138)</strong>: ${explorerAddressLink('0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e', '0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e', 'color: inherit; text-decoration: none;')}</li>
<li><strong>CCIP Sender (Chain 138)</strong>: ${explorerAddressLink('0x105F8A15b819948a89153505762444Ee9f324684', '0x105F8A15b819948a89153505762444Ee9f324684', 'color: inherit; text-decoration: none;')}</li>
</ul>
</div>
</div>
`;
container.innerHTML = html;
} catch (error) {
container.innerHTML = '<div class="error">Failed to load bridge data: ' + escapeHtml(error.message) + '</div>';
}
}
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;
}
function escapeJsSingleQuoted(value) {
return String(value == null ? '' : value).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
}
function explorerAddressLink(address, content, style) {
var safe = safeAddress(address);
if (!safe) return content || 'N/A';
return '<a class="hash" href="/address/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(shortenHash(safe))) + '</a>';
}
function explorerTransactionLink(txHash, content, style) {
var safe = safeTxHash(txHash);
if (!safe) return content || 'N/A';
return '<a class="hash" href="/tx/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(shortenHash(safe))) + '</a>';
}
function explorerBlockLink(blockNumber, content, style) {
var safe = safeBlockNumber(blockNumber);
if (!safe) return content || 'N/A';
return '<a href="/block/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(String(safe))) + '</a>';
}
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 = '<div class="error">Failed to load block: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showBlockDetail(\'' + escapeHtml(String(blockNumber)) + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
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 = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Block #${b.number}</h2>
<button onclick="exportBlockData(${b.number})" class="btn btn-primary" style="padding: 0.5rem 1rem;">
<i class="fas fa-download"></i> Export
</button>
</div>
<div class="info-row">
<div class="info-label">Block Number</div>
<div class="info-value">${b.number}</div>
</div>
<div class="info-row">
<div class="info-label">Hash</div>
<div class="info-value hash">${escapeHtml(b.hash || '')} <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard(\'' + String(b.hash || '').replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\', \'Copied\');" aria-label="Copy hash"><i class="fas fa-copy"></i></button></div>
</div>
<div class="info-row">
<div class="info-label">Parent Hash</div>
<div class="info-value">${explorerBlockLink(String(parseInt(b.number) - 1), escapeHtml(b.parent_hash || ''), 'color: inherit; text-decoration: none;')}</div>
</div>
<div class="info-row">
<div class="info-label">Timestamp</div>
<div class="info-value">${escapeHtml(timestamp)}</div>
</div>
<div class="info-row">
<div class="info-label">Miner</div>
<div class="info-value">${explorerAddressLink(b.miner || '', formatAddressWithLabel(b.miner || '') || 'N/A', 'color: inherit; text-decoration: none;')}</div>
</div>
<div class="info-row">
<div class="info-label">Transaction Count</div>
<div class="info-value">${b.transaction_count || 0}</div>
</div>
<div class="info-row">
<div class="info-label">Gas Used</div>
<div class="info-value">${formatNumber(b.gas_used || 0)} / ${formatNumber(b.gas_limit || 0)} (${gasUsedPercent}%)</div>
</div>
<div class="info-row">
<div class="info-label">Gas Limit</div>
<div class="info-value">${formatNumber(b.gas_limit || 0)}</div>
</div>
${b.base_fee_per_gas ? `
<div class="info-row">
<div class="info-label">Base Fee</div>
<div class="info-value">${baseFeeGwei} Gwei</div>
</div>
` : ''}
${b.burnt_fees && parseInt(b.burnt_fees) > 0 ? `
<div class="info-row">
<div class="info-label">Burnt Fees</div>
<div class="info-value">${burntFeesEth} ETH</div>
</div>
` : ''}
<div class="info-row">
<div class="info-label">Size</div>
<div class="info-value">${formatNumber(b.size || 0)} bytes</div>
</div>
<div class="info-row">
<div class="info-label">Difficulty</div>
<div class="info-value">${formatNumber(b.difficulty || 0)}</div>
</div>
<div class="info-row">
<div class="info-label">Nonce</div>
<div class="info-value">${escapeHtml(String(b.nonce || '0x0'))}</div>
</div>
${(rawBlockResponse && (rawBlockResponse.consensus !== undefined || rawBlockResponse.finality !== undefined || rawBlockResponse.validated !== undefined)) ? '<div class="info-row"><div class="info-label">Finality / Consensus</div><div class="info-value">' + escapeHtml(String(rawBlockResponse.consensus != null ? rawBlockResponse.consensus : (rawBlockResponse.finality != null ? rawBlockResponse.finality : rawBlockResponse.validated))) + '</div></div>' : ''}
`;
} else {
container.innerHTML = '<div class="error">Block not found</div>';
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load block: ' + escapeHtml(error.message) + '</div>';
}
}
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 = '<div class="error">Failed to load transaction: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showTransactionDetail(\'' + escapeHtml(String(txHash)) + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
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 = '<div class="error">Transaction not found</div>';
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 ? explorerAddressLink(t.to, formatAddressWithLabel(t.to), 'color: inherit; text-decoration: none;') + ' <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard(\'' + String(t.to).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\', \'Copied\');" aria-label="Copy address"><i class="fas fa-copy"></i></button>' : 'N/A';
let mainHtml = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Transaction</h2>
<div style="display: flex; gap: 0.5rem;">
<button onclick="exportTransactionData('${t.hash}')" class="btn btn-primary" style="padding: 0.5rem 1rem;">
<i class="fas fa-download"></i> JSON
</button>
<button onclick="exportTransactionCSV('${t.hash}')" class="btn btn-primary" style="padding: 0.5rem 1rem;">
<i class="fas fa-file-csv"></i> CSV
</button>
</div>
</div>
<div class="info-row">
<div class="info-label">Transaction Hash</div>
<div class="info-value hash">${escapeHtml(t.hash)} <button type="button" class="btn-copy" onclick="copyToClipboard('${escapeHtml(t.hash)}', 'Copied');" aria-label="Copy hash"><i class="fas fa-copy"></i></button></div>
</div>
<div class="info-row">
<div class="info-label">Type</div>
<div class="info-value"><span class="badge badge-primary">${txType}</span></div>
</div>
<div class="info-row">
<div class="info-label">Status</div>
<div class="info-value">
<span class="badge ${t.status === 1 ? 'badge-success' : 'badge-danger'}">
${t.status === 1 ? 'Success' : t.status === 0 ? 'Failed' : 'Pending'}
</span>
</div>
</div>
<div class="info-row">
<div class="info-label">Block Number</div>
<div class="info-value">${explorerBlockLink(String(t.block_number || ''), escapeHtml(String(t.block_number || 'N/A')), 'color: inherit; text-decoration: none;')}</div>
</div>
<div class="info-row">
<div class="info-label">Block Hash</div>
<div class="info-value hash">${escapeHtml(t.block_hash || 'N/A')}</div>
</div>
<div class="info-row">
<div class="info-label">From</div>
<div class="info-value">${explorerAddressLink(t.from || '', formatAddressWithLabel(t.from || ''), 'color: inherit; text-decoration: none;')} <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard('${escapeHtml(t.from || '')}', 'Copied');" aria-label="Copy address"><i class="fas fa-copy"></i></button></div>
</div>
<div class="info-row">
<div class="info-label">To</div>
<div class="info-value">${toCellContent}</div>
</div>
<div class="info-row">
<div class="info-label">Value</div>
<div class="info-value">${valueEth} ETH</div>
</div>
<div class="info-row">
<div class="info-label">Gas Used</div>
<div class="info-value">${t.gas_used ? formatNumber(t.gas_used) : 'N/A'}</div>
</div>
<div class="info-row">
<div class="info-label">Gas Limit</div>
<div class="info-value">${t.gas_limit ? formatNumber(t.gas_limit) : 'N/A'}</div>
</div>
${t.max_fee_per_gas ? `<div class="info-row"><div class="info-label">Max Fee Per Gas</div><div class="info-value">${maxFeeGwei} Gwei</div></div>` : ''}
${t.max_priority_fee_per_gas ? `<div class="info-row"><div class="info-label">Max Priority Fee</div><div class="info-value">${priorityFeeGwei} Gwei</div></div>` : ''}
${!t.max_fee_per_gas && t.gas_price ? `<div class="info-row"><div class="info-label">Gas Price</div><div class="info-value">${gasPriceGwei} Gwei</div></div>` : ''}
<div class="info-row">
<div class="info-label">Total Fee</div>
<div class="info-value">${totalFee} ETH</div>
</div>
${t.tx_burnt_fee && parseInt(t.tx_burnt_fee) > 0 ? `<div class="info-row"><div class="info-label">Burnt Fee</div><div class="info-value">${burntFeeEth} ETH</div></div>` : ''}
<div class="info-row"><div class="info-label">Nonce</div><div class="info-value">${t.nonce || 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Timestamp</div><div class="info-value">${timestamp}</div></div>
${t.contract_address ? `<div class="info-row"><div class="info-label">Contract Address</div><div class="info-value">${explorerAddressLink(t.contract_address, escapeHtml(t.contract_address), 'color: inherit; text-decoration: none;')}</div></div>` : ''}
`;
if (revertReason && t.status !== 1) {
const reasonStr = typeof revertReason === 'string' ? revertReason : (revertReason.message || JSON.stringify(revertReason));
mainHtml += `
<div class="card" style="margin-top: 1rem; border-left: 4px solid var(--danger);">
<h3 style="color: var(--danger); margin-bottom: 0.5rem;"><i class="fas fa-exclamation-triangle"></i> Revert Reason</h3>
<pre style="background: var(--light); padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.875rem;">${escapeHtml(reasonStr)}</pre>
</div>
`;
}
if (inputHex || decodedInput) {
mainHtml += `<div class="card" style="margin-top: 1rem;"><h3>Input Data</h3>`;
if (decodedInput && (decodedInput.method || decodedInput.params)) {
const method = decodedInput.method || decodedInput.name || 'Unknown';
mainHtml += `<p style="margin-bottom: 0.5rem;"><strong>Method:</strong> ${escapeHtml(method)}</p>`;
if (decodedInput.params && Array.isArray(decodedInput.params)) {
mainHtml += '<table class="table"><thead><tr><th>Param</th><th>Value</th></tr></thead><tbody>';
decodedInput.params.forEach(function(p) {
const name = (p.name || p.type || '');
const val = typeof p.value !== 'undefined' ? String(p.value) : (p.type || '');
mainHtml += '<tr><td>' + escapeHtml(name) + '</td><td class="hash" style="word-break: break-all;">' + escapeHtml(val) + '</td></tr>';
});
mainHtml += '</tbody></table>';
}
}
if (inputHex) {
mainHtml += `<p style="margin-top: 0.5rem;"><strong>Hex:</strong> <code id="txInputHex" style="word-break: break-all; font-size: 0.8rem;">${escapeHtml(inputHex)}</code> <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'txInputHex\').textContent); showToast(\'Copied\', \'success\');">Copy</button></p>`;
}
mainHtml += '</div>';
}
container.innerHTML = mainHtml;
if (CHAIN_ID === 138) {
const internalCard = document.createElement('div');
internalCard.className = 'card';
internalCard.style.marginTop = '1rem';
internalCard.innerHTML = '<h3><i class="fas fa-sitemap"></i> Internal Transactions</h3><div id="txInternalTxs" class="loading">Loading...</div>';
container.appendChild(internalCard);
const logsCard = document.createElement('div');
logsCard.className = 'card';
logsCard.style.marginTop = '1rem';
logsCard.innerHTML = '<h3><i class="fas fa-list"></i> Event Logs</h3><div id="txLogs" class="loading">Loading...</div>';
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 + '<p style="color: var(--text-light);">No internal transactions</p>';
} 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 + '<table class="table"><thead><tr><th>Type</th><th>From</th><th>To</th><th>Value</th></tr></thead><tbody>';
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 += '<tr><td>' + escapeHtml(type) + '</td><td>' + explorerAddressLink(from, escapeHtml(shortenHash(from)), 'color: inherit; text-decoration: none;') + '</td><td>' + explorerAddressLink(to, escapeHtml(shortenHash(to)), 'color: inherit; text-decoration: none;') + '</td><td>' + escapeHtml(val) + ' ETH</td></tr>';
});
if (filteredInternals.length === 0) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No internal transactions match the current filter.</td></tr>';
}
tbl += '</tbody></table>';
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 + '<p style="color: var(--text-light);">No event logs</p>';
} 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 + '<table class="table"><thead><tr><th>Address</th><th>Topics</th><th>Data</th><th>Decoded</th></tr></thead><tbody>';
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 += '<tr id="txLogRow' + idx + '"><td>' + explorerAddressLink(addr, escapeHtml(shortenHash(addr)), 'color: inherit; text-decoration: none;') + '</td><td style="word-break: break-all; font-size: 0.75rem;">' + escapeHtml(String(topicsStr).substring(0, 80)) + (String(topicsStr).length > 80 ? '...' : '') + '</td><td style="word-break: break-all; font-size: 0.75rem;">' + escapeHtml(String(data).substring(0, 66)) + (String(data).length > 66 ? '...' : '') + '</td><td id="txLogDecoded' + idx + '" style="font-size: 0.8rem;">—</td></tr>';
});
if (filteredLogs.length === 0) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No event logs match the current filter.</td></tr>';
}
tbl += '</tbody></table>';
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 = '<div class="error">Failed to load transaction: ' + escapeHtml(error.message) + '</div>';
}
}
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 = '<div class="error">Invalid address format</div>';
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 = '<div class="error">Failed to load address: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showAddressDetail(\'' + retryAddress + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
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 ? '<span class="badge badge-success" style="margin-left: 0.5rem;">Verified</span>' : '';
const encodedAddress = encodeURIComponent(address);
const escapedAddress = escapeHtml(address);
const addressForJs = address.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const contractLink = isContract ? `<a href="${EXPLORER_ORIGIN}/address/${encodedAddress}/contract" target="_blank" rel="noopener noreferrer" style="color: var(--primary); font-size: 0.875rem;">View contract on Blockscout</a>` : '';
const savedLabel = getAddressLabel(address);
const inWatchlist = isInWatchlist(address);
container.innerHTML = `
<div class="info-row">
<div class="info-label">Address</div>
<div class="info-value hash">${escapedAddress} <button type="button" class="btn-copy" onclick="copyToClipboard('${addressForJs}', 'Copied');" aria-label="Copy address"><i class="fas fa-copy"></i></button></div>
</div>
<div class="info-row">
<div class="info-label">Label</div>
<div class="info-value"><input type="text" id="addressLabelInput" value="${escapeHtml(savedLabel)}" placeholder="Optional label" style="padding: 0.35rem; border-radius: 6px; width: 200px; max-width: 100%;"> <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="var v=document.getElementById(\'addressLabelInput\').value; setAddressLabel(\'${addressForJs}\', v); showToast(\'Label saved\', \'success\');">Save</button></div>
</div>
<div class="info-row">
<div class="info-label">Watchlist</div>
<div class="info-value"><button type="button" id="addressWatchlistBtn" class="btn btn-primary" style="padding: 0.35rem 0.75rem;" onclick="(function(addr){ if(isInWatchlist(addr)){ removeFromWatchlist(addr); showToast(\'Removed from watchlist\', \'success\'); } else { addToWatchlist(addr); showToast(\'Added to watchlist\', \'success\'); } var b=document.getElementById(\'addressWatchlistBtn\'); if(b) b.innerHTML=isInWatchlist(addr)?\'<i class=\'fas fa-star\'></i> Remove from watchlist\':\'<i class=\'fas fa-star-o\'></i> Add to watchlist\'; })(\'${addressForJs}\')"><i class="fas fa-star${inWatchlist ? '' : '-o'}"></i> ${inWatchlist ? 'Remove from watchlist' : 'Add to watchlist'}</button></div>
</div>
<div class="info-row">
<div class="info-label">Token approvals</div>
<div class="info-value"><a href="https://revoke.cash/address/${encodedAddress}${CHAIN_ID === 138 ? '?chainId=138' : ''}" target="_blank" rel="noopener noreferrer" style="color: var(--primary);">Check token approvals</a></div>
</div>
<div class="info-row">
<div class="info-label">Balance</div>
<div class="info-value">${balanceEth} ETH</div>
</div>
<div class="info-row">
<div class="info-label">Transaction Count</div>
<div class="info-value">${a.transaction_count || 0}</div>
</div>
<div class="info-row">
<div class="info-label">Token Count</div>
<div class="info-value">${a.token_count || 0}</div>
</div>
<div class="info-row">
<div class="info-label">Type</div>
<div class="info-value">${a.is_contract ? '<span class="badge badge-success">Contract</span>' + verifiedBadge + (contractLink ? '<br/>' + contractLink : '') : '<span class="badge badge-primary">EOA</span>'}</div>
</div>
${a.creation_tx_hash ? `<div class="info-row"><div class="info-label">Contract created in</div><div class="info-value">${explorerTransactionLink(a.creation_tx_hash, escapeHtml(shortenHash(a.creation_tx_hash)), 'color: inherit; text-decoration: none;')} <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard('${escapeHtml(a.creation_tx_hash).replace(/'/g, "\\'")}', 'Copied');" aria-label="Copy"><i class="fas fa-copy"></i></button></div></div>` : ''}
${a.first_seen_at ? `<div class="info-row"><div class="info-label">First seen</div><div class="info-value">${escapeHtml(typeof a.first_seen_at === 'string' ? a.first_seen_at : new Date(a.first_seen_at).toISOString())}</div></div>` : ''}
${a.last_seen_at ? `<div class="info-row"><div class="info-label">Last seen</div><div class="info-value">${escapeHtml(typeof a.last_seen_at === 'string' ? a.last_seen_at : new Date(a.last_seen_at).toISOString())}</div></div>` : ''}
<div class="tabs" style="margin-top: 1.5rem;">
<button class="tab active" onclick="switchAddressTab('transactions', '${address}')" id="addrTabTxs" aria-selected="true">Transactions</button>
<button class="tab" onclick="switchAddressTab('tokens', '${address}')" id="addrTabTokens">Token Balances</button>
<button class="tab" onclick="switchAddressTab('internal', '${address}')" id="addrTabInternal">Internal Txns</button>
<button class="tab" onclick="switchAddressTab('nfts', '${address}')" id="addrTabNfts">NFTs</button>
${isContract ? '<button class="tab" onclick="switchAddressTab(\'contract\', \'' + address + '\')" id="addrTabContract">Contract (ABI / Bytecode)</button>' : ''}
</div>
<div id="addressTabTransactions" class="address-tab-content card" style="margin-top: 1rem;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<h3 style="margin: 0;">Recent Transactions</h3>
<button type="button" class="btn btn-primary" onclick="exportAddressTransactionsCSV('${address}')" style="padding: 0.5rem 1rem;"><i class="fas fa-file-csv"></i> Export CSV</button>
</div>
<div id="addressTransactions" class="loading">Loading transactions...</div>
</div>
<div id="addressTabTokens" class="address-tab-content card" style="margin-top: 1rem; display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;">
<h3 style="margin: 0;">Token Balances</h3>
<button type="button" class="btn btn-primary" onclick="exportAddressTokenBalancesCSV('${address}')" style="padding: 0.5rem 1rem;"><i class="fas fa-file-csv"></i> Export CSV</button>
</div>
<div id="addressTokenBalances" class="loading">Loading...</div>
</div>
<div id="addressTabInternal" class="address-tab-content card" style="margin-top: 1rem; display: none;">
<h3>Internal Transactions</h3>
<div id="addressInternalTxns" class="loading">Loading...</div>
</div>
<div id="addressTabNfts" class="address-tab-content card" style="margin-top: 1rem; display: none;">
<h3>NFT Inventory</h3>
<div id="addressNftInventory" class="loading">Loading...</div>
</div>
${isContract ? '<div id="addressTabContract" class="address-tab-content card" style="margin-top: 1rem; display: none;"><h3>Contract ABI & Bytecode</h3><div id="addressContractInfo" class="loading">Loading...</div></div>' : ''}
`;
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 + '<p style="color: var(--text-light);">No token balances</p>';
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 + '<table class="table"><thead><tr><th>Token</th><th>Contract</th><th>Balance</th><th>Type</th></tr></thead><tbody>';
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 += '<tr><td><a href="/token/' + encodeURIComponent(contract) + '">' + escapeHtml(symbol) + '</a></td><td>' + explorerAddressLink(contract, escapeHtml(shortenHash(contract)), 'color: inherit; text-decoration: none;') + '</td><td>' + escapeHtml(displayBalance) + '</td><td>' + escapeHtml(type) + '</td></tr>';
});
if (filteredItems.length === 0) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No token balances match the current filter.</td></tr>';
}
tbl += '</tbody></table>';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '<p class="error">Failed to load token balances</p>';
}
}
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 + '<p style="color: var(--text-light);">No NFT tokens</p>';
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 + '<table class="table"><thead><tr><th>Contract</th><th>Token ID</th><th>Name / Symbol</th><th>Balance</th></tr></thead><tbody>';
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 += '<tr><td>' + explorerAddressLink(contract, escapeHtml(shortenHash(contract)), 'color: inherit; text-decoration: none;') + '</td>';
tbl += '<td>' + (tokenId !== '-' ? '<a href="/nft/' + encodeURIComponent(contract) + '/' + encodeURIComponent(String(tokenId)) + '" onclick="event.preventDefault(); showNftDetail(\'' + escapeHtml(contract) + '\', \'' + escapeHtml(String(tokenId)) + '\'); updatePath(\'/nft/' + encodeURIComponent(contract) + '/' + encodeURIComponent(String(tokenId)) + '\');">' + escapeHtml(String(tokenId)) + '</a>' : '-') + '</td>';
tbl += '<td>' + escapeHtml(name) + '</td><td>' + escapeHtml(String(balance)) + '</td></tr>';
});
if (filteredItems.length === 0) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No NFT inventory matches the current filter.</td></tr>';
}
tbl += '</tbody></table>';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '<p class="error">Failed to load NFT inventory</p>';
}
}
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 + '<p style="color: var(--text-light);">No internal transactions</p>';
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 + '<table class="table"><thead><tr><th>Block</th><th>From</th><th>To</th><th>Value</th><th>Tx Hash</th></tr></thead><tbody>';
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 += '<tr><td>' + explorerBlockLink(block, escapeHtml(block), 'color: inherit; text-decoration: none;') + '</td><td>' + explorerAddressLink(from, escapeHtml(shortenHash(from)), 'color: inherit; text-decoration: none;') + '</td><td>' + explorerAddressLink(to, escapeHtml(shortenHash(to)), 'color: inherit; text-decoration: none;') + '</td><td>' + escapeHtml(val) + ' ETH</td><td>' + (txHash !== '-' ? explorerTransactionLink(txHash, escapeHtml(shortenHash(txHash)), 'color: inherit; text-decoration: none;') : '-') + '</td></tr>';
});
if (filteredItems.length === 0) {
tbl += '<tr><td colspan="5" style="text-align:center; padding: 1rem;">No internal transactions match the current filter.</td></tr>';
}
tbl += '</tbody></table>';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '<p class="error">Failed to load internal transactions</p>';
}
}
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 = '<p style="color: var(--text-light);">Contract source not indexed. <a href="' + EXPLORER_ORIGIN + '/address/' + encodeURIComponent(addr) + '/contract" target="_blank" rel="noopener noreferrer">Verify on Blockscout</a></p>';
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 = '<p><strong>Verification:</strong> ' + (data.is_verified ? '<span class="badge badge-success">Verified</span>' : '<span class="badge badge-warning">Unverified</span>') + '</p>';
html += '<div style="margin-top: 1rem;"><h4>ABI <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'contractAbiText\').textContent); showToast(\'Copied\', \'success\');">Copy</button> <a href="javascript:void(0)" onclick="var blob=new Blob([document.getElementById(\'contractAbiText\').textContent],{type:\'application/json\'}); var a=document.createElement(\'a\'); a.href=URL.createObjectURL(blob); a.download=\'abi-' + addr.substring(0,10) + '.json\'; a.click(); URL.revokeObjectURL(a.href); return false;">Download</a></h4>';
html += '<pre id="contractAbiText" style="background: var(--light); padding: 1rem; border-radius: 8px; overflow: auto; max-height: 300px; font-size: 0.75rem;">' + escapeHtml(abiStr) + '</pre></div>';
html += '<div style="margin-top: 1rem;"><h4>Bytecode <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'contractBytecodeText\').textContent); showToast(\'Copied\', \'success\');">Copy</button></h4>';
html += '<pre id="contractBytecodeText" style="background: var(--light); padding: 1rem; border-radius: 8px; overflow: auto; max-height: 150px; font-size: 0.7rem; word-break: break-all;">' + escapeHtml(String(bytecode).substring(0, 500)) + (String(bytecode).length > 500 ? '...' : '') + '</pre></div>';
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 += '<div style="margin-top: 1.5rem;"><h4><i class="fas fa-readme"></i> Read contract</h4><p style="color: var(--text-light); font-size: 0.875rem;">Call view/pure functions (requires ethers.js).</p>';
html += '<div style="margin-top: 0.5rem;"><label for="readContractSelect">Function:</label><select id="readContractSelect" style="margin-left: 0.5rem; padding: 0.35rem; border-radius: 6px; min-width: 200px;">';
viewFns.forEach(function(fn) { html += '<option value="' + escapeHtml(fn.name) + '">' + escapeHtml(fn.name) + '(' + (fn.inputs || []).map(function(i) { return i.type; }).join(',') + ')</option>'; });
html += '</select></div><div id="readContractInputs" style="margin-top: 0.5rem;"></div>';
html += '<button type="button" class="btn btn-primary" style="margin-top: 0.5rem;" id="readContractBtn">Query</button><pre id="readContractResult" style="background: var(--light); padding: 1rem; border-radius: 8px; margin-top: 0.5rem; min-height: 2rem; font-size: 0.8rem; display: none;"></pre></div>';
}
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 += '<div style="margin-top: 1.5rem;"><h4><i class="fas fa-pen"></i> Write contract</h4><p style="color: var(--text-light); font-size: 0.875rem;">Connect wallet to send transactions.</p>';
html += '<div style="margin-top: 0.5rem;"><label>Function:</label><select id="writeContractSelect" style="margin-left: 0.5rem; padding: 0.35rem; border-radius: 6px; min-width: 200px;">';
writeFns.forEach(function(fn) { var pay = (fn.stateMutability === 'payable'); html += '<option value="' + escapeHtml(fn.name) + '" data-payable="' + pay + '">' + escapeHtml(fn.name) + '(' + (fn.inputs || []).map(function(i) { return i.type; }).join(',') + ')' + (pay ? ' payable' : '') + '</option>'; });
html += '</select></div><div id="writeContractInputs" style="margin-top: 0.5rem;"></div>';
html += '<div id="writeContractValueRow" style="margin-top: 0.5rem; display: none;"><label>Value (ETH):</label><input type="text" id="writeContractValue" placeholder="0" style="margin-left: 0.5rem; padding: 0.35rem; border-radius: 6px; width: 120px;"></div>';
html += '<button type="button" class="btn btn-primary" style="margin-top: 0.5rem;" id="writeContractBtn">Write</button><pre id="writeContractResult" style="background: var(--light); padding: 1rem; border-radius: 8px; margin-top: 0.5rem; min-height: 2rem; font-size: 0.8rem; display: none;"></pre></div>';
}
html += '<p style="margin-top: 0.5rem;"><a href="' + EXPLORER_ORIGIN + '/address/' + encodeURIComponent(addr) + '/contract" target="_blank" rel="noopener noreferrer" style="color: var(--primary);">Read / Write contract on Blockscout</a></p>';
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 = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">';
inputs.forEach(function(inp, i) {
h += '<label>' + escapeHtml(inp.name || 'arg' + i) + ' (' + escapeHtml(inp.type) + '):</label><input type="text" id="readArg' + i + '" placeholder="' + escapeHtml(inp.type) + '" style="padding: 0.35rem; border-radius: 6px; min-width: 120px;">';
});
h += '</div>';
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 = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">';
inputs.forEach(function(inp, i) {
h += '<label>' + escapeHtml(inp.name || 'arg' + i) + ' (' + escapeHtml(inp.type) + '):</label><input type="text" id="writeArg' + i + '" placeholder="' + escapeHtml(inp.type) + '" style="padding: 0.35rem; border-radius: 6px; min-width: 120px;">';
});
h += '</div>';
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 = '<p class="error">Failed to load contract info</p>';
}
}
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 + '<table class="table"><thead><tr><th>Hash</th><th>Block</th><th>From</th><th>To</th><th>Value</th></tr></thead><tbody>';
filteredTxs.forEach(function(tx) {
txHtml += '<tr onclick="showTransactionDetail(\'' + escapeHtml(tx.hash) + '\')" style="cursor: pointer;"><td>' + explorerTransactionLink(tx.hash, escapeHtml(shortenHash(tx.hash)), 'color: inherit; text-decoration: none;') + '</td><td>' + explorerBlockLink(String(tx.block_number), escapeHtml(String(tx.block_number)), 'color: inherit; text-decoration: none;') + '</td><td>' + explorerAddressLink(tx.from, formatAddressWithLabel(tx.from), 'color: inherit; text-decoration: none;') + '</td><td>' + (tx.to ? explorerAddressLink(tx.to, formatAddressWithLabel(tx.to), 'color: inherit; text-decoration: none;') : 'N/A') + '</td><td>' + escapeHtml(formatEther(tx.value || '0')) + ' ETH</td></tr>';
});
if (filteredTxs.length === 0) {
txHtml += '<tr><td colspan="5" style="text-align:center; padding: 1rem;">No transactions match the current filter.</td></tr>';
}
txHtml += '</tbody></table>';
txContainer.innerHTML = txHtml;
} else {
txContainer.innerHTML = filterBar + '<p>No transactions found</p>';
}
}
} catch (e) {
const txContainer = document.getElementById('addressTransactions');
if (txContainer) txContainer.innerHTML = '<p>Failed to load transactions</p>';
}
} else {
container.innerHTML = '<div class="error">Address not found</div>';
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load address: ' + escapeHtml(error.message) + '</div>';
}
}
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 = '<p class="error">Token not found or not indexed.</p><p><a href="/address/' + encodeURIComponent(tokenAddress) + '">View as address</a></p>';
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 = '<div class="token-detail-actions" style="margin-bottom: 1.25rem;"><button type="button" class="btn btn-primary btn-add-token-wallet-prominent" onclick="window.addTokenToWallet && window.addTokenToWallet(\'' + addrEsc + '\', \'' + symbolEsc + '\', ' + decimals + ');" aria-label="Add token to wallet"><i class="fas fa-wallet" aria-hidden="true"></i> Add to wallet (MetaMask)</button></div>';
html += '<div class="info-row"><div class="info-label">Contract</div><div class="info-value">' + explorerAddressLink(tokenAddress, escapeHtml(tokenAddress), 'color: inherit; text-decoration: none;') + ' <button type="button" class="btn-add-token-wallet" onclick="window.addTokenToWallet && window.addTokenToWallet(\'' + addrEsc + '\', \'' + symbolEsc + '\', ' + decimals + ');" aria-label="Add to wallet" title="Add to wallet"><i class="fas fa-wallet" aria-hidden="true"></i></button></div></div>';
html += '<div class="info-row"><div class="info-label">Name</div><div class="info-value">' + escapeHtml(name) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Symbol</div><div class="info-value">' + escapeHtml(symbol) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Decimals</div><div class="info-value">' + decimals + '</div></div>';
html += '<div class="info-row"><div class="info-label">Total Supply</div><div class="info-value">' + supplyNum.toLocaleString(undefined, { maximumFractionDigits: 6 }) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Holders</div><div class="info-value">' + (holders !== '-' ? formatNumber(holders) : '-') + '</div></div>';
const transfersFilter = getExplorerPageFilter('tokenTransfers');
const transfersFilterBar = renderPageFilterBar('tokenTransfers', 'Filter by from, to, value, or tx hash...', 'Filters the recent transfers below.', 'showTokenDetail(\'' + addrEsc + '\')');
html += '<div class="card" style="margin-top: 1rem;"><h3>Recent Transfers</h3>';
if (transfers.length === 0) {
html += transfersFilterBar + '<p style="color: var(--text-light);">No transfers</p>';
} 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 + '<table class="table"><thead><tr><th>From</th><th>To</th><th>Value</th><th>Tx</th></tr></thead><tbody>';
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 += '<tr><td>' + explorerAddressLink(from, escapeHtml(shortenHash(from)), 'color: inherit; text-decoration: none;') + '</td><td>' + explorerAddressLink(to, escapeHtml(shortenHash(to)), 'color: inherit; text-decoration: none;') + '</td><td>' + escapeHtml(v.toLocaleString(undefined, { maximumFractionDigits: 6 })) + '</td><td>' + (txHash ? explorerTransactionLink(txHash, escapeHtml(shortenHash(txHash)), 'color: inherit; text-decoration: none;') : '-') + '</td></tr>';
});
if (filteredTransfers.length === 0) {
html += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No transfers match the current filter.</td></tr>';
}
html += '</tbody></table>';
}
html += '</div>';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '<div class="error">Failed to load token: ' + escapeHtml(err.message || 'Unknown') + '</div>';
}
}
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 = '<div class="info-row"><div class="info-label">Contract</div><div class="info-value">' + explorerAddressLink(contractAddress, escapeHtml(contractAddress), 'color: inherit; text-decoration: none;') + '</div></div>';
html += '<div class="info-row"><div class="info-label">Token ID</div><div class="info-value">' + escapeHtml(String(tokenId)) + '</div></div>';
if (data) {
if (data.metadata && data.metadata.image) {
html += '<div class="info-row"><div class="info-label">Image</div><div class="info-value"><img src="' + escapeHtml(data.metadata.image) + '" alt="NFT" style="max-width: 200px; border-radius: 8px;" onerror="this.style.display=\'none\'"></div></div>';
}
if (data.name) html += '<div class="info-row"><div class="info-label">Name</div><div class="info-value">' + escapeHtml(data.name) + '</div></div>';
if (data.description) html += '<div class="info-row"><div class="info-label">Description</div><div class="info-value">' + escapeHtml(data.description) + '</div></div>';
if (data.owner) { var ownerAddr = (data.owner.hash || data.owner); html += '<div class="info-row"><div class="info-label">Owner</div><div class="info-value">' + explorerAddressLink(ownerAddr, escapeHtml(ownerAddr), 'color: inherit; text-decoration: none;') + '</div></div>'; }
if (data.metadata && data.metadata.attributes && Array.isArray(data.metadata.attributes)) {
html += '<div class="info-row"><div class="info-label">Traits</div><div class="info-value"><div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">';
data.metadata.attributes.forEach(function(attr) {
var traitType = (attr.trait_type || attr.traitType || ''); var val = (attr.value != null ? attr.value : '');
if (traitType || val) html += '<span style="background: var(--light); padding: 0.25rem 0.5rem; border-radius: 6px; font-size: 0.85rem;">' + escapeHtml(traitType) + ': ' + escapeHtml(String(val)) + '</span>';
});
html += '</div></div></div>';
}
}
html += '<p style="margin-top: 1rem;"><a href="' + EXPLORER_ORIGIN + '/token/' + encodeURIComponent(contractAddress) + '/instance/' + encodeURIComponent(tokenId) + '" target="_blank" rel="noopener noreferrer" style="color: var(--primary);">View on Blockscout</a></p>';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '<div class="error">Failed to load NFT: ' + escapeHtml(err.message || 'Unknown') + '</div>';
}
}
window.showNftDetail = showNftDetail;
function showSearchResultsList(items, query) {
showView('searchResults');
var container = document.getElementById('searchResultsContent');
if (!container) return;
var html = '<p style="color: var(--text-light); margin-bottom: 1rem;">Found ' + items.length + ' result(s) for "' + escapeHtml(query) + '". Click a row to open.</p>';
html += '<table class="table"><thead><tr><th>Type</th><th>Value</th></tr></thead><tbody>';
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 += '<tr style="cursor: pointer;" onclick="showTokenDetail(\'' + escapeHtml(tokenAddr) + '\')"><td>Token</td><td class="hash">' + escapeHtml(shortenHash(tokenAddr)) + ' ' + (item.name || item.symbol ? ' (' + escapeHtml(item.name || item.symbol) + ')' : '') + '</td></tr>';
return;
}
}
if (item.address_hash || item.hash) {
addr = item.address_hash || item.hash;
if (/^0x[a-f0-9]{40}$/i.test(addr)) {
html += '<tr><td>Address</td><td>' + explorerAddressLink(addr, escapeHtml(shortenHash(addr)), 'color: inherit; text-decoration: none;') + '</td></tr>';
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 += '<tr><td>Transaction</td><td>' + explorerTransactionLink(txHash, escapeHtml(shortenHash(txHash)), 'color: inherit; text-decoration: none;') + '</td></tr>';
return;
}
}
if (item.block_number != null) {
blockNum = String(item.block_number);
html += '<tr><td>Block</td><td>' + explorerBlockLink(blockNum, '#' + escapeHtml(blockNum), 'color: inherit; text-decoration: none;') + '</td></tr>';
return;
}
html += '<tr><td>' + escapeHtml(type || 'Unknown') + '</td><td>' + escapeHtml(String(label).substring(0, 80)) + '</td></tr>';
});
html += '</tbody></table>';
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 '<div style="display:flex; justify-content:' + (isAssistant ? 'flex-start' : 'flex-end') + ';">' +
'<div style="max-width: 88%; padding: 0.85rem 0.95rem; border-radius: 16px; ' + bubbleStyle + '">' +
'<div style="font-size:0.72rem; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-light); margin-bottom:0.35rem;">' + (isAssistant ? 'Explorer AI' : 'You') + '</div>' +
'<div style="white-space:pre-wrap; line-height:1.55;">' + escapeHtml(message.content || '') + '</div>' +
'</div>' +
'</div>';
}).join('');
if (_explorerAIState.loading) {
list.innerHTML += '<div style="display:flex; justify-content:flex-start;"><div style="padding:0.8rem 0.95rem; border-radius:16px; background:rgba(37,99,235,0.08); border:1px solid rgba(37,99,235,0.16); color:var(--text-light);">Thinking through indexed data, live routes, and docs...</div></div>';
}
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 = '<i class="fas fa-robot" aria-hidden="true" style="margin-right:0.45rem;"></i>Explorer AI';
button.addEventListener('click', function() { toggleExplorerAIPanel(); });
var panel = document.createElement('section');
panel.id = 'explorerAIPanel';
panel.setAttribute('aria-label', 'Explorer AI');
panel.innerHTML = '' +
'<div style="padding:1rem 1rem 0.85rem; border-bottom:1px solid var(--border); background:linear-gradient(180deg, rgba(37,99,235,0.10), rgba(37,99,235,0));">' +
'<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:1rem;">' +
'<div>' +
'<div style="font-size:1rem; font-weight:800;">Explorer AI</div>' +
'<div id="explorerAIStatus" style="font-size:0.84rem; color:var(--text-light); margin-top:0.2rem;">Read-only assistant using indexed explorer data, route APIs, and curated docs.</div>' +
'</div>' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.6rem;" onclick="toggleExplorerAIPanel(false)">Close</button>' +
'</div>' +
'<div style="display:flex; flex-wrap:wrap; gap:0.45rem; margin-top:0.85rem;">' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.65rem;" onclick="submitExplorerAIMessage(\'Which Chain 138 routes are live right now?\')">Live routes</button>' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.65rem;" onclick="submitExplorerAIMessage(\'Why would a route show partial instead of live?\')">Route status</button>' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.65rem;" onclick="submitExplorerAIMessage(\'Summarize the current page context and what I can do next.\')">Current page</button>' +
'</div>' +
'</div>' +
'<div id="explorerAIMessageList" style="flex:1; overflow:auto; padding:1rem; display:grid; gap:0.7rem; background:linear-gradient(180deg, rgba(15,23,42,0.02), rgba(15,23,42,0));"></div>' +
'<div style="padding:1rem; border-top:1px solid var(--border); display:grid; gap:0.7rem;">' +
'<div style="font-size:0.78rem; color:var(--text-light);">Public explorer and route data only. No private key handling, no transaction execution.</div>' +
'<textarea id="explorerAIInput" placeholder="Ask about a tx hash, address, bridge path, liquidity pool, or route status..."></textarea>' +
'<div style="display:flex; justify-content:space-between; align-items:center; gap:0.75rem;">' +
'<div style="font-size:0.78rem; color:var(--text-light);">Shift+Enter for a new line. Enter to send.</div>' +
'<button type="button" class="btn btn-primary" id="explorerAISendBtn">Ask Explorer AI</button>' +
'</div>' +
'</div>';
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();
}
});