diff --git a/docs/ENV_VERIFICATION_REPORT.md b/docs/ENV_VERIFICATION_REPORT.md index 986ca7c..778f3dc 100644 --- a/docs/ENV_VERIFICATION_REPORT.md +++ b/docs/ENV_VERIFICATION_REPORT.md @@ -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 | diff --git a/frontend/public/explorer-spa.js b/frontend/public/explorer-spa.js index 68e93a6..66f2f73 100644 --- a/frontend/public/explorer-spa.js +++ b/frontend/public/explorer-spa.js @@ -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 = '

No addresses in watchlist. Open an address and click "Add to watchlist".

'; return; } var html = ''; list.forEach(function(addr){ var label = getAddressLabel(addr) || ''; html += ''; }); html += '
AddressLabel
' + escapeHtml(shortenHash(addr)) + '' + escapeHtml(label) + '
'; 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, '"'); } + function renderPageFilterBar(key, placeholder, helperText, reloadJs) { + var inputId = key + 'FilterInput'; + var value = getExplorerPageFilter(key); + var safeReload = reloadJs ? String(reloadJs) : ''; + var html = '
'; + html += ''; + html += ''; + html += ''; + if (helperText) html += '' + escapeHtml(helperText) + ''; + html += '
'; + 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 + '

No addresses in watchlist. Open an address and click "Add to watchlist".

'; return; } + if (filtered.length === 0) { container.innerHTML = filterBar + '

No watchlist entries match the current filter.

'; return; } + var html = filterBar + ''; + filtered.forEach(function(addr){ var label = getAddressLabel(addr) || ''; html += ''; }); + html += '
AddressLabel
' + escapeHtml(shortenHash(addr)) + '' + escapeHtml(label) + '
'; + 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 += '/'; breadcrumbHTML += 'Token ' + escapeHtml(shortenHash(identifier)) + ''; break; + case 'pools': + breadcrumbContainer = document.getElementById('poolsBreadcrumb'); + breadcrumbHTML += '/'; + breadcrumbHTML += 'Pools'; + break; + case 'more': + breadcrumbContainer = document.getElementById('moreBreadcrumb'); + breadcrumbHTML += '/'; + breadcrumbHTML += 'More'; + break; case 'nft': breadcrumbContainer = document.getElementById('nftDetailBreadcrumb'); breadcrumbHTML += '/'; @@ -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 = '
No blocks found.
'; + if (container) container.innerHTML = filterBar + '
No blocks found.
'; + } else if (filteredBlocks.length === 0) { + if (container) container.innerHTML = filterBar + '
No blocks match the current filter.
'; } else { // Create HTML with duplicated blocks for seamless infinite loop - let html = '
'; + let html = filterBar + '
'; html += '
'; // 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 = ''; + let html = filterBar + '
HashFromToValueBlock
'; if (limitedTransactions.length === 0) { html += ''; + } else if (filteredTransactions.length === 0) { + html += ''; } 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 = '
HashFromToValueBlock
No transactions found
No transactions match the current filter.
'; + 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 + '
BlockHashTransactionsTimestamp
'; if (blocks.length === 0) { html += ''; + } else if (filteredBlocks.length === 0) { + html += ''; } else { - blocks.forEach(function(block) { + filteredBlocks.forEach(function(block) { var d = normalizeBlockDisplay(block); html += ''; }); @@ -2124,12 +2216,24 @@ } } - let html = '
BlockHashTransactionsTimestamp
No blocks found
No blocks match the current filter
' + escapeHtml(String(d.blockNum)) + '' + escapeHtml(shortenHash(d.hash)) + '' + escapeHtml(String(d.txCount)) + '' + escapeHtml(d.timestampFormatted) + '
'; + 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 + '
HashFromToValueBlock
'; if (transactions.length === 0) { html += ''; + } else if (filteredTransactions.length === 0) { + html += ''; } 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 = '
HashFromToValueBlock
No transactions found
No transactions match the current filter
'; - 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 + '
TokenContractType
'; + 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 += ''; }); + if (filteredItems.length === 0) { + html += ''; + } html += '
TokenContractType
' + escapeHtml(name) + (symbolDisplay ? ' (' + escapeHtml(symbolDisplay) + ')' : '') + '' + escapeHtml(shortenHash(addr)) + '' + escapeHtml(type) + '
No tokens match the current filter.
'; container.innerHTML = html; return; } } catch (e) {} } - container.innerHTML = '

