feat(frontend): explorer SPA and index updates; env verification report

- Expand explorer-spa.js and index.html for Chain 138 explorer UX
- Refresh ENV_VERIFICATION_REPORT.md

Made-with: Cursor
This commit is contained in:
defiQUG
2026-03-24 18:11:08 -07:00
parent 04bea35e89
commit ed86d01e1d
3 changed files with 374 additions and 59 deletions

View File

@@ -96,7 +96,7 @@
## ✅ Contract Address Verification
### Chain 138 Contracts (Verified Against Documentation)
Have we
| Contract | Address | Status | Source |
|----------|---------|--------|--------|
| **CCIP Router** | `0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e` | ✅ Verified | CCIP_CONFIGURATION_STATUS.md |

View File

@@ -64,16 +64,68 @@
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', bridge: 'Bridge', weth: 'WETH', tokens: 'Tokens', analytics: 'Analytics', operator: 'Operator', watchlist: 'Watchlist', searchPlaceholder: 'Address, tx hash, block number, or token/contract name...', connectWallet: 'Connect Wallet', darkMode: 'Dark mode', lightMode: 'Light mode', back: 'Back', exportCsv: 'Export CSV', tokenBalances: 'Token Balances', internalTxns: 'Internal Txns', readContract: 'Read contract', writeContract: 'Write contract', addToWatchlist: 'Add to watchlist', removeFromWatchlist: 'Remove from watchlist', checkApprovals: 'Check token approvals', copied: 'Copied' },
de: { home: 'Start', blocks: 'Blöcke', transactions: 'Transaktionen', bridge: 'Brücke', weth: 'WETH', tokens: 'Token', 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', bridge: 'Pont', weth: 'WETH', tokens: 'Jetons', 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é' }
en: { home: 'Home', blocks: 'Blocks', transactions: 'Transactions', 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', 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', 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('searchInput'); 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'); }
window._renderWatchlist = function() { var container = document.getElementById('watchlistContent'); if (!container) return; var list = getWatchlist(); if (list.length === 0) { container.innerHTML = '<p style="color: var(--text-light);">No addresses in watchlist. Open an address and click "Add to watchlist".</p>'; return; } var html = '<table class="table"><thead><tr><th>Address</th><th>Label</th><th></th></tr></thead><tbody>'; list.forEach(function(addr){ var label = getAddressLabel(addr) || ''; html += '<tr><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(addr) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(addr)) + '</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 _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;'); }
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 class="hash" onclick="showAddressDetail(\'' + escapeHtml(addr) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(addr)) + '</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' };
var LIQUIDITY_POOL_ROWS = [
{ category: 'Public Liquidity Pools', poolPair: 'cUSDT / cUSDC', poolType: 'DODO PMM', address: '0x9fcB06Aa1FD5215DC0E91Fd098aeff4B62fEa5C8', status: 'Created', notes: 'Pool created via CreateCUSDTCUSDCPool.s.sol' },
{ category: 'Public Liquidity Pools', poolPair: 'cUSDT / USDT (official)', poolType: 'DODO PMM', address: '0xa3Ee6091696B28e5497b6F491fA1e99047250c59', status: 'Created', notes: 'Pool created via CreateCUSDTUSDTPool.s.sol' },
{ category: 'Public Liquidity Pools', poolPair: 'cUSDC / USDC (official)', poolType: 'DODO PMM', address: '0x90bd9Bf18Daa26Af3e814ea224032d015db58Ea5', status: 'Created', notes: 'Pool created via CreateCUSDCUSDCPool.s.sol' },
{ category: 'Public Liquidity Pools', poolPair: 'cUSDT / XAU', poolType: 'DODO PMM', address: '', status: 'Not deployed', notes: 'Requires XAU token (not on chain)' },
{ category: 'Public Liquidity Pools', poolPair: 'cUSDC / XAU', poolType: 'DODO PMM', address: '', status: 'Not deployed', notes: 'Requires XAU token' },
{ category: 'Public Liquidity Pools', poolPair: 'cEURT / XAU', poolType: 'DODO PMM', address: '', status: 'Not deployed', notes: 'Requires XAU; cEURT is deployed' },
{ category: 'Private Stabilization Pools', poolPair: 'cUSDT ↔ XAU', poolType: 'PrivatePoolRegistry', address: '', status: 'Not deployed', notes: 'Stabilizer-only swap path' },
{ category: 'Private Stabilization Pools', poolPair: 'cUSDC ↔ XAU', poolType: 'PrivatePoolRegistry', address: '', status: 'Not deployed', notes: 'Stabilizer-only swap path' },
{ category: 'Private Stabilization Pools', poolPair: 'cEURT ↔ XAU', poolType: 'PrivatePoolRegistry', address: '', status: 'Not deployed', notes: 'Requires cEURT + XAU' },
{ category: 'Reserve Pools / Vault Backing', poolPair: 'ReserveSystem', poolType: 'Reserve', address: '0x607e97cD626f209facfE48c1464815DDE15B5093', status: 'Deployed', notes: 'Reserve core' },
{ category: 'Reserve Pools / Vault Backing', poolPair: 'ReserveTokenIntegration', poolType: 'Reserve', address: '0x34B73e6EDFd9f85a7c25EeD31dcB13aB6E969b96', status: 'Deployed', notes: 'Reserve token integration' },
{ category: 'Reserve Pools / Vault Backing', poolPair: 'StablecoinReserveVault', poolType: 'Reserve', address: '', status: 'Not on Chain 138', notes: 'Designed for Ethereum Mainnet' },
{ category: 'Reserve Pools / Vault Backing', poolPair: 'Bridge_Vault', poolType: 'Vault', address: '0x31884f84555210FFB36a19D2471b8eBc7372d0A8', status: 'Deployed', notes: 'Bridge vault' },
{ category: 'Bridge Liquidity Pool', poolPair: 'LiquidityPoolETH', poolType: 'Bridge LP', address: '', status: 'Placeholder', notes: 'ETH, WETH' }
];
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'); } }
@@ -100,7 +152,7 @@
_blocksScrollAnimationId = null;
}
currentView = viewName;
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens'];
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','pools','more'];
if (detailViews.indexOf(viewName) === -1) currentDetailKey = '';
var homeEl = document.getElementById('homeView');
if (homeEl) homeEl.style.display = viewName === 'home' ? 'block' : 'none';
@@ -116,6 +168,8 @@
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; } };
window.showPools = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('pools'); if (window._showPools) window._showPools(); } 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; } };
@@ -168,10 +222,8 @@
// Show/hide navigation items based on track
const analyticsNav = document.getElementById('analyticsNav');
const operatorNav = document.getElementById('operatorNav');
const moreWrap = document.getElementById('navDropdownMoreWrap');
if (analyticsNav) analyticsNav.style.display = hasAccess(3) ? 'block' : 'none';
if (operatorNav) operatorNav.style.display = hasAccess(4) ? 'block' : 'none';
if (moreWrap) moreWrap.style.display = (hasAccess(3) || hasAccess(4)) ? '' : 'none';
}
// Wallet authentication
@@ -1265,7 +1317,7 @@
function showView(viewName) {
currentView = viewName;
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens'];
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','pools','more'];
if (detailViews.indexOf(viewName) === -1) currentDetailKey = '';
document.querySelectorAll('.detail-view').forEach(v => v.classList.remove('active'));
const homeView = document.getElementById('homeView');
@@ -1326,6 +1378,8 @@
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') showPools(); 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; }
@@ -1425,6 +1479,16 @@
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 '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>';
@@ -1799,22 +1863,30 @@
}
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 = '<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>';
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 = '<div class="blocks-scroll-container" id="blocksScrollContainer">';
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)
limitedBlocks.forEach(function(block, index) {
filteredBlocks.forEach(function(block, index) {
var animationClass = index < 3 ? 'new-block' : '';
html += createBlockCardHtml(block, { animationClass: animationClass });
});
// Duplicate blocks for seamless infinite loop
limitedBlocks.forEach(function(block) {
filteredBlocks.forEach(function(block) {
html += createBlockCardHtml(block, {});
});
@@ -1826,7 +1898,7 @@
const scrollContent = scrollContainer?.querySelector('.blocks-scroll-content');
if (scrollContainer && scrollContent) {
const cardWidth = 200 + 16; // card width (200px) + gap (16px = 1rem)
const singleSetWidth = limitedBlocks.length * cardWidth;
const singleSetWidth = filteredBlocks.length * cardWidth;
// Use CSS transform for smooth animation
let scrollPosition = 0;
@@ -1920,10 +1992,20 @@
// 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(limitedTransactions.map(tx => String(tx.hash || '')));
const newTransactions = limitedTransactions.filter(tx => !previousTransactionHashes.has(String(tx.hash || '')));
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;
@@ -1933,12 +2015,14 @@
container.innerHTML = createSkeletonLoader('table');
}
let html = '<table class="table"><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Value</th><th>Block</th></tr></thead><tbody>';
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 {
limitedTransactions.forEach((tx, index) => {
filteredTransactions.forEach((tx, index) => {
// Transaction is already normalized by adapter
const hash = String(tx.hash || 'N/A');
const from = String(tx.from || 'N/A');
@@ -2045,12 +2129,20 @@
}
}
let html = '<table class="table"><thead><tr><th>Block</th><th>Hash</th><th>Transactions</th><th>Timestamp</th></tr></thead><tbody>';
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 {
blocks.forEach(function(block) {
filteredBlocks.forEach(function(block) {
var d = normalizeBlockDisplay(block);
html += '<tr onclick="showBlockDetail(\'' + escapeHtml(String(d.blockNum)) + '\')" style="cursor: pointer;"><td>' + escapeHtml(String(d.blockNum)) + '</td><td class="hash">' + escapeHtml(shortenHash(d.hash)) + '</td><td>' + escapeHtml(String(d.txCount)) + '</td><td>' + escapeHtml(d.timestampFormatted) + '</td></tr>';
});
@@ -2124,12 +2216,24 @@
}
}
let html = '<table class="table"><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Value</th><th>Block</th></tr></thead><tbody>';
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 {
transactions.forEach(tx => {
filteredTransactions.forEach(tx => {
const hash = String(tx.hash || 'N/A');
const from = String(tx.from || 'N/A');
const to = String(tx.to || 'N/A');
@@ -2166,8 +2270,18 @@
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': { name: 'Wrapped Ether', symbol: 'WETH' },
'0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': { name: 'Wrapped Ether v10', symbol: 'WETH' }
};
var html = '<table class="table"><thead><tr><th>Token</th><th>Contract</th><th>Type</th><th aria-label="Add to wallet"></th></tr></thead><tbody>';
items.forEach(function(t) {
var 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) || '-';
@@ -2179,19 +2293,88 @@
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 = '<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>';
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 = '<div class="error">Failed to load tokens. Use the search bar to find a token by address or name.</div>';
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 showPools() {
showView('pools');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'pools') updatePath('/pools');
var container = document.getElementById('poolsContent');
if (!container) return;
try {
var filter = getExplorerPageFilter('poolsList');
var filterBar = renderPageFilterBar('poolsList', 'Filter by category, pair, type, status, address, or notes...', 'Tracks the pools and reserve-related entries we know about.', 'showPools()');
var rows = LIQUIDITY_POOL_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 page lists known liquidity, reserve, and bridge pool references. Pool entries are grouped by role, and placeholder rows mark planned or external assets.</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) ? '<span class="hash" onclick="showAddressDetail(\'' + escapeHtml(addr) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(addr)) + '</span>' : '<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>';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '<div class="error">Failed to load pools: ' + escapeHtml(err.message || 'Unknown error') + '</div>';
}
}
window._showPools = showPools;
function showMore() {
showView('more');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'more') updatePath('/more');
var container = document.getElementById('moreContent');
if (!container) return;
var cards = [
{ href: '/bridge', icon: 'fa-bridge', title: 'Bridge', desc: 'Inspect CCIP routes, bridge endpoints, and cross-chain references.', action: 'showBridgeMonitoring();' },
{ href: '/weth', icon: 'fa-coins', title: 'WETH', desc: 'Wrap and unwrap WETH9 / WETH10 and review utility contract details.', action: 'showWETHUtilities();' },
{ href: '/tokens', icon: 'fa-tag', title: 'Tokens', desc: 'Browse the indexed token list and jump into token detail pages.', action: 'showTokensList();' },
{ href: '/watchlist', icon: 'fa-star', title: 'Watchlist', desc: 'Track saved addresses and revisit them quickly.', action: 'showWatchlist();' },
{ href: '/pools', icon: 'fa-water', title: 'Pools', desc: 'Review the liquidity snapshot and config caps for public pools and reserve links.', action: 'showPools();' },
{ href: '/analytics', icon: 'fa-chart-line', title: 'Analytics', desc: 'Open the Track 3 analytics hub for network and bridge insight.', action: 'showAnalytics();' },
{ href: '/operator', icon: 'fa-cog', title: 'Operator', desc: 'Open the Track 4 operator panel for deployment and maintenance tools.', action: 'showOperator();' }
];
var html = '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem;">';
cards.forEach(function(card) {
html += '<a href="' + card.href + '" onclick="event.preventDefault(); ' + card.action + ' updatePath(\'' + card.href + '\'); closeNavMenu();" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius: 14px; padding: 1rem; background: var(--light); box-shadow: 0 2px 8px rgba(0,0,0,0.04);">';
html += '<div style="display:flex; align-items:center; gap:0.75rem; margin-bottom:0.75rem;"><span style="width:2.2rem; height:2.2rem; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; background: rgba(102,126,234,0.12); color: var(--primary);"><i class="fas ' + escapeHtml(card.icon) + '"></i></span><strong style="font-size:1rem;">' + escapeHtml(card.title) + '</strong></div>';
html += '<div style="color: var(--text-light); font-size: 0.92rem; line-height: 1.5;">' + escapeHtml(card.desc) + '</div>';
html += '</a>';
});
html += '</div>';
container.innerHTML = html;
}
window._showMore = showMore;
async function refreshBridgeData() {
const container = document.getElementById('bridgeContent');
if (!container) return;
@@ -2229,8 +2412,11 @@
}
};
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 = `
let html = filterBar + `
<div class="bridge-chain-card">
<div class="chain-name"><i class="fas fa-network-wired"></i> CCIP Bridge Ecosystem</div>
<div style="color: var(--text-light); margin-bottom: 1rem;">
@@ -2284,7 +2470,16 @@
`;
// Add WETH9 routes
for (const [chain, address] of Object.entries(routes.weth9)) {
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="3" 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>
@@ -2320,7 +2515,10 @@
`;
// Add WETH10 routes
for (const [chain, address] of Object.entries(routes.weth10)) {
if (weth10Routes.length === 0) {
html += '<tr><td colspan="3" 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>
@@ -2703,17 +2901,30 @@
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 = '<p style="color: var(--text-light);">No internal transactions</p>';
internalEl.innerHTML = internalFilterBar + '<p style="color: var(--text-light);">No internal transactions</p>';
} else {
let tbl = '<table class="table"><thead><tr><th>Type</th><th>From</th><th>To</th><th>Value</th></tr></thead><tbody>';
internals.forEach(function(it) {
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 class="hash" onclick="showAddressDetail(\'' + escapeHtml(from) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(from)) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(to) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(to)) + '</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;
}
@@ -2721,17 +2932,31 @@
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 = '<p style="color: var(--text-light);">No event logs</p>';
logsEl.innerHTML = logsFilterBar + '<p style="color: var(--text-light);">No event logs</p>';
} else {
let tbl = '<table class="table"><thead><tr><th>Address</th><th>Topics</th><th>Data</th><th>Decoded</th></tr></thead><tbody>';
logs.forEach(function(log, idx) {
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 class="hash" onclick="showAddressDetail(\'' + escapeHtml(addr) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(addr)) + '</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) {
@@ -3024,12 +3249,26 @@
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 = '<p style="color: var(--text-light);">No token balances</p>';
el.innerHTML = filterBar + '<p style="color: var(--text-light);">No token balances</p>';
return;
}
let tbl = '<table class="table"><thead><tr><th>Token</th><th>Contract</th><th>Balance</th><th>Type</th></tr></thead><tbody>';
items.forEach(function(b) {
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 || '-';
@@ -3040,6 +3279,9 @@
const type = token.type || b.token_type || 'ERC-20';
tbl += '<tr><td><a href="/token/' + encodeURIComponent(contract) + '">' + escapeHtml(symbol) + '</a></td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(contract) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(contract)) + '</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) {
@@ -3063,12 +3305,23 @@
}) : [];
}
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 = '<p style="color: var(--text-light);">No NFT tokens</p>';
el.innerHTML = filterBar + '<p style="color: var(--text-light);">No NFT tokens</p>';
return;
}
var tbl = '<table class="table"><thead><tr><th>Contract</th><th>Token ID</th><th>Name / Symbol</th><th>Balance</th></tr></thead><tbody>';
items.forEach(function(b) {
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 : '-'));
@@ -3078,6 +3331,9 @@
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) {
@@ -3093,12 +3349,24 @@
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 = '<p style="color: var(--text-light);">No internal transactions</p>';
el.innerHTML = filterBar + '<p style="color: var(--text-light);">No internal transactions</p>';
return;
}
let tbl = '<table class="table"><thead><tr><th>Block</th><th>From</th><th>To</th><th>Value</th><th>Tx Hash</th></tr></thead><tbody>';
items.slice(0, 25).forEach(function(it) {
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';
@@ -3106,6 +3374,9 @@
const txHash = it.transaction_hash || it.tx_hash || '-';
tbl += '<tr><td onclick="showBlockDetail(\'' + escapeHtml(block) + '\')" style="cursor: pointer;">' + escapeHtml(block) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(from) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(from)) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(to) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(to)) + '</td><td>' + escapeHtml(val) + ' ETH</td><td class="hash" onclick="showTransactionDetail(\'' + escapeHtml(txHash) + '\')" style="cursor: pointer;">' + (txHash !== '-' ? escapeHtml(shortenHash(txHash)) : '-') + '</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) {
@@ -3296,15 +3567,24 @@
}
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) {
let txHtml = '<table class="table"><thead><tr><th>Hash</th><th>Block</th><th>From</th><th>To</th><th>Value</th></tr></thead><tbody>';
txs.data.forEach(function(tx) {
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 class="hash">' + escapeHtml(shortenHash(tx.hash)) + '</td><td>' + escapeHtml(String(tx.block_number)) + '</td><td class="hash" onclick="event.stopPropagation(); showAddressDetail(\'' + escapeHtml(tx.from) + '\')" style="cursor: pointer;">' + formatAddressWithLabel(tx.from) + '</td><td class="hash" onclick="event.stopPropagation(); showAddressDetail(\'' + escapeHtml(tx.to || '') + '\')" style="cursor: pointer;">' + (tx.to ? formatAddressWithLabel(tx.to) : '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 = '<p>No transactions found</p>';
txContainer.innerHTML = filterBar + '<p>No transactions found</p>';
}
}
} catch (e) {
@@ -3370,12 +3650,23 @@
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 += '<p style="color: var(--text-light);">No transfers</p>';
html += transfersFilterBar + '<p style="color: var(--text-light);">No transfers</p>';
} else {
html += '<table class="table"><thead><tr><th>From</th><th>To</th><th>Value</th><th>Tx</th></tr></thead><tbody>';
transfers.forEach(function(tr) {
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');
@@ -3384,6 +3675,9 @@
var txHash = tr.transaction_hash || tr.tx_hash || '';
html += '<tr><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(from) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(from)) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(to) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(to)) + '</td><td>' + escapeHtml(v.toLocaleString(undefined, { maximumFractionDigits: 6 })) + '</td><td class="hash" onclick="showTransactionDetail(\'' + escapeHtml(txHash) + '\')" style="cursor: pointer;">' + (txHash ? escapeHtml(shortenHash(txHash)) : '-') + '</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>';

View File

@@ -888,17 +888,12 @@
<li role="none"><a href="/bridge" role="menuitem" onclick="event.preventDefault(); showBridgeMonitoring(); updatePath('/bridge'); closeNavMenu();" aria-label="View bridge monitoring"><i class="fas fa-bridge" aria-hidden="true"></i> <span data-i18n="bridge">Bridge</span></a></li>
<li role="none"><a href="/weth" role="menuitem" onclick="event.preventDefault(); showWETHUtilities(); updatePath('/weth'); closeNavMenu();" aria-label="View WETH utilities"><i class="fas fa-coins" aria-hidden="true"></i> <span data-i18n="weth">WETH</span></a></li>
<li role="none"><a href="/tokens" role="menuitem" onclick="event.preventDefault(); if(typeof showTokensList==='function')showTokensList();else focusSearchWithHint('token'); updatePath('/tokens'); closeNavMenu();" aria-label="View token list"><i class="fas fa-tag" aria-hidden="true"></i> <span data-i18n="tokens">Tokens</span></a></li>
<li role="none"><a href="/pools" role="menuitem" onclick="event.preventDefault(); showPools(); updatePath('/pools'); closeNavMenu();" aria-label="View pools"><i class="fas fa-water" aria-hidden="true"></i> <span data-i18n="pools">Pools</span></a></li>
<li role="none"><a href="/watchlist" role="menuitem" onclick="event.preventDefault(); showWatchlist(); updatePath('/watchlist'); closeNavMenu();" aria-label="Watchlist"><i class="fas fa-star" aria-hidden="true"></i> <span data-i18n="watchlist">Watchlist</span></a></li>
</ul>
</li>
<li><a href="/snap/" target="_self" rel="noopener" aria-label="Chain 138 MetaMask Snap"><i class="fas fa-wallet" aria-hidden="true"></i> <span>MetaMask Snap</span></a></li>
<li class="nav-dropdown" id="navDropdownMoreWrap" style="display: none;">
<button type="button" class="nav-dropdown-trigger" aria-expanded="false" aria-haspopup="true" aria-controls="navMenuMore" id="navTriggerMore"><i class="fas fa-ellipsis-h" aria-hidden="true"></i> <span data-i18n="more">More</span> <i class="fas fa-chevron-down" aria-hidden="true"></i></button>
<ul class="nav-dropdown-menu" id="navMenuMore" role="menu">
<li role="none" id="analyticsNav" style="display: none;"><a href="/analytics" role="menuitem" onclick="event.preventDefault(); showAnalytics(); updatePath('/analytics'); closeNavMenu();" aria-label="View analytics"><i class="fas fa-chart-line" aria-hidden="true"></i> <span data-i18n="analytics">Analytics</span></a></li>
<li role="none" id="operatorNav" style="display: none;"><a href="/operator" role="menuitem" onclick="event.preventDefault(); showOperator(); updatePath('/operator'); closeNavMenu();" aria-label="View operator panel"><i class="fas fa-cog" aria-hidden="true"></i> <span data-i18n="operator">Operator</span></a></li>
</ul>
</li>
<li role="none"><a href="/more" role="menuitem" onclick="event.preventDefault(); showMore(); updatePath('/more'); closeNavMenu();" aria-label="View more pages"><i class="fas fa-ellipsis-h" aria-hidden="true"></i> <span data-i18n="more">More</span></a></li>
</ul>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<select id="localeSelect" onchange="setLocale(this.value)" style="padding: 0.35rem 0.5rem; border-radius: 6px; background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); font-size: 0.875rem;" aria-label="Language">
@@ -1251,6 +1246,32 @@
</div>
</div>
<div id="poolsView" class="detail-view">
<div class="breadcrumb" id="poolsBreadcrumb"><a href="/home">Home</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">Pools</span></div>
<div class="card">
<div class="card-header">
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back to home page"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
<h2 class="card-title"><i class="fas fa-water"></i> Pools</h2>
</div>
<div id="poolsContent">
<div class="loading"><i class="fas fa-spinner"></i> Loading pools...</div>
</div>
</div>
</div>
<div id="moreView" class="detail-view">
<div class="breadcrumb" id="moreBreadcrumb"><a href="/home">Home</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">More</span></div>
<div class="card">
<div class="card-header">
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back to home page"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
<h2 class="card-title"><i class="fas fa-ellipsis-h"></i> More</h2>
</div>
<div id="moreContent">
<div class="loading"><i class="fas fa-spinner"></i> Loading more pages...</div>
</div>
</div>
</div>
<div id="analyticsView" class="detail-view">
<div class="card">
<div class="card-header">
@@ -1276,6 +1297,6 @@
</div>
</div>
<script src="/explorer-spa.js?v=9"></script>
<script src="/explorer-spa.js?v=16"></script>
</body>
</html>