No token index available. Use the search bar to find tokens by name, symbol, or contract address (0x...).

'; + container.innerHTML = renderPageFilterBar('tokensList', 'Filter by token name, symbol, contract, or type...', 'Filters the indexed token list below.', 'loadTokensList()') + '

No token index available. Use the search bar to find tokens by name, symbol, or contract address (0x...).

'; } catch (err) { - container.innerHTML = '
Failed to load tokens. Use the search bar to find a token by address or name.
'; + container.innerHTML = renderPageFilterBar('tokensList', 'Filter by token name, symbol, contract, or type...', 'Filters the indexed token list below.', 'loadTokensList()') + '
Failed to load tokens. Use the search bar to find a token by address or name.
'; } } window._loadTokensList = loadTokensList; + function 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 + '
This page lists known liquidity, reserve, and bridge pool references. Pool entries are grouped by role, and placeholder rows mark planned or external assets.
'; + html += '
'; + if (rows.length === 0) { + html += ''; + } else if (filtered.length === 0) { + html += ''; + } else { + filtered.forEach(function(entry) { + var row = entry.row; + var addr = row.address || ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + } + html += '
CategoryPool PairSystemAddressStatusNotes
No pool data available yet.
No pools match the current filter.
' + escapeHtml(row.category) + '' + escapeHtml(row.poolPair) + '' + escapeHtml(row.poolType) + '' + (safeAddress(addr) ? '' + escapeHtml(shortenHash(addr)) + '' : '') + '' + escapeHtml(row.status) + '' + escapeHtml(row.notes) + '
'; + container.innerHTML = html; + } catch (err) { + container.innerHTML = '
Failed to load pools: ' + escapeHtml(err.message || 'Unknown error') + '
'; + } + } + 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 = '
'; + cards.forEach(function(card) { + html += ''; + html += '
' + escapeHtml(card.title) + '
'; + html += '
' + escapeHtml(card.desc) + '
'; + html += '
'; + }); + html += '
'; + 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 + `
CCIP Bridge Ecosystem
@@ -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 += 'No WETH9 routes match the current filter.'; + } + for (const [chain, address] of weth9Routes) { const chainId = chain.match(/\\((\d+)\\)/)?.[1] || ''; html += ` @@ -2320,7 +2515,10 @@ `; // Add WETH10 routes - for (const [chain, address] of Object.entries(routes.weth10)) { + if (weth10Routes.length === 0) { + html += 'No WETH10 routes match the current filter.'; + } + for (const [chain, address] of weth10Routes) { const chainId = chain.match(/\\((\d+)\\)/)?.[1] || ''; html += ` @@ -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 = '

No internal transactions

'; + internalEl.innerHTML = internalFilterBar + '

No internal transactions

'; } else { - let tbl = ''; - 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 + '
TypeFromToValue
'; + 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 += ''; }); + if (filteredInternals.length === 0) { + tbl += ''; + } tbl += '
TypeFromToValue
' + escapeHtml(type) + '' + escapeHtml(shortenHash(from)) + '' + escapeHtml(shortenHash(to)) + '' + escapeHtml(val) + ' ETH
No internal transactions match the current filter.
'; 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 = '

No event logs

'; + logsEl.innerHTML = logsFilterBar + '

No event logs

'; } else { - let tbl = ''; - 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 + '
AddressTopicsDataDecoded
'; + 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 += ''; }); + if (filteredLogs.length === 0) { + tbl += ''; + } tbl += '
AddressTopicsDataDecoded
' + escapeHtml(shortenHash(addr)) + '' + escapeHtml(String(topicsStr).substring(0, 80)) + (String(topicsStr).length > 80 ? '...' : '') + '' + escapeHtml(String(data).substring(0, 66)) + (String(data).length > 66 ? '...' : '') + '
No event logs match the current filter.
'; 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 = '

No token balances

'; + el.innerHTML = filterBar + '

No token balances

'; return; } - let tbl = ''; - 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 + '
TokenContractBalanceType
'; + 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 += ''; }); + if (filteredItems.length === 0) { + tbl += ''; + } tbl += '
TokenContractBalanceType
' + escapeHtml(symbol) + '' + escapeHtml(shortenHash(contract)) + '' + escapeHtml(displayBalance) + '' + escapeHtml(type) + '
No token balances match the current filter.
'; 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 = '

No NFT tokens

'; + el.innerHTML = filterBar + '

No NFT tokens

'; return; } - var tbl = ''; - 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 + '
ContractToken IDName / SymbolBalance
'; + 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 += ''; tbl += ''; }); + if (filteredItems.length === 0) { + tbl += ''; + } tbl += '
ContractToken IDName / SymbolBalance
' + (tokenId !== '-' ? '' + escapeHtml(String(tokenId)) + '' : '-') + '' + escapeHtml(name) + '' + escapeHtml(String(balance)) + '
No NFT inventory matches the current filter.
'; 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 = '

No internal transactions

'; + el.innerHTML = filterBar + '

No internal transactions

'; return; } - let tbl = ''; - 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 + '
BlockFromToValueTx Hash
'; + filteredItems.forEach(function(it) { const from = it.from?.hash || it.from || 'N/A'; const to = it.to?.hash || it.to || 'N/A'; const val = it.value ? formatEther(it.value) : '0'; @@ -3106,6 +3374,9 @@ const txHash = it.transaction_hash || it.tx_hash || '-'; tbl += ''; }); + if (filteredItems.length === 0) { + tbl += ''; + } tbl += '
BlockFromToValueTx Hash
' + escapeHtml(block) + '' + escapeHtml(shortenHash(from)) + '' + escapeHtml(shortenHash(to)) + '' + escapeHtml(val) + ' ETH' + (txHash !== '-' ? escapeHtml(shortenHash(txHash)) : '-') + '
No internal transactions match the current filter.
'; 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 = ''; - 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 + '
HashBlockFromToValue
'; + filteredTxs.forEach(function(tx) { txHtml += ''; }); + if (filteredTxs.length === 0) { + txHtml += ''; + } txHtml += '
HashBlockFromToValue
' + escapeHtml(shortenHash(tx.hash)) + '' + escapeHtml(String(tx.block_number)) + '' + formatAddressWithLabel(tx.from) + '' + (tx.to ? formatAddressWithLabel(tx.to) : 'N/A') + '' + escapeHtml(formatEther(tx.value || '0')) + ' ETH
No transactions match the current filter.
'; txContainer.innerHTML = txHtml; } else { - txContainer.innerHTML = '

No transactions found

'; + txContainer.innerHTML = filterBar + '

No transactions found

'; } } } catch (e) { @@ -3370,12 +3650,23 @@ html += '
Decimals
' + decimals + '
'; html += '
Total Supply
' + supplyNum.toLocaleString(undefined, { maximumFractionDigits: 6 }) + '
'; html += '
Holders
' + (holders !== '-' ? formatNumber(holders) : '-') + '
'; + const transfersFilter = getExplorerPageFilter('tokenTransfers'); + const transfersFilterBar = renderPageFilterBar('tokenTransfers', 'Filter by from, to, value, or tx hash...', 'Filters the recent transfers below.', 'showTokenDetail(\'' + addrEsc + '\')'); html += '

Recent Transfers

'; if (transfers.length === 0) { - html += '

No transfers

'; + html += transfersFilterBar + '

No transfers

'; } else { - html += ''; - 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 + '
FromToValueTx
'; + 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 += ''; }); + if (filteredTransfers.length === 0) { + html += ''; + } html += '
FromToValueTx
' + escapeHtml(shortenHash(from)) + '' + escapeHtml(shortenHash(to)) + '' + escapeHtml(v.toLocaleString(undefined, { maximumFractionDigits: 6 })) + '' + (txHash ? escapeHtml(shortenHash(txHash)) : '-') + '
No transfers match the current filter.
'; } html += '
'; diff --git a/frontend/public/index.html b/frontend/public/index.html index 28bfcf4..d2adf33 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -888,17 +888,12 @@
  • Bridge
  • WETH
  • Tokens
  • +
  • Pools
  • Watchlist
  • MetaMask Snap
  • - +
  • More