2026-02-16 03:09:53 -08:00
const API _BASE = '/api' ;
2026-03-27 14:12:14 -07:00
const EXPLORER _API _BASE = '/explorer-api' ;
const EXPLORER _API _V1 _BASE = EXPLORER _API _BASE + '/v1' ;
2026-03-27 12:02:36 -07:00
const TOKEN _AGGREGATION _API _BASE = '/token-aggregation/api' ;
2026-03-27 14:12:14 -07:00
const EXPLORER _AI _API _BASE = EXPLORER _API _V1 _BASE + '/ai' ;
2026-02-16 03:09:53 -08:00
const FETCH _TIMEOUT _MS = 15000 ;
const RPC _HEALTH _TIMEOUT _MS = 5000 ;
const FETCH _MAX _RETRIES = 3 ;
const RETRY _DELAY _MS = 1000 ;
window . DEBUG _EXPLORER = false ;
2026-02-22 15:35:45 -08:00
let _apiUnavailableBannerShown = false ;
function showAPIUnavailableBanner ( status ) {
if ( _apiUnavailableBannerShown ) return ;
_apiUnavailableBannerShown = true ;
var main = document . getElementById ( 'mainContent' ) ;
if ( ! main ) return ;
var banner = document . createElement ( 'div' ) ;
banner . id = 'apiUnavailableBanner' ;
banner . setAttribute ( 'role' , 'alert' ) ;
banner . style . cssText = 'background: rgba(200,80,80,0.95); color: #fff; padding: 0.75rem 1rem; margin-bottom: 1rem; border-radius: 8px; font-size: 0.9rem;' ;
2026-03-28 00:21:18 -07:00
banner . innerHTML = '<strong>Explorer API temporarily unavailable</strong> (HTTP ' + status + '). Stats, blocks, and transactions cannot load until the backend is running. <a href="https://gitea.d-bis.org/d-bis/explorer-monorepo/src/branch/main/docs/EXPLORER_API_ACCESS.md" target="_blank" rel="noopener noreferrer" style="color: #fff; text-decoration: underline;">See docs</a>.' ;
2026-02-22 15:35:45 -08:00
main . insertBefore ( banner , main . firstChild ) ;
}
2026-02-16 03:09:53 -08:00
( function ( ) {
var _log = console . log , _warn = console . warn ;
console . log = function ( ) { if ( window . DEBUG _EXPLORER ) _log . apply ( console , arguments ) ; } ;
console . warn = function ( ) { if ( window . DEBUG _EXPLORER ) _warn . apply ( console , arguments ) ; } ;
} ) ( ) ;
// RPC/WebSocket: VMID 2201 (public RPC). FQDN when HTTPS (avoids mixed content); IP when HTTP (e.g. http://192.168.11.140)
const RPC _IP = 'http://192.168.11.221:8545' ; // Chain 138 - VMID 2201 besu-rpc-public-1
const RPC _WS _IP = 'ws://192.168.11.221:8546' ;
const RPC _FQDN = 'https://rpc-http-pub.d-bis.org' ; // VMID 2201 - HTTPS
const RPC _WS _FQDN = 'wss://rpc-ws-pub.d-bis.org' ;
const RPC _URLS = ( typeof window !== 'undefined' && window . location && window . location . protocol === 'https:' )
? [ RPC _FQDN ] : [ RPC _IP ] ;
const RPC _URL = ( typeof window !== 'undefined' && window . location && window . location . protocol === 'https:' ) ? RPC _FQDN : RPC _IP ;
const RPC _WS _URL = ( typeof window !== 'undefined' && window . location && window . location . protocol === 'https:' ) ? RPC _WS _FQDN : RPC _WS _IP ;
let _rpcUrlIndex = 0 ;
let _blocksScrollAnimationId = null ;
2026-03-27 13:37:53 -07:00
let _explorerAIState = {
open : false ,
loading : false ,
messages : [
{
role : 'assistant' ,
content : 'Explorer AI is ready for read-only ecosystem analysis. Ask about routes, liquidity, bridges, addresses, transactions, or current Chain 138 status.'
}
]
} ;
2026-02-16 03:09:53 -08:00
async function getRpcUrl ( ) {
if ( RPC _URLS . length <= 1 ) return RPC _URLS [ 0 ] ;
const ac = new AbortController ( ) ;
const t = setTimeout ( ( ) => ac . abort ( ) , RPC _HEALTH _TIMEOUT _MS ) ;
for ( let i = 0 ; i < RPC _URLS . length ; i ++ ) {
const url = RPC _URLS [ ( _rpcUrlIndex + i ) % RPC _URLS . length ] ;
try {
const r = await fetch ( url , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { jsonrpc : '2.0' , method : 'eth_blockNumber' , params : [ ] , id : 1 } ) , signal : ac . signal } ) ;
clearTimeout ( t ) ;
if ( r . ok ) { _rpcUrlIndex = ( _rpcUrlIndex + i ) % RPC _URLS . length ; return url ; }
} catch ( e ) { }
}
clearTimeout ( t ) ;
return RPC _URLS [ _rpcUrlIndex % RPC _URLS . length ] ;
}
const CHAIN _ID = 138 ; // Hyperledger Besu ChainID 138
async function rpcCall ( method , params ) {
const url = await getRpcUrl ( ) ;
const r = await fetch ( url , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body : JSON . stringify ( { jsonrpc : '2.0' , method , params : params || [ ] , id : 1 } ) } ) ;
const j = await r . json ( ) ;
if ( j . error ) throw new Error ( j . error . message || 'RPC error' ) ;
return j . result ;
}
const BLOCKSCOUT _API _ORIGIN = 'https://explorer.d-bis.org/api' ; // fallback when not on explorer host
2026-02-22 15:35:45 -08:00
// Use relative /api when on explorer host so API always hits same host (avoids CORS/origin mismatch with www, port, or proxy)
const EXPLORER _HOSTS = [ 'explorer.d-bis.org' , '192.168.11.140' ] ;
const isOnExplorerHost = ( typeof window !== 'undefined' && window . location && window . location . hostname && EXPLORER _HOSTS . indexOf ( window . location . hostname ) !== - 1 ) ;
const BLOCKSCOUT _API = isOnExplorerHost ? '/api' : BLOCKSCOUT _API _ORIGIN ;
2026-02-16 03:09:53 -08:00
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 = {
2026-03-27 12:02:36 -07:00
en : { home : 'Home' , blocks : 'Blocks' , transactions : 'Transactions' , addresses : 'Addresses' , bridge : 'Bridge' , weth : 'WETH' , tokens : 'Tokens' , pools : 'Pools' , more : 'More' , analytics : 'Analytics' , operator : 'Operator' , watchlist : 'Watchlist' , searchPlaceholder : 'Address, tx hash, block number, or token/contract name...' , connectWallet : 'Connect Wallet' , darkMode : 'Dark mode' , lightMode : 'Light mode' , back : 'Back' , exportCsv : 'Export CSV' , tokenBalances : 'Token Balances' , internalTxns : 'Internal Txns' , readContract : 'Read contract' , writeContract : 'Write contract' , addToWatchlist : 'Add to watchlist' , removeFromWatchlist : 'Remove from watchlist' , checkApprovals : 'Check token approvals' , copied : 'Copied' } ,
de : { home : 'Start' , blocks : 'Blöcke' , transactions : 'Transaktionen' , addresses : 'Adressen' , bridge : 'Brücke' , weth : 'WETH' , tokens : 'Token' , pools : 'Pools' , more : 'Mehr' , analytics : 'Analysen' , operator : 'Operator' , watchlist : 'Beobachtungsliste' , searchPlaceholder : 'Adresse, Tx-Hash, Blocknummer oder Token/Vertrag…' , connectWallet : 'Wallet verbinden' , darkMode : 'Dunkelmodus' , lightMode : 'Hellmodus' , back : 'Zurück' , exportCsv : 'CSV exportieren' , tokenBalances : 'Token-Bestände' , internalTxns : 'Interne Transaktionen' , readContract : 'Vertrag lesen' , writeContract : 'Vertrag schreiben' , addToWatchlist : 'Zur Beobachtungsliste' , removeFromWatchlist : 'Aus Beobachtungsliste entfernen' , checkApprovals : 'Token-Freigaben prüfen' , copied : 'Kopiert' } ,
fr : { home : 'Accueil' , blocks : 'Blocs' , transactions : 'Transactions' , addresses : 'Adresses' , bridge : 'Pont' , weth : 'WETH' , tokens : 'Jetons' , pools : 'Pools' , more : 'Plus' , analytics : 'Analyses' , operator : 'Opérateur' , watchlist : 'Liste de suivi' , searchPlaceholder : 'Adresse, hash de tx, numéro de bloc ou nom de token/contrat…' , connectWallet : 'Connecter le portefeuille' , darkMode : 'Mode sombre' , lightMode : 'Mode clair' , back : 'Retour' , exportCsv : 'Exporter CSV' , tokenBalances : 'Soldes de jetons' , internalTxns : 'Transactions internes' , readContract : 'Lire le contrat' , writeContract : 'Écrire le contrat' , addToWatchlist : 'Ajouter à la liste' , removeFromWatchlist : 'Retirer de la liste' , checkApprovals : 'Vérifier les approbations' , copied : 'Copié' }
2026-02-16 03:09:53 -08:00
} ;
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 ( ) ; }
2026-03-27 12:02:36 -07:00
function applyI18n ( ) { document . querySelectorAll ( '[data-i18n]' ) . forEach ( function ( el ) { var k = el . getAttribute ( 'data-i18n' ) ; if ( k ) el . textContent = t ( k ) ; } ) ; var searchIn = document . getElementById ( 'smartSearchInput' ) ; if ( searchIn ) searchIn . placeholder = t ( 'searchPlaceholder' ) ; var localeSel = document . getElementById ( 'localeSelect' ) ; if ( localeSel ) localeSel . value = currentLocale ; var wcBtn = document . getElementById ( 'walletConnectBtn' ) ; if ( wcBtn ) wcBtn . textContent = t ( 'connectWallet' ) ; }
2026-03-24 18:11:08 -07:00
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 , '"' ) ; }
2026-03-27 12:02:36 -07:00
const SMART _SEARCH _HISTORY _KEY = 'explorerSmartSearchHistory' ;
const SMART _SEARCH _HISTORY _LIMIT = 8 ;
let _smartSearchScope = 'all' ;
let _smartSearchPreviewTimer = null ;
let _smartSearchPreviewRequestId = 0 ;
let _smartSearchTrendingCache = null ;
function getSmartSearchHistory ( ) {
try {
var raw = localStorage . getItem ( SMART _SEARCH _HISTORY _KEY ) ;
var parsed = raw ? JSON . parse ( raw ) : [ ] ;
return Array . isArray ( parsed ) ? parsed . filter ( Boolean ) : [ ] ;
} catch ( e ) {
return [ ] ;
}
}
function saveSmartSearchHistory ( query ) {
var value = String ( query || '' ) . trim ( ) ;
if ( ! value ) return ;
try {
var history = getSmartSearchHistory ( ) . filter ( function ( item ) {
return String ( item ) . toLowerCase ( ) !== value . toLowerCase ( ) ;
} ) ;
history . unshift ( value ) ;
history = history . slice ( 0 , SMART _SEARCH _HISTORY _LIMIT ) ;
localStorage . setItem ( SMART _SEARCH _HISTORY _KEY , JSON . stringify ( history ) ) ;
} catch ( e ) { }
}
function detectSmartSearchType ( query ) {
var value = String ( query || '' ) . trim ( ) ;
var normalized = value . replace ( /\s/g , '' ) ;
if ( ! value ) return { type : 'recent' , label : 'Recent searches' , detail : 'Start typing to narrow the explorer.' } ;
if ( /^0x[a-fA-F0-9]{64}$/ . test ( normalized ) ) return { type : 'transaction' , label : 'Transaction hash' , detail : 'Enter will open the transaction detail page.' } ;
if ( /^0x[a-fA-F0-9]{40}$/ . test ( normalized ) ) return { type : 'address' , label : 'Address' , detail : 'Enter will open the address detail page.' } ;
if ( /^\d+$/ . test ( value ) ) return { type : 'block' , label : 'Block number' , detail : 'Enter will open the block detail page.' } ;
if ( /\.eth$/i . test ( value ) ) return { type : 'ens' , label : 'ENS/domain' , detail : 'The explorer will search or resolve this name.' } ;
if ( /^[a-z0-9][a-z0-9._:-]{1,31}$/i . test ( value ) ) return { type : 'token' , label : 'Token / asset symbol' , detail : 'The explorer will search token and contract matches.' } ;
return { type : 'search' , label : 'Explorer search' , detail : 'The explorer will search across indexed results.' } ;
}
function normalizeSmartSearchItemType ( item ) {
var type = String ( ( item && ( item . type || item . address _type || item . entity _type || item . kind ) ) || '' ) . toLowerCase ( ) ;
if ( item && ( item . tx _hash || ( item . hash && String ( item . hash ) . length === 66 ) ) ) return 'transactions' ;
if ( item && ( item . block _number != null ) ) return 'blocks' ;
if ( item && ( item . token _address || item . token _contract _address _hash ) ) return 'tokens' ;
if ( type . indexOf ( 'tx' ) !== - 1 || type . indexOf ( 'transaction' ) !== - 1 ) return 'transactions' ;
if ( type . indexOf ( 'block' ) !== - 1 ) return 'blocks' ;
if ( type . indexOf ( 'token' ) !== - 1 || type . indexOf ( 'contract' ) !== - 1 ) return 'tokens' ;
if ( type . indexOf ( 'address' ) !== - 1 ) return 'addresses' ;
return 'all' ;
}
function setSmartSearchScope ( scope ) {
_smartSearchScope = scope || 'all' ;
try {
document . querySelectorAll ( '.smart-search-scope-btn' ) . forEach ( function ( btn ) {
var active = btn . getAttribute ( 'data-scope' ) === _smartSearchScope ;
btn . classList . toggle ( 'btn-primary' , active ) ;
btn . classList . toggle ( 'btn-secondary' , ! active ) ;
} ) ;
} catch ( e ) { }
var input = document . getElementById ( 'smartSearchInput' ) ;
updateSmartSearchPreview ( input ? input . value : '' ) ;
}
window . setSmartSearchScope = setSmartSearchScope ;
function renderSmartSearchHistory ( ) {
var history = getSmartSearchHistory ( ) ;
if ( ! history . length ) {
return '<div style="color:var(--text-light);">No recent searches yet.</div>' ;
}
var html = '<div style="display:grid; gap:0.5rem;">' ;
history . forEach ( function ( item ) {
var safeItem = String ( item ) . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) ;
html += '<button type="button" class="btn btn-secondary" style="text-align:left; justify-content:flex-start; padding:0.65rem 0.8rem;" onclick="openSmartSearchModal(\'' + safeItem + '\')"><i class="fas fa-clock" aria-hidden="true" style="margin-right:0.55rem;"></i>' + escapeHtml ( item ) + '</button>' ;
} ) ;
html += '</div>' ;
return html ;
}
function renderSmartSearchResultCard ( item , action ) {
var type = ( item . type || item . address _type || '' ) . toLowerCase ( ) ;
var title = item . name || item . symbol || item . address _hash || item . hash || item . tx _hash || ( item . block _number != null ? 'Block #' + item . block _number : '' ) || 'Result' ;
var subtitle = item . symbol && item . name ? item . symbol + ' • ' + item . name : item . symbol || item . name || item . address _hash || item . hash || item . tx _hash || '' ;
var meta = [ ] ;
if ( item . address _hash || item . hash || item . token _address || item . token _contract _address _hash ) {
meta . push ( shortenHash ( item . address _hash || item . hash || item . token _address || item . token _contract _address _hash ) ) ;
}
if ( item . block _number != null ) meta . push ( 'Block #' + item . block _number ) ;
if ( item . transaction _count != null ) meta . push ( String ( item . transaction _count ) + ' tx' ) ;
var html = '<button type="button" class="btn btn-secondary" style="width:100%; text-align:left; justify-content:flex-start; padding:0.85rem 0.95rem; border-radius:16px; border:1px solid var(--border); background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.08));"' + ( action ? ' onclick="' + action + '; closeSmartSearchModal();"' : ' disabled' ) + '>' ;
html += '<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:0.85rem; width:100%;">' ;
html += '<div style="min-width:0; flex:1;">' ;
html += '<div style="display:flex; align-items:center; gap:0.5rem; flex-wrap:wrap; margin-bottom:0.35rem;">' ;
html += '<span style="font-size:0.72rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light);">' + escapeHtml ( type || 'result' ) + '</span>' ;
html += '<span style="padding:0.18rem 0.45rem; border-radius:999px; background:rgba(37,99,235,0.12); color:var(--primary); font-size:0.72rem;">' + escapeHtml ( type ? type . toUpperCase ( ) : 'MATCH' ) + '</span>' ;
html += '</div>' ;
html += '<div style="font-size:0.98rem; font-weight:700; line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">' + escapeHtml ( String ( title ) . substring ( 0 , 96 ) ) + '</div>' ;
if ( subtitle ) {
html += '<div style="color:var(--text-light); font-size:0.84rem; line-height:1.35; margin-top:0.2rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">' + escapeHtml ( String ( subtitle ) . substring ( 0 , 120 ) ) + '</div>' ;
}
if ( meta . length ) {
html += '<div style="display:flex; flex-wrap:wrap; gap:0.35rem; margin-top:0.55rem;">' ;
meta . forEach ( function ( bit ) {
html += '<span style="padding:0.18rem 0.45rem; border:1px solid var(--border); border-radius:999px; font-size:0.72rem; color:var(--text-light);">' + escapeHtml ( bit ) + '</span>' ;
} ) ;
html += '</div>' ;
}
html += '</div>' ;
html += '<div style="display:flex; align-items:center; color:var(--text-light); padding-top:0.1rem;"><i class="fas fa-arrow-right"></i></div>' ;
html += '</div>' ;
html += '</button>' ;
return html ;
}
function renderSmartSearchPreviewItems ( items , query ) {
if ( ! items || ! items . length ) {
return '<div style="color:var(--text-light);">No live suggestions yet. Press Enter to search everything, or try a more specific address, hash, block, or token symbol.</div>' ;
}
var html = '<div style="display:grid; gap:0.45rem;">' ;
items . slice ( 0 , 6 ) . forEach ( function ( item ) {
if ( _smartSearchScope !== 'all' && normalizeSmartSearchItemType ( item ) !== _smartSearchScope ) return ;
var action = null ;
if ( item . token _address || item . token _contract _address _hash ) {
var tokenAddr = item . token _address || item . token _contract _address _hash ;
if ( /^0x[a-f0-9]{40}$/i . test ( tokenAddr ) ) action = 'showTokenDetail(\'' + escapeHtml ( tokenAddr ) + '\')' ;
}
if ( ! action && ( item . address _hash || item . hash ) && /^0x[a-f0-9]{40}$/i . test ( item . address _hash || item . hash ) ) {
var addr = item . address _hash || item . hash ;
action = 'showAddressDetail(\'' + escapeHtml ( addr ) + '\')' ;
}
if ( ! action && ( item . tx _hash || ( item . hash && item . hash . length === 66 ) ) && /^0x[a-f0-9]{64}$/i . test ( item . tx _hash || item . hash ) ) {
var txHash = item . tx _hash || item . hash ;
action = 'showTransactionDetail(\'' + escapeHtml ( txHash ) + '\')' ;
}
if ( ! action && item . block _number != null ) {
action = 'showBlockDetail(\'' + escapeHtml ( String ( item . block _number ) ) + '\')' ;
}
html += renderSmartSearchResultCard ( item , action ) ;
} ) ;
html += '</div>' ;
return html ;
}
function renderSmartSearchTrendingTokens ( tokens ) {
if ( ! tokens || ! tokens . length ) {
return '<div style="color:var(--text-light);">No trending tokens found yet.</div>' ;
}
var html = '<div style="display:grid; gap:0.45rem;">' ;
tokens . slice ( 0 , 6 ) . forEach ( function ( token ) {
var tokenAddr = ( token . address && ( token . address . hash || token . address ) ) || token . address _hash || token . token _address || token . contract _address _hash || '' ;
if ( ! tokenAddr ) return ;
var item = {
type : 'token' ,
name : token . name || token . symbol || 'Token' ,
symbol : token . symbol || '' ,
token _address : tokenAddr
} ;
html += renderSmartSearchResultCard ( item , 'showTokenDetail(\'' + String ( tokenAddr ) . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) + '\')' ) ;
} ) ;
html += '</div>' ;
return html ;
}
async function fetchSmartSearchTrendingTokens ( ) {
if ( Array . isArray ( _smartSearchTrendingCache ) ) return _smartSearchTrendingCache ;
try {
var resp = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/tokens?page=1&page_size=6' ) ;
var items = ( resp && ( resp . items || resp . data ) ) || [ ] ;
_smartSearchTrendingCache = Array . isArray ( items ) ? items : [ ] ;
} catch ( e ) {
_smartSearchTrendingCache = [ ] ;
}
return _smartSearchTrendingCache ;
}
async function updateSmartSearchPreview ( query ) {
var input = document . getElementById ( 'smartSearchInput' ) ;
var preview = document . getElementById ( 'smartSearchPreview' ) ;
var detected = document . getElementById ( 'smartSearchDetected' ) ;
if ( ! preview || ! detected ) return ;
var info = detectSmartSearchType ( query ) ;
if ( ! query ) {
detected . style . display = 'inline-block' ;
detected . textContent = _smartSearchScope === 'all' ? 'All' : ( _smartSearchScope . charAt ( 0 ) . toUpperCase ( ) + _smartSearchScope . slice ( 1 ) ) ;
if ( input ) input . setAttribute ( 'aria-describedby' , 'smartSearchDetected' ) ;
var emptyRequestId = ++ _smartSearchPreviewRequestId ;
preview . innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Recent searches</div>' + renderSmartSearchHistory ( ) + '</div><div><div style="font-weight:700; margin-bottom:0.45rem;">Trending tokens</div><div style="color:var(--text-light);">Loading trending token watchlist...</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Scope</div><div style="color:var(--text-light); line-height:1.6;">Current filter: <strong>' + escapeHtml ( _smartSearchScope ) + '</strong>. Use the left rail to narrow the search feed.</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Quick tips</div><div style="color:var(--text-light); line-height:1.6;">Try an address, transaction hash, block number, token symbol, or contract name. Press <strong>Enter</strong> to search and <strong>Esc</strong> to close.</div></div></div>' ;
fetchSmartSearchTrendingTokens ( ) . then ( function ( tokens ) {
if ( emptyRequestId !== _smartSearchPreviewRequestId ) return ;
preview . innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Recent searches</div>' + renderSmartSearchHistory ( ) + '</div><div><div style="font-weight:700; margin-bottom:0.45rem;">Trending tokens</div>' + renderSmartSearchTrendingTokens ( tokens ) + '</div><div><div style="font-weight:700; margin-bottom:0.45rem;">Scope</div><div style="color:var(--text-light); line-height:1.6;">Current filter: <strong>' + escapeHtml ( _smartSearchScope ) + '</strong>. Use the left rail to narrow the search feed.</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Quick tips</div><div style="color:var(--text-light); line-height:1.6;">Try an address, transaction hash, block number, token symbol, or contract name. Press <strong>Enter</strong> to search and <strong>Esc</strong> to close.</div></div></div>' ;
} ) ;
return ;
}
detected . style . display = 'inline-block' ;
detected . textContent = _smartSearchScope === 'all' ? info . label : ( _smartSearchScope . charAt ( 0 ) . toUpperCase ( ) + _smartSearchScope . slice ( 1 ) ) ;
preview . innerHTML = '<div style="color:var(--text-light);">Searching live suggestions...</div>' ;
var requestId = ++ _smartSearchPreviewRequestId ;
if ( _smartSearchPreviewTimer ) {
clearTimeout ( _smartSearchPreviewTimer ) ;
_smartSearchPreviewTimer = null ;
}
_smartSearchPreviewTimer = setTimeout ( async function ( ) {
try {
var liveItems = [ ] ;
if ( CHAIN _ID === 138 ) {
var resp = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/search?q=' + encodeURIComponent ( query ) ) ;
if ( requestId !== _smartSearchPreviewRequestId ) return ;
liveItems = ( resp && resp . items ) ? resp . items : [ ] ;
}
if ( requestId !== _smartSearchPreviewRequestId ) return ;
if ( _smartSearchScope !== 'all' ) {
liveItems = liveItems . filter ( function ( item ) {
return normalizeSmartSearchItemType ( item ) === _smartSearchScope ;
} ) ;
}
preview . innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Detected: ' + escapeHtml ( info . label ) + '</div><div style="color:var(--text-light); line-height:1.5;">' + escapeHtml ( info . detail ) + '</div></div><div><div style="font-weight:700; margin-bottom:0.45rem;">Live suggestions</div>' + renderSmartSearchPreviewItems ( liveItems , query ) + '</div></div>' ;
} catch ( error ) {
if ( requestId !== _smartSearchPreviewRequestId ) return ;
if ( _smartSearchScope !== 'all' ) {
preview . innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Detected: ' + escapeHtml ( info . label ) + '</div><div style="color:var(--text-light); line-height:1.5;">' + escapeHtml ( info . detail ) + '</div></div><div style="color:var(--text-light);">Live suggestions unavailable for the selected scope. Press Enter to search directly.</div></div>' ;
return ;
}
preview . innerHTML = '<div style="display:grid; gap:1rem;"><div><div style="font-weight:700; margin-bottom:0.45rem;">Detected: ' + escapeHtml ( info . label ) + '</div><div style="color:var(--text-light); line-height:1.5;">' + escapeHtml ( info . detail ) + '</div></div><div style="color:var(--text-light);">Live suggestions unavailable. Press Enter to search directly.</div></div>' ;
}
} , 220 ) ;
}
function openSmartSearchModal ( prefill ) {
var modal = document . getElementById ( 'smartSearchModal' ) ;
var input = document . getElementById ( 'smartSearchInput' ) ;
if ( ! modal || ! input ) return ;
modal . style . display = 'block' ;
modal . setAttribute ( 'aria-hidden' , 'false' ) ;
document . body . style . overflow = 'hidden' ;
setSmartSearchScope ( _smartSearchScope || 'all' ) ;
if ( prefill != null ) {
input . value = prefill ;
}
updateSmartSearchPreview ( input . value || '' ) ;
setTimeout ( function ( ) {
input . focus ( ) ;
input . select ( ) ;
} , 0 ) ;
}
function closeSmartSearchModal ( ) {
var modal = document . getElementById ( 'smartSearchModal' ) ;
if ( ! modal ) return ;
modal . style . display = 'none' ;
modal . setAttribute ( 'aria-hidden' , 'true' ) ;
document . body . style . overflow = '' ;
}
window . openSmartSearchModal = openSmartSearchModal ;
window . closeSmartSearchModal = closeSmartSearchModal ;
2026-03-24 18:11:08 -07:00
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>' ;
2026-03-28 14:09:23 -07:00
filtered . forEach ( function ( addr ) { var label = getAddressLabel ( addr ) || '' ; html += '<tr><td>' + explorerAddressLink ( addr , escapeHtml ( shortenHash ( addr ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + escapeHtml ( label ) + '</td><td><button type="button" class="btn btn-secondary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="event.stopPropagation(); removeFromWatchlist(\'' + escapeHtml ( addr ) + '\'); if(window._renderWatchlist) window._renderWatchlist();">Remove</button></td></tr>' ; } ) ;
2026-03-24 18:11:08 -07:00
html += '</tbody></table>' ;
container . innerHTML = html ;
} ;
2026-02-16 03:09:53 -08:00
var KNOWN _ADDRESS _LABELS = { '0x89dd12025bfcd38a168455a44b400e913ed33be2' : 'CCIP WETH9 Bridge' , '0xe0e93247376aa097db308b92e6ba36ba015535d0' : 'CCIP WETH10 Bridge' , '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' : 'WETH9' , '0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f' : 'WETH10' , '0x8078a09637e47fa5ed34f626046ea2094a5cde5e' : 'CCIP Router' , '0x105f8a15b819948a89153505762444ee9f324684' : 'CCIP Sender' } ;
2026-03-27 12:02:36 -07:00
const CHAIN _138 _PMM _INTEGRATION _ADDRESS = '0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d' ;
const CHAIN _138 _PRIVATE _POOL _REGISTRY = '0xb27057B27db09e8Df353AF722c299f200519882A' ;
const CHAIN _138 _CUSDT _ADDRESS = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22' ;
const CHAIN _138 _CUSDC _ADDRESS = '0xf22258f57794CC8E06237084b353Ab30fFfa640b' ;
const CHAIN _138 _CEURT _ADDRESS = '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72' ;
const CHAIN _138 _CXAUC _ADDRESS = '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' ;
const CHAIN _138 _CXAUT _ADDRESS = '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' ;
const MAINNET _USDT _ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7' ;
const MAINNET _USDC _ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' ;
const RESERVE _SYSTEM _ADDRESS = '0x607e97cD626f209facfE48c1464815DDE15B5093' ;
const RESERVE _TOKEN _INTEGRATION _ADDRESS = '0x34B73e6EDFd9f85a7c25EeD31dcB13aB6E969b96' ;
const BRIDGE _VAULT _ADDRESS = '0x31884f84555210FFB36a19D2471b8eBc7372d0A8' ;
const CHAIN _138 _POOL _BALANCE _DECIMALS = 6 ;
const SELECTOR _POOLS = '0x901754d7' ;
const SELECTOR _GET _PRIVATE _POOL = '0xc427540d' ;
const SELECTOR _BALANCE _OF = '0x70a08231' ;
const SELECTOR _SYMBOL = '0x95d89b41' ;
const SELECTOR _OFFICIAL _USDT = '0xe015a3b8' ;
const SELECTOR _OFFICIAL _USDC = '0xc82ab874' ;
const SELECTOR _COMPLIANT _USDT = '0x15fdffdf' ;
const SELECTOR _COMPLIANT _USDC = '0x59916868' ;
const ROUTE _TREE _REFRESH _MS = 120000 ;
function buildRoutePriorityQueries ( ctx ) {
var officialUsdt = ( ctx && ctx . officialUSDT ) || '' ;
var officialUsdc = ( ctx && ctx . officialUSDC ) || '' ;
var queries = [
{
key : 'local-cusdt-cusdc' ,
title : 'Local direct: cUSDT / cUSDC' ,
tokenIn : CHAIN _138 _CUSDT _ADDRESS ,
tokenOut : CHAIN _138 _CUSDC _ADDRESS ,
destinationChainId : 138 ,
amountIn : '1000000' ,
} ,
{
key : 'bridge-cusdt-usdt' ,
title : 'Mainnet bridge path: cUSDT -> USDT' ,
tokenIn : CHAIN _138 _CUSDT _ADDRESS ,
tokenOut : MAINNET _USDT _ADDRESS ,
destinationChainId : 1 ,
amountIn : '1000000' ,
} ,
{
key : 'bridge-cusdc-usdc' ,
title : 'Mainnet bridge path: cUSDC -> USDC' ,
tokenIn : CHAIN _138 _CUSDC _ADDRESS ,
tokenOut : MAINNET _USDC _ADDRESS ,
destinationChainId : 1 ,
amountIn : '1000000' ,
}
] ;
if ( safeAddress ( officialUsdt ) ) {
queries . unshift ( {
key : 'local-cusdt-usdt' ,
title : 'Local direct: cUSDT / USDT (official mirror, Chain 138)' ,
tokenIn : CHAIN _138 _CUSDT _ADDRESS ,
tokenOut : officialUsdt ,
destinationChainId : 138 ,
amountIn : '1000000' ,
} ) ;
}
if ( safeAddress ( officialUsdc ) ) {
queries . splice ( 2 , 0 , {
key : 'local-cusdc-usdc' ,
title : 'Local direct: cUSDC / USDC (official mirror, Chain 138)' ,
tokenIn : CHAIN _138 _CUSDC _ADDRESS ,
tokenOut : officialUsdc ,
destinationChainId : 138 ,
amountIn : '1000000' ,
} ) ;
}
return queries ;
}
const CHAIN _138 _ROUTE _SWEEP _TOKENS = [
{ symbol : 'cUSDT' , address : '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22' } ,
{ symbol : 'cUSDC' , address : '0xf22258f57794CC8E06237084b353Ab30fFfa640b' } ,
{ symbol : 'cEURC' , address : '0x8085961F9cF02b4d800A3c6d386D31da4B34266a' } ,
{ symbol : 'cEURT' , address : '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72' } ,
{ symbol : 'cGBPC' , address : '0x003960f16D9d34F2e98d62723B6721Fb92074aD2' } ,
{ symbol : 'cGBPT' , address : '0x350f54e4D23795f86A9c03988c7135357CCaD97c' } ,
{ symbol : 'cAUDC' , address : '0xD51482e567c03899eecE3CAe8a058161FD56069D' } ,
{ symbol : 'cJPYC' , address : '0xEe269e1226a334182aace90056EE4ee5Cc8A6770' } ,
{ symbol : 'cCHFC' , address : '0x873990849DDa5117d7C644f0aF24370797C03885' } ,
{ symbol : 'cCADC' , address : '0x54dBd40cF05e15906A2C21f600937e96787f5679' } ,
{ symbol : 'cXAUC' , address : '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' } ,
{ symbol : 'cXAUT' , address : '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' }
2026-03-24 18:11:08 -07:00
] ;
2026-03-27 18:52:03 -07:00
function buildRouteSweepQueries ( ctx ) {
var officialUsdt = ( ctx && ctx . officialUSDT ) || '' ;
var officialUsdc = ( ctx && ctx . officialUSDC ) || '' ;
var queries = [ ] ;
CHAIN _138 _ROUTE _SWEEP _TOKENS . forEach ( function ( token ) {
var anchors = [ ] ;
if ( token . symbol === 'cUSDT' ) {
anchors . push ( { symbol : 'cUSDC' , address : CHAIN _138 _CUSDC _ADDRESS } ) ;
if ( safeAddress ( officialUsdt ) ) anchors . push ( { symbol : 'USDT' , address : officialUsdt } ) ;
} else if ( token . symbol === 'cUSDC' ) {
anchors . push ( { symbol : 'cUSDT' , address : CHAIN _138 _CUSDT _ADDRESS } ) ;
if ( safeAddress ( officialUsdc ) ) anchors . push ( { symbol : 'USDC' , address : officialUsdc } ) ;
} else {
anchors . push ( { symbol : 'cUSDT' , address : CHAIN _138 _CUSDT _ADDRESS } ) ;
anchors . push ( { symbol : 'cUSDC' , address : CHAIN _138 _CUSDC _ADDRESS } ) ;
}
anchors . forEach ( function ( anchor ) {
if ( ! safeAddress ( anchor . address ) ) return ;
if ( String ( anchor . address ) . toLowerCase ( ) === String ( token . address ) . toLowerCase ( ) ) return ;
queries . push ( {
key : token . symbol . toLowerCase ( ) + '-' + anchor . symbol . toLowerCase ( ) ,
title : token . symbol + ' / ' + anchor . symbol + ' coverage probe' ,
symbol : token . symbol ,
pairLabel : token . symbol + ' / ' + anchor . symbol ,
tokenIn : token . address ,
tokenOut : anchor . address ,
destinationChainId : 138 ,
amountIn : '1000000'
} ) ;
} ) ;
} ) ;
return queries ;
}
2026-03-27 12:02:36 -07:00
function stripHexPrefix ( value ) {
return String ( value || '' ) . replace ( /^0x/i , '' ) ;
}
function encodeAddressWord ( address ) {
return stripHexPrefix ( address ) . toLowerCase ( ) . padStart ( 64 , '0' ) ;
}
function decodeAddressWord ( data ) {
var hex = stripHexPrefix ( data ) ;
if ( ! hex || hex . length < 64 ) return '' ;
return '0x' + hex . slice ( hex . length - 40 ) ;
}
function decodeUint256Word ( data , offsetWords ) {
var hex = stripHexPrefix ( data ) ;
var start = ( offsetWords || 0 ) * 64 ;
var word = hex . slice ( start , start + 64 ) ;
if ( ! word ) return 0 n ;
try { return BigInt ( '0x' + word ) ; } catch ( e ) { return 0 n ; }
}
async function rpcEthCall ( to , data ) {
return rpcCall ( 'eth_call' , [ { to : to , data : data } , 'latest' ] ) ;
}
async function rpcCodeExists ( address ) {
if ( ! safeAddress ( address ) ) return false ;
try {
var code = await rpcCall ( 'eth_getCode' , [ address , 'latest' ] ) ;
return ! ! code && code !== '0x' ;
} catch ( e ) {
return false ;
}
}
async function rpcReadAddress ( to , selector ) {
if ( ! safeAddress ( to ) ) return '' ;
try {
var out = await rpcEthCall ( to , selector ) ;
var addr = decodeAddressWord ( out ) ;
return safeAddress ( addr ) ? addr : '' ;
} catch ( e ) {
return '' ;
}
}
async function rpcReadMappedPool ( integration , tokenA , tokenB ) {
if ( ! safeAddress ( integration ) || ! safeAddress ( tokenA ) || ! safeAddress ( tokenB ) ) return '' ;
try {
var out = await rpcEthCall ( integration , SELECTOR _POOLS + encodeAddressWord ( tokenA ) + encodeAddressWord ( tokenB ) ) ;
var addr = decodeAddressWord ( out ) ;
return safeAddress ( addr ) ? addr : '' ;
} catch ( e ) {
return '' ;
}
}
async function rpcReadPrivatePool ( registry , tokenA , tokenB ) {
if ( ! safeAddress ( registry ) || ! safeAddress ( tokenA ) || ! safeAddress ( tokenB ) ) return '' ;
try {
var out = await rpcEthCall ( registry , SELECTOR _GET _PRIVATE _POOL + encodeAddressWord ( tokenA ) + encodeAddressWord ( tokenB ) ) ;
var addr = decodeAddressWord ( out ) ;
return safeAddress ( addr ) ? addr : '' ;
} catch ( e ) {
return '' ;
}
}
async function rpcReadBalanceOf ( token , account ) {
if ( ! safeAddress ( token ) || ! safeAddress ( account ) ) return 0 n ;
try {
var out = await rpcEthCall ( token , SELECTOR _BALANCE _OF + encodeAddressWord ( account ) ) ;
return decodeUint256Word ( out , 0 ) ;
} catch ( e ) {
return 0 n ;
}
}
async function rpcReadSymbol ( token ) {
if ( ! safeAddress ( token ) ) return '' ;
try {
var out = await rpcEthCall ( token , SELECTOR _SYMBOL ) ;
var hex = stripHexPrefix ( out ) ;
if ( ! hex || hex . length < 128 ) return '' ;
var len = Number ( BigInt ( '0x' + hex . slice ( 64 , 128 ) ) ) ;
if ( ! len || ! Number . isFinite ( len ) ) return '' ;
var dataHex = hex . slice ( 128 , 128 + ( len * 2 ) ) ;
var bytes = [ ] ;
for ( var i = 0 ; i < dataHex . length ; i += 2 ) {
bytes . push ( parseInt ( dataHex . slice ( i , i + 2 ) , 16 ) ) ;
}
return new TextDecoder ( ) . decode ( new Uint8Array ( bytes ) ) . replace ( /\0+$/ , '' ) ;
} catch ( e ) {
return '' ;
}
}
function formatTokenUnits ( value , decimals , precision ) {
var amount = typeof value === 'bigint' ? value : BigInt ( value || 0 ) ;
var scale = BigInt ( Math . pow ( 10 , Number ( decimals || 0 ) ) ) ;
if ( scale === 0 n ) return '0' ;
var whole = amount / scale ;
var fraction = amount % scale ;
if ( fraction === 0 n ) return whole . toString ( ) ;
var fracStr = fraction . toString ( ) . padStart ( Number ( decimals || 0 ) , '0' ) ;
var trimmed = fracStr . slice ( 0 , Math . max ( 0 , Number ( precision == null ? 3 : precision ) ) ) . replace ( /0+$/ , '' ) ;
return trimmed ? ( whole . toString ( ) + '.' + trimmed ) : whole . toString ( ) ;
}
async function fetchCurrentPmmContext ( ) {
var integration = CHAIN _138 _PMM _INTEGRATION _ADDRESS ;
var officialUSDT = await rpcReadAddress ( integration , SELECTOR _OFFICIAL _USDT ) ;
var officialUSDC = await rpcReadAddress ( integration , SELECTOR _OFFICIAL _USDC ) ;
var compliantUSDT = await rpcReadAddress ( integration , SELECTOR _COMPLIANT _USDT ) ;
var compliantUSDC = await rpcReadAddress ( integration , SELECTOR _COMPLIANT _USDC ) ;
return {
integration : integration ,
privateRegistry : CHAIN _138 _PRIVATE _POOL _REGISTRY ,
officialUSDT : officialUSDT ,
officialUSDC : officialUSDC ,
compliantUSDT : compliantUSDT || CHAIN _138 _CUSDT _ADDRESS ,
compliantUSDC : compliantUSDC || CHAIN _138 _CUSDC _ADDRESS
} ;
}
function inferKnownTokenSymbol ( address , ctx ) {
var lower = String ( address || '' ) . toLowerCase ( ) ;
if ( lower === String ( ( ctx && ctx . compliantUSDT ) || CHAIN _138 _CUSDT _ADDRESS ) . toLowerCase ( ) ) return 'cUSDT' ;
if ( lower === String ( ( ctx && ctx . compliantUSDC ) || CHAIN _138 _CUSDC _ADDRESS ) . toLowerCase ( ) ) return 'cUSDC' ;
if ( lower === String ( ( ctx && ctx . officialUSDT ) || '' ) . toLowerCase ( ) ) return 'USDT' ;
if ( lower === String ( ( ctx && ctx . officialUSDC ) || '' ) . toLowerCase ( ) ) return 'USDC' ;
var matched = CHAIN _138 _ROUTE _SWEEP _TOKENS . find ( function ( token ) { return String ( token . address ) . toLowerCase ( ) === lower ; } ) ;
return matched ? matched . symbol : '' ;
}
async function buildLiveDirectRouteFallback ( query , ctx ) {
if ( ! query || Number ( query . destinationChainId || query . chainId || 138 ) !== 138 || ! safeAddress ( query . tokenOut ) ) return null ;
var poolAddress = await rpcReadMappedPool ( ( ctx && ctx . integration ) || CHAIN _138 _PMM _INTEGRATION _ADDRESS , query . tokenIn , query . tokenOut ) ;
if ( ! safeAddress ( poolAddress ) || ! ( await rpcCodeExists ( poolAddress ) ) ) return null ;
var reserveIn = await rpcReadBalanceOf ( query . tokenIn , poolAddress ) ;
var reserveOut = await rpcReadBalanceOf ( query . tokenOut , poolAddress ) ;
var funded = reserveIn > 0 n && reserveOut > 0 n ;
var partial = ( reserveIn > 0 n || reserveOut > 0 n ) && ! funded ;
var status = funded ? 'live' : ( partial ? 'partial' : 'unavailable' ) ;
var tokenInSymbol = inferKnownTokenSymbol ( query . tokenIn , ctx ) || await rpcReadSymbol ( query . tokenIn ) || shortenHash ( query . tokenIn ) ;
var tokenOutSymbol = inferKnownTokenSymbol ( query . tokenOut , ctx ) || await rpcReadSymbol ( query . tokenOut ) || shortenHash ( query . tokenOut ) ;
var reserveInFormatted = formatTokenUnits ( reserveIn , CHAIN _138 _POOL _BALANCE _DECIMALS , 3 ) ;
var reserveOutFormatted = formatTokenUnits ( reserveOut , CHAIN _138 _POOL _BALANCE _DECIMALS , 3 ) ;
return {
generatedAt : new Date ( ) . toISOString ( ) ,
source : {
chainId : 138 ,
chainName : 'DeFi Oracle Meta Mainnet' ,
tokenIn : {
address : query . tokenIn ,
symbol : tokenInSymbol ,
name : tokenInSymbol ,
decimals : CHAIN _138 _POOL _BALANCE _DECIMALS ,
source : 'live-rpc'
}
} ,
destination : {
chainId : 138 ,
chainName : 'DeFi Oracle Meta Mainnet'
} ,
decision : 'direct-pool' ,
tree : [ {
id : 'live-direct:' + String ( poolAddress ) . toLowerCase ( ) ,
kind : 'direct-pool' ,
label : tokenInSymbol + '/' + tokenOutSymbol ,
chainId : 138 ,
chainName : 'DeFi Oracle Meta Mainnet' ,
status : status ,
depth : {
tvlUsd : 0 ,
reserve0 : reserveInFormatted ,
reserve1 : reserveOutFormatted ,
estimatedTradeCapacityUsd : 0 ,
freshnessSeconds : 0 ,
status : funded ? 'live' : ( partial ? 'stale' : 'unavailable' )
} ,
tokenIn : {
address : query . tokenIn ,
symbol : tokenInSymbol ,
name : tokenInSymbol ,
decimals : CHAIN _138 _POOL _BALANCE _DECIMALS ,
source : 'live-rpc'
} ,
tokenOut : {
address : query . tokenOut ,
symbol : tokenOutSymbol ,
name : tokenOutSymbol ,
decimals : CHAIN _138 _POOL _BALANCE _DECIMALS ,
source : 'live-rpc'
} ,
poolAddress : poolAddress ,
dexType : 'DODO PMM' ,
path : [ query . tokenIn , query . tokenOut ] ,
notes : [
'Live fallback from on-chain DODOPMMIntegration mapping' ,
'Base reserve ' + reserveInFormatted ,
'Quote reserve ' + reserveOutFormatted
]
} ] ,
pools : [ {
poolAddress : poolAddress ,
dexType : 'DODO PMM' ,
token0 : {
address : query . tokenIn ,
symbol : tokenInSymbol ,
name : tokenInSymbol ,
decimals : CHAIN _138 _POOL _BALANCE _DECIMALS ,
source : 'live-rpc'
} ,
token1 : {
address : query . tokenOut ,
symbol : tokenOutSymbol ,
name : tokenOutSymbol ,
decimals : CHAIN _138 _POOL _BALANCE _DECIMALS ,
source : 'live-rpc'
} ,
depth : {
tvlUsd : 0 ,
reserve0 : reserveInFormatted ,
reserve1 : reserveOutFormatted ,
estimatedTradeCapacityUsd : 0 ,
freshnessSeconds : 0 ,
status : funded ? 'live' : ( partial ? 'stale' : 'unavailable' )
}
} ] ,
missingQuoteTokenPools : [ ]
} ;
}
async function discoverPublicXauPool ( baseToken , anchors , integration ) {
for ( var i = 0 ; i < anchors . length ; i ++ ) {
var anchor = anchors [ i ] ;
var pool = await rpcReadMappedPool ( integration , baseToken , anchor . address ) ;
if ( safeAddress ( pool ) ) {
return { pool : pool , anchor : anchor } ;
}
}
return { pool : '' , anchor : anchors [ 0 ] } ;
}
async function discoverPrivateXauPool ( baseToken , anchors , registry ) {
for ( var i = 0 ; i < anchors . length ; i ++ ) {
var anchor = anchors [ i ] ;
var pool = await rpcReadPrivatePool ( registry , baseToken , anchor . address ) ;
if ( safeAddress ( pool ) ) {
return { pool : pool , anchor : anchor } ;
}
}
return { pool : '' , anchor : anchors [ 0 ] } ;
}
async function getLivePoolRows ( ) {
var ctx = await fetchCurrentPmmContext ( ) ;
var xauAnchors = [
{ symbol : 'cXAUC' , address : CHAIN _138 _CXAUC _ADDRESS } ,
{ symbol : 'cXAUT' , address : CHAIN _138 _CXAUT _ADDRESS }
] ;
var rows = [ ] ;
async function buildLivePoolRow ( category , poolPair , poolType , poolAddress , baseToken , quoteToken , notesPrefix ) {
var codeExists = await rpcCodeExists ( poolAddress ) ;
var baseBal = codeExists ? await rpcReadBalanceOf ( baseToken , poolAddress ) : 0 n ;
var quoteBal = codeExists ? await rpcReadBalanceOf ( quoteToken , poolAddress ) : 0 n ;
var funded = baseBal > 0 n && quoteBal > 0 n ;
var partial = ( baseBal > 0 n || quoteBal > 0 n ) && ! funded ;
var status = ! safeAddress ( poolAddress ) ? 'Not created' : ( funded ? 'Funded (live)' : ( partial ? 'Partially funded' : ( codeExists ? 'Created (unfunded)' : 'Missing code' ) ) ) ;
var notes = [ ] ;
if ( notesPrefix ) notes . push ( notesPrefix ) ;
if ( safeAddress ( poolAddress ) && codeExists ) {
notes . push ( 'Base reserve: ' + formatTokenUnits ( baseBal , CHAIN _138 _POOL _BALANCE _DECIMALS , 3 ) ) ;
notes . push ( 'Quote reserve: ' + formatTokenUnits ( quoteBal , CHAIN _138 _POOL _BALANCE _DECIMALS , 3 ) ) ;
}
return { category : category , poolPair : poolPair , poolType : poolType , address : poolAddress || '' , status : status , notes : notes . join ( ' | ' ) } ;
}
rows . push ( await buildLivePoolRow ( 'Public Liquidity Pools' , 'cUSDT / cUSDC' , 'DODO PMM' ,
await rpcReadMappedPool ( ctx . integration , ctx . compliantUSDT , ctx . compliantUSDC ) ,
ctx . compliantUSDT , ctx . compliantUSDC , 'Derived from live DODOPMMIntegration state' ) ) ;
rows . push ( await buildLivePoolRow ( 'Public Liquidity Pools' , 'cUSDT / USDT (official mirror)' , 'DODO PMM' ,
await rpcReadMappedPool ( ctx . integration , ctx . compliantUSDT , ctx . officialUSDT ) ,
ctx . compliantUSDT , ctx . officialUSDT , ( await rpcCodeExists ( ctx . officialUSDT ) ) ? ( 'Live quote token ' + shortenHash ( ctx . officialUSDT ) ) : 'Quote-side USDT contract missing on Chain 138' ) ) ;
rows . push ( await buildLivePoolRow ( 'Public Liquidity Pools' , 'cUSDC / USDC (official mirror)' , 'DODO PMM' ,
await rpcReadMappedPool ( ctx . integration , ctx . compliantUSDC , ctx . officialUSDC ) ,
ctx . compliantUSDC , ctx . officialUSDC , ( await rpcCodeExists ( ctx . officialUSDC ) ) ? ( 'Live quote token ' + shortenHash ( ctx . officialUSDC ) ) : 'Quote-side USDC contract missing on Chain 138' ) ) ;
var publicCusdtXau = await discoverPublicXauPool ( ctx . compliantUSDT , xauAnchors , ctx . integration ) ;
rows . push ( await buildLivePoolRow ( 'Public Liquidity Pools' , 'cUSDT / XAU (' + publicCusdtXau . anchor . symbol + ')' , 'DODO PMM' ,
publicCusdtXau . pool , ctx . compliantUSDT , publicCusdtXau . anchor . address ,
safeAddress ( publicCusdtXau . pool ) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered' ) ) ;
var publicCusdcXau = await discoverPublicXauPool ( ctx . compliantUSDC , xauAnchors , ctx . integration ) ;
rows . push ( await buildLivePoolRow ( 'Public Liquidity Pools' , 'cUSDC / XAU (' + publicCusdcXau . anchor . symbol + ')' , 'DODO PMM' ,
publicCusdcXau . pool , ctx . compliantUSDC , publicCusdcXau . anchor . address ,
safeAddress ( publicCusdcXau . pool ) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered' ) ) ;
var publicCeurtXau = await discoverPublicXauPool ( CHAIN _138 _CEURT _ADDRESS , xauAnchors , ctx . integration ) ;
rows . push ( await buildLivePoolRow ( 'Public Liquidity Pools' , 'cEURT / XAU (' + publicCeurtXau . anchor . symbol + ')' , 'DODO PMM' ,
publicCeurtXau . pool , CHAIN _138 _CEURT _ADDRESS , publicCeurtXau . anchor . address ,
safeAddress ( publicCeurtXau . pool ) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered' ) ) ;
var privateCusdtXau = await discoverPrivateXauPool ( ctx . compliantUSDT , xauAnchors , ctx . privateRegistry ) ;
rows . push ( await buildLivePoolRow ( 'Private Stabilization Pools' , 'cUSDT ↔ XAU (' + privateCusdtXau . anchor . symbol + ')' , 'PrivatePoolRegistry' ,
privateCusdtXau . pool , ctx . compliantUSDT , privateCusdtXau . anchor . address ,
safeAddress ( privateCusdtXau . pool ) ? ( 'Registered in live PrivatePoolRegistry ' + shortenHash ( ctx . privateRegistry ) ) : 'No live private XAU pool currently registered' ) ) ;
var privateCusdcXau = await discoverPrivateXauPool ( ctx . compliantUSDC , xauAnchors , ctx . privateRegistry ) ;
rows . push ( await buildLivePoolRow ( 'Private Stabilization Pools' , 'cUSDC ↔ XAU (' + privateCusdcXau . anchor . symbol + ')' , 'PrivatePoolRegistry' ,
privateCusdcXau . pool , ctx . compliantUSDC , privateCusdcXau . anchor . address ,
safeAddress ( privateCusdcXau . pool ) ? ( 'Registered in live PrivatePoolRegistry ' + shortenHash ( ctx . privateRegistry ) ) : 'No live private XAU pool currently registered' ) ) ;
var privateCeurtXau = await discoverPrivateXauPool ( CHAIN _138 _CEURT _ADDRESS , xauAnchors , ctx . privateRegistry ) ;
rows . push ( await buildLivePoolRow ( 'Private Stabilization Pools' , 'cEURT ↔ XAU (' + privateCeurtXau . anchor . symbol + ')' , 'PrivatePoolRegistry' ,
privateCeurtXau . pool , CHAIN _138 _CEURT _ADDRESS , privateCeurtXau . anchor . address ,
safeAddress ( privateCeurtXau . pool ) ? ( 'Registered in live PrivatePoolRegistry ' + shortenHash ( ctx . privateRegistry ) ) : 'No live private XAU pool currently registered' ) ) ;
rows . push ( {
category : 'Reserve Pools / Vault Backing' ,
poolPair : 'ReserveSystem' ,
poolType : 'Reserve' ,
address : RESERVE _SYSTEM _ADDRESS ,
status : ( await rpcCodeExists ( RESERVE _SYSTEM _ADDRESS ) ) ? 'Deployed (live)' : 'Missing code' ,
notes : 'Live code check against Chain 138'
} ) ;
rows . push ( {
category : 'Reserve Pools / Vault Backing' ,
poolPair : 'ReserveTokenIntegration' ,
poolType : 'Reserve' ,
address : RESERVE _TOKEN _INTEGRATION _ADDRESS ,
status : ( await rpcCodeExists ( RESERVE _TOKEN _INTEGRATION _ADDRESS ) ) ? 'Deployed (live)' : 'Missing code' ,
notes : 'Live code check against Chain 138'
} ) ;
rows . push ( {
category : 'Reserve Pools / Vault Backing' ,
poolPair : 'StablecoinReserveVault' ,
poolType : 'Reserve' ,
address : '' ,
status : 'External / Mainnet-side' ,
notes : 'Reserve vault lives on Mainnet by design; no local Chain 138 contract is expected here'
} ) ;
rows . push ( {
category : 'Reserve Pools / Vault Backing' ,
poolPair : 'Bridge_Vault' ,
poolType : 'Vault' ,
address : BRIDGE _VAULT _ADDRESS ,
status : ( await rpcCodeExists ( BRIDGE _VAULT _ADDRESS ) ) ? 'Deployed (live)' : 'Missing code' ,
notes : 'Live code check against Chain 138'
} ) ;
rows . push ( {
category : 'Bridge Liquidity Pool' ,
poolPair : 'LiquidityPoolETH' ,
poolType : 'Bridge LP' ,
address : '' ,
status : 'External / Mainnet bridge LP' ,
notes : 'Trustless bridge liquidity pool is a Mainnet-side contract, not a local Chain 138 pool'
} ) ;
return { rows : rows , context : ctx } ;
}
function getActiveBridgeContractCount ( ) {
return new Set ( [
'0x971cD9D156f193df8051E48043C476e53ECd4693' ,
'0xe0E93247376aa097dB308B92e6Ba36bA015535D0' ,
'0x2A0840e5117683b11682ac46f5CF5621E67269E3' ,
'0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03' ,
'0x8078a09637e47fa5ed34f626046ea2094a5cde5e' ,
'0xa780ef19a041745d353c9432f2a7f5a241335ffe' ,
'0x105f8a15b819948a89153505762444ee9f324684' ,
'0xdab0591e5e89295ffad75a71dcfc30c5625c4fa2'
] . map ( function ( addr ) { return String ( addr || '' ) . toLowerCase ( ) ; } ) ) . size ;
}
2026-02-16 03:09:53 -08:00
function getAddressLabel ( addr ) { if ( ! addr ) return '' ; var lower = addr . toLowerCase ( ) ; if ( KNOWN _ADDRESS _LABELS [ lower ] ) return KNOWN _ADDRESS _LABELS [ lower ] ; try { var j = localStorage . getItem ( 'explorerAddressLabels' ) ; if ( ! j ) return '' ; var m = JSON . parse ( j ) ; return m [ lower ] || '' ; } catch ( e ) { return '' ; } }
function formatAddressWithLabel ( addr ) { if ( ! addr ) return '' ; var label = getAddressLabel ( addr ) ; return label ? escapeHtml ( label ) + ' (' + escapeHtml ( shortenHash ( addr ) ) + ')' : escapeHtml ( shortenHash ( addr ) ) ; }
function copyToClipboard ( val , msg ) { if ( ! val ) return ; try { navigator . clipboard . writeText ( String ( val ) ) ; showToast ( msg || 'Copied' , 'success' ) ; } catch ( e ) { showToast ( 'Copy failed' , 'error' ) ; } }
function setAddressLabel ( addr , label ) { try { var j = localStorage . getItem ( 'explorerAddressLabels' ) || '{}' ; var m = JSON . parse ( j ) ; m [ addr . toLowerCase ( ) ] = ( label || '' ) . trim ( ) ; localStorage . setItem ( 'explorerAddressLabels' , JSON . stringify ( m ) ) ; return true ; } catch ( e ) { return false ; } }
function getWatchlist ( ) { try { var j = localStorage . getItem ( 'explorerWatchlist' ) ; if ( ! j ) return [ ] ; var a = JSON . parse ( j ) ; return Array . isArray ( a ) ? a : [ ] ; } catch ( e ) { return [ ] ; } }
function addToWatchlist ( addr ) { if ( ! addr || ! /^0x[a-fA-F0-9]{40}$/i . test ( addr ) ) return false ; var a = getWatchlist ( ) ; var lower = addr . toLowerCase ( ) ; if ( a . indexOf ( lower ) === - 1 ) { a . push ( lower ) ; try { localStorage . setItem ( 'explorerWatchlist' , JSON . stringify ( a ) ) ; return true ; } catch ( e ) { } } return false ; }
function removeFromWatchlist ( addr ) { var a = getWatchlist ( ) . filter ( function ( x ) { return x !== addr . toLowerCase ( ) ; } ) ; try { localStorage . setItem ( 'explorerWatchlist' , JSON . stringify ( a ) ) ; return true ; } catch ( e ) { return false ; } }
function isInWatchlist ( addr ) { return getWatchlist ( ) . indexOf ( ( addr || '' ) . toLowerCase ( ) ) !== - 1 ; }
let currentView = 'home' ;
2026-03-27 12:02:36 -07:00
let _poolsRouteTreeRefreshTimer = null ;
2026-02-16 03:09:53 -08:00
let currentDetailKey = '' ;
2026-03-27 12:02:36 -07:00
let latestPoolsSnapshot = null ;
2026-02-16 03:09:53 -08:00
var _inNavHandler = false ; // re-entrancy guard: prevents hashchange -> applyHashRoute -> stub from recursing
let provider = null ;
let signer = null ;
let userAddress = null ;
// Tiered Architecture: Track and Authentication
let userTrack = 1 ; // Default to Track 1 (public)
let authToken = null ;
// View switch helper: works even if rest of script fails. Do NOT set location.hash here (we use path-based URLs).
function switchToView ( viewName ) {
2026-03-28 15:04:42 -07:00
if ( viewName !== 'pools' && viewName !== 'routes' && _poolsRouteTreeRefreshTimer ) {
2026-03-27 12:02:36 -07:00
clearInterval ( _poolsRouteTreeRefreshTimer ) ;
_poolsRouteTreeRefreshTimer = null ;
}
2026-02-16 03:09:53 -08:00
if ( viewName !== 'blocks' && _blocksScrollAnimationId != null ) {
cancelAnimationFrame ( _blocksScrollAnimationId ) ;
_blocksScrollAnimationId = null ;
}
currentView = viewName ;
2026-03-28 15:04:42 -07:00
var detailViews = [ 'blockDetail' , 'transactionDetail' , 'addressDetail' , 'tokenDetail' , 'nftDetail' , 'watchlist' , 'searchResults' , 'tokens' , 'addresses' , 'pools' , 'routes' , 'liquidity' , 'more' ] ;
2026-02-16 03:09:53 -08:00
if ( detailViews . indexOf ( viewName ) === - 1 ) currentDetailKey = '' ;
var homeEl = document . getElementById ( 'homeView' ) ;
if ( homeEl ) homeEl . style . display = viewName === 'home' ? 'block' : 'none' ;
document . querySelectorAll ( '.detail-view' ) . forEach ( function ( el ) { el . classList . remove ( 'active' ) ; } ) ;
var target = document . getElementById ( viewName + 'View' ) ;
if ( target ) target . classList . add ( 'active' ) ;
}
2026-03-27 12:02:36 -07:00
// Compatibility wrappers for nav buttons; re-entrancy guard prevents hashchange recursion.
2026-02-16 03:09:53 -08:00
window . showHome = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'home' ) ; if ( window . _showHome ) window . _showHome ( ) ; } finally { _inNavHandler = false ; } } ;
window . showBlocks = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'blocks' ) ; if ( window . _showBlocks ) window . _showBlocks ( ) ; } finally { _inNavHandler = false ; } } ;
window . showTransactions = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'transactions' ) ; if ( window . _showTransactions ) window . _showTransactions ( ) ; } finally { _inNavHandler = false ; } } ;
2026-03-27 12:02:36 -07:00
window . showAddresses = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'addresses' ) ; if ( window . _showAddresses ) window . _showAddresses ( ) ; } finally { _inNavHandler = false ; } } ;
2026-02-16 03:09:53 -08:00
window . showBridgeMonitoring = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'bridge' ) ; if ( window . _showBridgeMonitoring ) window . _showBridgeMonitoring ( ) ; } finally { _inNavHandler = false ; } } ;
window . showWETHUtilities = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'weth' ) ; if ( window . _showWETHUtilities ) window . _showWETHUtilities ( ) ; } finally { _inNavHandler = false ; } } ;
window . showWETHTab = function ( ) { } ;
window . showWatchlist = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'watchlist' ) ; if ( window . _renderWatchlist ) window . _renderWatchlist ( ) ; } finally { _inNavHandler = false ; } } ;
2026-03-27 12:02:36 -07:00
function openPoolsView ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'pools' ) ; if ( typeof renderPoolsView === 'function' ) renderPoolsView ( ) ; } finally { _inNavHandler = false ; } }
window . openPoolsView = openPoolsView ;
// Back-compat alias for older menu wiring; prefer openPoolsView() and renderPoolsView().
window . showPools = openPoolsView ;
2026-03-28 15:04:42 -07:00
window . showRoutes = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'routes' ) ; if ( typeof renderRoutesView === 'function' ) renderRoutesView ( ) ; } finally { _inNavHandler = false ; } } ;
2026-03-27 12:02:36 -07:00
window . showLiquidityAccess = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'liquidity' ) ; if ( typeof renderLiquidityAccessView === 'function' ) renderLiquidityAccessView ( ) ; } finally { _inNavHandler = false ; } } ;
2026-03-24 18:11:08 -07:00
window . showMore = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'more' ) ; if ( window . _showMore ) window . _showMore ( ) ; } finally { _inNavHandler = false ; } } ;
2026-02-16 03:09:53 -08:00
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 ; } } ;
2026-03-27 12:02:36 -07:00
// Compatibility wrappers for detail views; defer to the next tick to avoid synchronous recursion.
2026-03-02 12:14:13 -08:00
window . showBlockDetail = function ( n ) { if ( window . _showBlockDetail ) setTimeout ( function ( ) { window . _showBlockDetail ( n ) ; } , 0 ) ; } ;
window . showTransactionDetail = function ( h ) { if ( window . _showTransactionDetail ) setTimeout ( function ( ) { window . _showTransactionDetail ( h ) ; } , 0 ) ; } ;
window . showAddressDetail = function ( a ) { if ( window . _showAddressDetail ) setTimeout ( function ( ) { window . _showAddressDetail ( a ) ; } , 0 ) ; } ;
2026-02-16 03:09:53 -08:00
window . toggleDarkMode = function ( ) { document . body . classList . toggle ( 'dark-theme' ) ; var icon = document . getElementById ( 'themeIcon' ) ; if ( icon ) icon . className = document . body . classList . contains ( 'dark-theme' ) ? 'fas fa-sun' : 'fas fa-moon' ; try { localStorage . setItem ( 'explorerTheme' , document . body . classList . contains ( 'dark-theme' ) ? 'dark' : 'light' ) ; } catch ( e ) { } } ;
// Feature flags
const FEATURE _FLAGS = {
ADDRESS _FULL _DETAIL : { track : 2 } ,
TOKEN _BALANCES : { track : 2 } ,
TX _HISTORY : { track : 2 } ,
INTERNAL _TXS : { track : 2 } ,
ENHANCED _SEARCH : { track : 2 } ,
ANALYTICS _DASHBOARD : { track : 3 } ,
FLOW _TRACKING : { track : 3 } ,
BRIDGE _ANALYTICS : { track : 3 } ,
OPERATOR _PANEL : { track : 4 } ,
} ;
function hasAccess ( requiredTrack ) {
return userTrack >= requiredTrack ;
}
function isFeatureEnabled ( featureName ) {
const feature = FEATURE _FLAGS [ featureName ] ;
if ( ! feature ) return false ;
return hasAccess ( feature . track ) ;
}
// Load feature flags from API
async function loadFeatureFlags ( ) {
try {
2026-03-27 14:12:14 -07:00
const response = await fetch ( EXPLORER _API _V1 _BASE + '/features' , {
2026-02-16 03:09:53 -08:00
headers : authToken ? { 'Authorization' : ` Bearer ${ authToken } ` } : { }
} ) ;
if ( response . ok ) {
const data = await response . json ( ) ;
userTrack = data . track || 1 ;
updateUIForTrack ( ) ;
}
} catch ( error ) {
console . error ( 'Failed to load feature flags:' , error ) ;
}
}
function updateUIForTrack ( ) {
// Show/hide navigation items based on track
const analyticsNav = document . getElementById ( 'analyticsNav' ) ;
const operatorNav = document . getElementById ( 'operatorNav' ) ;
if ( analyticsNav ) analyticsNav . style . display = hasAccess ( 3 ) ? 'block' : 'none' ;
if ( operatorNav ) operatorNav . style . display = hasAccess ( 4 ) ? 'block' : 'none' ;
}
// Wallet authentication
async function connectWallet ( ) {
if ( typeof ethers === 'undefined' ) {
alert ( 'Ethers.js not loaded. Please refresh the page.' ) ;
return ;
}
try {
if ( ! window . ethereum ) {
alert ( 'MetaMask not detected. Please install MetaMask.' ) ;
return ;
}
const provider = new ethers . providers . Web3Provider ( window . ethereum ) ;
const accounts = await provider . send ( "eth_requestAccounts" , [ ] ) ;
const address = accounts [ 0 ] ;
// Request nonce
2026-03-27 14:12:14 -07:00
const nonceResp = await fetch ( EXPLORER _API _V1 _BASE + '/auth/nonce' , {
2026-02-16 03:09:53 -08:00
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { address } )
} ) ;
const nonceData = await nonceResp . json ( ) ;
// Sign message
const message = ` Sign this message to authenticate with SolaceScanScout Explorer. \n \n Nonce: ${ nonceData . nonce } ` ;
const signer = provider . getSigner ( ) ;
const signature = await signer . signMessage ( message ) ;
// Authenticate
2026-03-27 14:12:14 -07:00
const authResp = await fetch ( EXPLORER _API _V1 _BASE + '/auth/wallet' , {
2026-02-16 03:09:53 -08:00
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { address , signature , nonce : nonceData . nonce } )
} ) ;
if ( authResp . ok ) {
const authData = await authResp . json ( ) ;
authToken = authData . token ;
userTrack = authData . track ;
userAddress = address ;
localStorage . setItem ( 'authToken' , authToken ) ;
localStorage . setItem ( 'userAddress' , userAddress ) ;
updateUIForTrack ( ) ;
const walletBtn = document . getElementById ( 'walletConnectBtn' ) ;
const walletStatus = document . getElementById ( 'walletStatus' ) ;
const walletAddress = document . getElementById ( 'walletAddress' ) ;
if ( walletBtn ) walletBtn . style . display = 'none' ;
if ( walletStatus ) walletStatus . style . display = 'flex' ;
if ( walletAddress ) walletAddress . textContent = shortenHash ( address ) ;
await loadFeatureFlags ( ) ;
showToast ( 'Wallet connected successfully!' , 'success' ) ;
} else {
const errorData = await authResp . json ( ) ;
alert ( 'Authentication failed: ' + ( errorData . error ? . message || 'Unknown error' ) ) ;
}
} catch ( error ) {
2026-03-02 12:14:13 -08:00
var msg = ( error && error . message ) ? String ( error . message ) : '' ;
var friendly = ( error && error . code === 4001 ) || /not been authorized|rejected|denied/i . test ( msg )
? 'Connection was rejected. Please approve the MetaMask popup to connect.'
: ( 'Failed to connect wallet: ' + ( msg || 'Unknown error' ) ) ;
alert ( friendly ) ;
2026-02-16 03:09:53 -08:00
}
}
// Check for stored auth token on load
window . addEventListener ( 'DOMContentLoaded' , ( ) => {
const storedToken = localStorage . getItem ( 'authToken' ) ;
const storedAddress = localStorage . getItem ( 'userAddress' ) ;
if ( storedToken && storedAddress ) {
authToken = storedToken ;
userAddress = storedAddress ;
const walletBtn = document . getElementById ( 'walletConnectBtn' ) ;
const walletStatus = document . getElementById ( 'walletStatus' ) ;
const walletAddress = document . getElementById ( 'walletAddress' ) ;
if ( walletBtn ) walletBtn . style . display = 'none' ;
if ( walletStatus ) walletStatus . style . display = 'flex' ;
if ( walletAddress ) walletAddress . textContent = shortenHash ( storedAddress ) ;
loadFeatureFlags ( ) ;
} else {
const walletBtn = document . getElementById ( 'walletConnectBtn' ) ;
if ( walletBtn ) walletBtn . style . display = 'block' ;
}
} ) ;
// WETH Contract Addresses
const WETH9 _ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' ;
// WETH10 address - will be checksummed when ethers is loaded
const WETH10 _ADDRESS _RAW = '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f' ;
let WETH10 _ADDRESS = WETH10 _ADDRESS _RAW ; // Will be updated to checksummed version
// Data adapter functions to normalize Blockscout API responses
function normalizeBlock ( blockscoutBlock ) {
if ( ! blockscoutBlock ) return null ;
return {
number : blockscoutBlock . height || blockscoutBlock . number || parseInt ( blockscoutBlock . block _number , 10 ) ,
hash : blockscoutBlock . hash || blockscoutBlock . block _hash ,
parent _hash : blockscoutBlock . parent _hash || blockscoutBlock . parentHash ,
timestamp : blockscoutBlock . timestamp ,
miner : blockscoutBlock . miner ? . hash || blockscoutBlock . miner || blockscoutBlock . miner _hash ,
transaction _count : blockscoutBlock . transaction _count || blockscoutBlock . transactions _count || 0 ,
gas _used : blockscoutBlock . gas _used || '0' ,
gas _limit : blockscoutBlock . gas _limit || blockscoutBlock . gasLimit || '0' ,
size : blockscoutBlock . size || 0 ,
difficulty : blockscoutBlock . difficulty || '0' ,
base _fee _per _gas : blockscoutBlock . base _fee _per _gas ,
burnt _fees : blockscoutBlock . burnt _fees || '0' ,
total _difficulty : blockscoutBlock . total _difficulty || '0' ,
nonce : blockscoutBlock . nonce || '0x0' ,
extra _data : blockscoutBlock . extra _data || '0x'
} ;
}
function normalizeTransaction ( blockscoutTx ) {
if ( ! blockscoutTx ) return null ;
// Map status: "ok" -> 1, "error" -> 0, others -> 0
let status = 0 ;
if ( blockscoutTx . status === 'ok' || blockscoutTx . status === 'success' ) {
status = 1 ;
} else if ( blockscoutTx . status === 1 || blockscoutTx . status === '1' ) {
status = 1 ;
}
return {
hash : blockscoutTx . hash || blockscoutTx . tx _hash ,
from : blockscoutTx . from ? . hash || blockscoutTx . from || blockscoutTx . from _address _hash || blockscoutTx . from _address ,
to : blockscoutTx . to ? . hash || blockscoutTx . to || blockscoutTx . to _address _hash || blockscoutTx . to _address ,
value : blockscoutTx . value || '0' ,
block _number : blockscoutTx . block _number || blockscoutTx . block || null ,
block _hash : blockscoutTx . block _hash || blockscoutTx . blockHash || null ,
transaction _index : blockscoutTx . position || blockscoutTx . transaction _index || blockscoutTx . index || 0 ,
gas _price : blockscoutTx . gas _price || blockscoutTx . max _fee _per _gas || '0' ,
gas _used : blockscoutTx . gas _used || '0' ,
gas _limit : blockscoutTx . gas _limit || blockscoutTx . gas || '0' ,
nonce : blockscoutTx . nonce || '0' ,
status : status ,
created _at : blockscoutTx . timestamp || blockscoutTx . created _at || blockscoutTx . block _timestamp ,
input : blockscoutTx . input || blockscoutTx . raw _input || '0x' ,
max _fee _per _gas : blockscoutTx . max _fee _per _gas ,
max _priority _fee _per _gas : blockscoutTx . max _priority _fee _per _gas ,
priority _fee : blockscoutTx . priority _fee ,
tx _burnt _fee : blockscoutTx . tx _burnt _fee || blockscoutTx . burnt _fees || '0' ,
type : blockscoutTx . type || 0 ,
confirmations : blockscoutTx . confirmations || 0 ,
contract _address : blockscoutTx . created _contract _address _hash || ( blockscoutTx . contract _creation && ( blockscoutTx . to ? . hash || blockscoutTx . to _address _hash ) ) || null ,
revert _reason : blockscoutTx . revert _reason || blockscoutTx . error || blockscoutTx . result || null ,
decoded _input : blockscoutTx . decoded _input || null ,
method _id : blockscoutTx . method _id || null
} ;
}
function normalizeAddress ( blockscoutAddr ) {
if ( ! blockscoutAddr || typeof blockscoutAddr !== 'object' ) return null ;
var hash = blockscoutAddr . hash || blockscoutAddr . address || blockscoutAddr . address _hash ;
if ( ! hash && blockscoutAddr . creator _address _hash === undefined ) return null ;
var creationTx = blockscoutAddr . creation _tx _hash || blockscoutAddr . creator _tx _hash || blockscoutAddr . creation _transaction _hash || null ;
var firstSeen = blockscoutAddr . first _transaction _at || blockscoutAddr . first _seen _at || blockscoutAddr . first _tx _at || null ;
var lastSeen = blockscoutAddr . last _transaction _at || blockscoutAddr . last _seen _at || blockscoutAddr . last _tx _at || null ;
2026-03-28 13:44:47 -07:00
var txSent = blockscoutAddr . transactions _sent _count ;
if ( txSent == null ) txSent = blockscoutAddr . tx _sent _count ;
if ( txSent == null ) txSent = blockscoutAddr . transactions _count ;
var txReceived = blockscoutAddr . transactions _received _count ;
if ( txReceived == null ) txReceived = blockscoutAddr . tx _received _count ;
if ( txReceived == null ) txReceived = 0 ;
2026-02-16 03:09:53 -08:00
return {
2026-03-28 13:44:47 -07:00
address : hash || null ,
2026-02-16 03:09:53 -08:00
hash : hash || null ,
balance : blockscoutAddr . balance || blockscoutAddr . coin _balance || blockscoutAddr . coin _balance _value || '0' ,
transaction _count : blockscoutAddr . transactions _count != null ? blockscoutAddr . transactions _count : ( blockscoutAddr . transaction _count != null ? blockscoutAddr . transaction _count : ( blockscoutAddr . tx _count != null ? blockscoutAddr . tx _count : 0 ) ) ,
token _count : blockscoutAddr . token _count != null ? blockscoutAddr . token _count : 0 ,
is _contract : ! ! blockscoutAddr . is _contract ,
is _verified : ! ! blockscoutAddr . is _verified ,
2026-03-28 13:44:47 -07:00
tx _sent : txSent != null ? txSent : 0 ,
tx _received : txReceived != null ? txReceived : 0 ,
label : blockscoutAddr . name || blockscoutAddr . ens _domain _name || null ,
2026-02-16 03:09:53 -08:00
name : blockscoutAddr . name || null ,
ens _domain _name : blockscoutAddr . ens _domain _name || null ,
creation _tx _hash : creationTx ,
first _seen _at : firstSeen ,
last _seen _at : lastSeen
} ;
}
2026-03-28 13:59:00 -07:00
function hexToDecimalString ( value ) {
if ( value == null || value === '' ) return '0' ;
var stringValue = String ( value ) ;
if ( ! /^0x/i . test ( stringValue ) ) return stringValue ;
try {
return BigInt ( stringValue ) . toString ( ) ;
} catch ( e ) {
return '0' ;
}
}
function hexToNumber ( value ) {
if ( value == null || value === '' ) return 0 ;
if ( typeof value === 'number' ) return value ;
try {
return Number ( BigInt ( String ( value ) ) ) ;
} catch ( e ) {
return Number ( value ) || 0 ;
}
}
function rpcTimestampToIso ( value ) {
var timestampSeconds = hexToNumber ( value ) ;
if ( ! timestampSeconds ) return new Date ( 0 ) . toISOString ( ) ;
return new Date ( timestampSeconds * 1000 ) . toISOString ( ) ;
}
function normalizeRpcBlock ( block ) {
if ( ! block || ! block . hash ) return null ;
return normalizeBlock ( {
number : hexToDecimalString ( block . number ) ,
hash : block . hash ,
parent _hash : block . parentHash ,
timestamp : rpcTimestampToIso ( block . timestamp ) ,
miner : block . miner ,
transaction _count : Array . isArray ( block . transactions ) ? block . transactions . length : 0 ,
gas _used : hexToDecimalString ( block . gasUsed ) ,
gas _limit : hexToDecimalString ( block . gasLimit ) ,
size : hexToNumber ( block . size ) ,
difficulty : hexToDecimalString ( block . difficulty ) ,
base _fee _per _gas : block . baseFeePerGas ? hexToDecimalString ( block . baseFeePerGas ) : undefined ,
nonce : block . nonce || '0x0'
} ) ;
}
function normalizeRpcTransaction ( tx , receipt , block ) {
if ( ! tx || ! tx . hash ) return null ;
var blockTimestamp = block && block . timestamp ? rpcTimestampToIso ( block . timestamp ) : new Date ( 0 ) . toISOString ( ) ;
var txType = tx . type != null ? hexToNumber ( tx . type ) : 0 ;
var effectiveGasPrice = receipt && receipt . effectiveGasPrice ? receipt . effectiveGasPrice : tx . gasPrice ;
return normalizeTransaction ( {
hash : tx . hash ,
from : tx . from ,
to : tx . to ,
value : hexToDecimalString ( tx . value ) ,
block _number : tx . blockNumber ? hexToDecimalString ( tx . blockNumber ) : null ,
block _hash : tx . blockHash || null ,
transaction _index : tx . transactionIndex ? hexToNumber ( tx . transactionIndex ) : 0 ,
gas _price : effectiveGasPrice ? hexToDecimalString ( effectiveGasPrice ) : '0' ,
gas _used : receipt && receipt . gasUsed ? hexToDecimalString ( receipt . gasUsed ) : '0' ,
gas _limit : tx . gas ? hexToDecimalString ( tx . gas ) : '0' ,
nonce : tx . nonce != null ? String ( hexToNumber ( tx . nonce ) ) : '0' ,
status : receipt && receipt . status != null ? hexToNumber ( receipt . status ) : 0 ,
created _at : blockTimestamp ,
input : tx . input || '0x' ,
max _fee _per _gas : tx . maxFeePerGas ? hexToDecimalString ( tx . maxFeePerGas ) : undefined ,
max _priority _fee _per _gas : tx . maxPriorityFeePerGas ? hexToDecimalString ( tx . maxPriorityFeePerGas ) : undefined ,
type : txType ,
contract _address : receipt && receipt . contractAddress ? receipt . contractAddress : null
} ) ;
}
async function fetchChain138BlocksPage ( page , pageSize ) {
try {
var response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/blocks?page= ${ page } &page_size= ${ pageSize } ` ) ;
if ( response && response . items ) {
return response . items . map ( normalizeBlock ) . filter ( function ( block ) { return block !== null ; } ) ;
}
} catch ( error ) {
console . warn ( 'Falling back to RPC blocks list:' , error . message || error ) ;
}
var blocks = [ ] ;
var latestBlockHex = await rpcCall ( 'eth_blockNumber' , [ ] ) ;
var latestBlock = hexToNumber ( latestBlockHex ) ;
var startIndex = Math . max ( 0 , latestBlock - ( ( page - 1 ) * pageSize ) ) ;
for ( var i = 0 ; i < pageSize && startIndex - i >= 0 ; i ++ ) {
var block = await rpcCall ( 'eth_getBlockByNumber' , [ '0x' + ( startIndex - i ) . toString ( 16 ) , false ] ) . catch ( function ( ) { return null ; } ) ;
var normalized = normalizeRpcBlock ( block ) ;
if ( normalized ) blocks . push ( normalized ) ;
}
return blocks ;
}
async function fetchChain138TransactionsPage ( page , pageSize ) {
try {
var response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions?page= ${ page } &page_size= ${ pageSize } ` ) ;
if ( response && response . items ) {
return response . items . map ( normalizeTransaction ) . filter ( function ( tx ) { return tx !== null ; } ) ;
}
} catch ( error ) {
console . warn ( 'Falling back to RPC transactions list:' , error . message || error ) ;
}
var transactions = [ ] ;
var latestBlockHex = await rpcCall ( 'eth_blockNumber' , [ ] ) ;
var latestBlock = hexToNumber ( latestBlockHex ) ;
var blockCursor = Math . max ( 0 , latestBlock - ( ( page - 1 ) * 5 ) ) ;
while ( blockCursor >= 0 && transactions . length < pageSize ) {
var block = await rpcCall ( 'eth_getBlockByNumber' , [ '0x' + blockCursor . toString ( 16 ) , true ] ) . catch ( function ( ) { return null ; } ) ;
if ( block && Array . isArray ( block . transactions ) ) {
var blockIsoTimestamp = rpcTimestampToIso ( block . timestamp ) ;
block . transactions . forEach ( function ( tx ) {
if ( transactions . length >= pageSize ) return ;
var normalized = normalizeTransaction ( {
hash : tx . hash ,
from : tx . from ,
to : tx . to ,
value : hexToDecimalString ( tx . value ) ,
block _number : tx . blockNumber ? hexToDecimalString ( tx . blockNumber ) : hexToDecimalString ( block . number ) ,
block _hash : tx . blockHash || block . hash ,
gas _price : tx . gasPrice ? hexToDecimalString ( tx . gasPrice ) : ( tx . maxFeePerGas ? hexToDecimalString ( tx . maxFeePerGas ) : '0' ) ,
gas _limit : tx . gas ? hexToDecimalString ( tx . gas ) : '0' ,
nonce : tx . nonce != null ? String ( hexToNumber ( tx . nonce ) ) : '0' ,
created _at : blockIsoTimestamp ,
input : tx . input || '0x' ,
max _fee _per _gas : tx . maxFeePerGas ? hexToDecimalString ( tx . maxFeePerGas ) : undefined ,
max _priority _fee _per _gas : tx . maxPriorityFeePerGas ? hexToDecimalString ( tx . maxPriorityFeePerGas ) : undefined ,
type : tx . type != null ? hexToNumber ( tx . type ) : 0
} ) ;
if ( normalized ) transactions . push ( normalized ) ;
} ) ;
}
blockCursor -= 1 ;
}
return transactions ;
}
async function fetchChain138BlockDetail ( blockNumber ) {
try {
var response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/blocks/ ${ blockNumber } ` ) ;
return {
block : normalizeBlock ( response ) ,
rawBlockResponse : response
} ;
} catch ( error ) {
console . warn ( 'Falling back to RPC block detail:' , error . message || error ) ;
}
var rpcBlock = await rpcCall ( 'eth_getBlockByNumber' , [ '0x' + Number ( blockNumber ) . toString ( 16 ) , true ] ) ;
return {
block : normalizeRpcBlock ( rpcBlock ) ,
rawBlockResponse : null
} ;
}
async function fetchChain138TransactionDetail ( txHash ) {
try {
var response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions/ ${ txHash } ` ) ;
return {
transaction : normalizeTransaction ( response ) ,
rawTransaction : response
} ;
} catch ( error ) {
console . warn ( 'Falling back to RPC transaction detail:' , error . message || error ) ;
}
var rpcTx = await rpcCall ( 'eth_getTransactionByHash' , [ txHash ] ) ;
if ( ! rpcTx ) {
return { transaction : null , rawTransaction : null } ;
}
var receipt = await rpcCall ( 'eth_getTransactionReceipt' , [ txHash ] ) . catch ( function ( ) { return null ; } ) ;
var block = rpcTx . blockNumber ? await rpcCall ( 'eth_getBlockByNumber' , [ rpcTx . blockNumber , false ] ) . catch ( function ( ) { return null ; } ) : null ;
return {
transaction : normalizeRpcTransaction ( rpcTx , receipt , block ) ,
rawTransaction : null
} ;
}
2026-02-16 03:09:53 -08:00
// Skeleton loader function
function createSkeletonLoader ( type ) {
switch ( type ) {
case 'stats' :
return `
< div class = "stats-grid" >
< div class = "stat-card" >
< div class = "skeleton skeleton-title" > < / d i v >
< div class = "skeleton skeleton-text" style = "width: 80%;" > < / d i v >
< / d i v >
< div class = "stat-card" >
< div class = "skeleton skeleton-title" > < / d i v >
< div class = "skeleton skeleton-text" style = "width: 80%;" > < / d i v >
< / d i v >
< div class = "stat-card" >
< div class = "skeleton skeleton-title" > < / d i v >
< div class = "skeleton skeleton-text" style = "width: 80%;" > < / d i v >
< / d i v >
< div class = "stat-card" >
< div class = "skeleton skeleton-title" > < / d i v >
< div class = "skeleton skeleton-text" style = "width: 80%;" > < / d i v >
< / d i v >
< / d i v >
` ;
case 'table' :
return `
< table class = "table" >
< thead >
< tr >
< th > < div class = "skeleton skeleton-text" > < / d i v > < / t h >
< th > < div class = "skeleton skeleton-text" > < / d i v > < / t h >
< th > < div class = "skeleton skeleton-text" > < / d i v > < / t h >
< th > < div class = "skeleton skeleton-text" > < / d i v > < / t h >
< / t r >
< / t h e a d >
< tbody >
$ { Array ( 5 ) . fill ( 0 ) . map ( ( ) => `
< tr >
< td > < div class = "skeleton skeleton-text" > < / d i v > < / t d >
< td > < div class = "skeleton skeleton-text" > < / d i v > < / t d >
< td > < div class = "skeleton skeleton-text" > < / d i v > < / t d >
< td > < div class = "skeleton skeleton-text" > < / d i v > < / t d >
< / t r >
` ).join('')}
< / t b o d y >
< / t a b l e >
` ;
case 'detail' :
return `
< div class = "card" >
< div class = "card-header" >
< div class = "skeleton skeleton-title" style = "width: 40%;" > < / d i v >
< / d i v >
< div class = "card-body" >
$ { Array ( 8 ) . fill ( 0 ) . map ( ( ) => `
< div class = "info-row" >
< div class = "info-label" >
< div class = "skeleton skeleton-text" style = "width: 120px;" > < / d i v >
< / d i v >
< div class = "info-value" >
< div class = "skeleton skeleton-text" style = "width: 200px;" > < / d i v >
< / d i v >
< / d i v >
` ).join('')}
< / d i v >
< / d i v >
` ;
default :
return '<div class="loading"><i class="fas fa-spinner fa-spin"></i> Loading...</div>' ;
}
}
2026-03-02 12:14:13 -08:00
/ * *
* Parse amount from WETH wrap / unwrap field . Label is "Amount (ETH)" .
* - If input looks like raw wei ( integer string , 18 + digits , no decimal ) , use as wei .
* - Otherwise treat as ETH and convert with parseEther ( e . g . "100" - > 100 ETH ) .
* So pasting 100000000000000000000 wraps 100 ETH ; typing 100 also wraps 100 ETH .
* /
function parseWETHAmount ( inputStr ) {
var s = ( inputStr && String ( inputStr ) . trim ( ) ) || '' ;
if ( ! s ) return null ;
try {
if ( /^\d+$/ . test ( s ) && s . length >= 18 ) {
return ethers . BigNumber . from ( s ) ;
}
return ethers . utils . parseEther ( s ) ;
} catch ( e ) {
return null ;
}
}
2026-02-16 03:09:53 -08:00
// WETH ABI (Standard ERC-20 + WETH functions)
const WETH _ABI = [
"function deposit() payable" ,
"function withdraw(uint256 wad)" ,
"function balanceOf(address account) view returns (uint256)" ,
"function transfer(address to, uint256 amount) returns (bool)" ,
"function approve(address spender, uint256 amount) returns (bool)" ,
"function allowance(address owner, address spender) view returns (uint256)" ,
"function totalSupply() view returns (uint256)" ,
"function name() view returns (string)" ,
"function symbol() view returns (string)" ,
"function decimals() view returns (uint8)" ,
"event Deposit(address indexed dst, uint256 wad)" ,
"event Withdrawal(address indexed src, uint256 wad)"
] ;
// Helper function to check if ethers is loaded
function ensureEthers ( ) {
// Check immediately - ethers might already be loaded
if ( typeof ethers !== 'undefined' ) {
window . ethersReady = true ;
return Promise . resolve ( true ) ;
}
// Wait for ethers to load if it's still loading
return new Promise ( ( resolve , reject ) => {
let resolved = false ;
// Check immediately first (double check)
if ( typeof ethers !== 'undefined' ) {
window . ethersReady = true ;
resolve ( true ) ;
return ;
}
// Wait for ethersReady event
const timeout = setTimeout ( ( ) => {
if ( ! resolved ) {
resolved = true ;
// Final check before rejecting
if ( typeof ethers !== 'undefined' ) {
window . ethersReady = true ;
resolve ( true ) ;
} else {
console . error ( 'Ethers library failed to load after 20 seconds' ) ;
reject ( new Error ( 'Ethers library failed to load. Please refresh the page.' ) ) ;
}
}
} , 20000 ) ; // 20 second timeout
const checkInterval = setInterval ( ( ) => {
if ( typeof ethers !== 'undefined' && ! resolved ) {
resolved = true ;
clearInterval ( checkInterval ) ;
clearTimeout ( timeout ) ;
window . ethersReady = true ;
console . log ( '✅ Ethers detected via polling' ) ;
resolve ( true ) ;
}
} , 100 ) ;
// Listen for ethersReady event
const onReady = function ( ) {
if ( ! resolved ) {
resolved = true ;
clearInterval ( checkInterval ) ;
clearTimeout ( timeout ) ;
window . removeEventListener ( 'ethersReady' , onReady ) ;
if ( typeof ethers !== 'undefined' ) {
window . ethersReady = true ;
console . log ( '✅ Ethers ready via event' ) ;
resolve ( true ) ;
} else {
console . error ( 'ethersReady event fired but ethers is still undefined' ) ;
reject ( new Error ( 'Ethers library is not loaded. Please refresh the page.' ) ) ;
}
}
} ;
window . addEventListener ( 'ethersReady' , onReady , { once : true } ) ;
} ) ;
}
// Helper function to get API URL based on chain ID
function getAPIUrl ( endpoint ) {
// For ChainID 138, use Blockscout API
if ( CHAIN _ID === 138 ) {
return ` ${ BLOCKSCOUT _API } ${ endpoint } ` ;
}
// For other networks, use v2 Etherscan/Blockscan APIs
return ` ${ API _BASE } /v2 ${ endpoint } ` ;
}
// Initialize - only run once
let initialized = false ;
document . addEventListener ( 'DOMContentLoaded' , async ( ) => {
if ( initialized ) {
console . warn ( 'Initialization already completed, skipping...' ) ;
return ;
}
initialized = true ;
applyStoredTheme ( ) ;
var localeSel = document . getElementById ( 'localeSelect' ) ; if ( localeSel ) localeSel . value = currentLocale ;
if ( typeof applyI18n === 'function' ) applyI18n ( ) ;
2026-03-27 12:02:36 -07:00
var initialRoute = ( window . location . pathname || '/' ) . replace ( /^\// , '' ) . replace ( /\/$/ , '' ) . replace ( /^index\.html$/i , '' ) ;
2026-02-16 03:09:53 -08:00
applyHashRoute ( ) ;
window . addEventListener ( 'popstate' , function ( ) { applyHashRoute ( ) ; } ) ;
window . addEventListener ( 'hashchange' , function ( ) { applyHashRoute ( ) ; } ) ;
window . addEventListener ( 'load' , function ( ) { applyHashRoute ( ) ; } ) ;
2026-03-27 12:02:36 -07:00
var shouldLoadHomeData = ! initialRoute || initialRoute === 'home' ;
if ( shouldLoadHomeData ) {
console . log ( 'Loading stats, blocks, and transactions...' ) ;
loadStats ( ) ;
loadLatestBlocks ( ) ;
loadLatestTransactions ( ) ;
startTransactionUpdates ( ) ;
} else {
setTimeout ( function ( ) { applyHashRoute ( ) ; } , 0 ) ;
}
2026-02-16 03:09:53 -08:00
// Ethers is only needed for MetaMask/WETH; don't block feeds on it
try {
await ensureEthers ( ) ;
console . log ( 'Ethers ready.' ) ;
} catch ( error ) {
console . warn ( 'Ethers not ready, continuing without MetaMask features:' , error ) ;
}
setTimeout ( ( ) => {
if ( typeof ethers !== 'undefined' && typeof window . ethereum !== 'undefined' ) {
checkMetaMaskConnection ( ) ;
}
} , 500 ) ;
} ) ;
// MetaMask Connection
let checkingMetaMask = false ;
async function checkMetaMaskConnection ( ) {
// Prevent multiple simultaneous checks
if ( checkingMetaMask ) {
console . log ( 'checkMetaMaskConnection already in progress, skipping...' ) ;
return ;
}
checkingMetaMask = true ;
try {
// Ensure ethers is loaded before checking MetaMask
if ( typeof ethers === 'undefined' ) {
try {
await ensureEthers ( ) ;
// Wait a bit more to ensure ethers is fully initialized
await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
} catch ( error ) {
console . warn ( 'Ethers not available, skipping MetaMask check:' , error ) ;
return ;
}
}
// Double-check ethers is available
if ( typeof ethers === 'undefined' ) {
console . warn ( 'Ethers still not available after ensureEthers(), skipping MetaMask check' ) ;
return ;
}
if ( typeof window . ethereum !== 'undefined' ) {
try {
const accounts = await window . ethereum . request ( { method : 'eth_accounts' } ) ;
if ( accounts . length > 0 ) {
await connectMetaMask ( ) ;
}
2026-03-02 12:14:13 -08:00
} catch ( _ ) {
// User has not authorized the site yet; skip auto-connect silently
2026-02-16 03:09:53 -08:00
}
}
} finally {
checkingMetaMask = false ;
}
}
2026-03-02 12:14:13 -08:00
var ERC20 _META _ABI = [ 'function symbol() view returns (string)' , 'function name() view returns (string)' , 'function decimals() view returns (uint8)' ] ;
var SYMBOL _SELECTOR = '0x95d89b41' ;
var NAME _SELECTOR = '0x06fdde03' ;
var DECIMALS _SELECTOR = '0x313ce567' ;
function decodeBytes32OrString ( hex ) {
if ( ! hex || typeof hex !== 'string' || hex . length < 66 ) return '' ;
var data = hex . slice ( 2 ) ;
if ( data . length >= 64 ) {
var offset = parseInt ( data . slice ( 0 , 64 ) , 16 ) ;
var len = parseInt ( data . slice ( 64 , 128 ) , 16 ) ;
if ( offset === 32 && len > 0 && data . length >= 128 + len * 2 ) {
return ethers . utils . toUtf8String ( '0x' + data . slice ( 128 , 128 + len * 2 ) ) . replace ( /\0+$/g , '' ) ;
}
var fixed = '0x' + data . slice ( 0 , 64 ) ;
try {
return ethers . utils . parseBytes32String ( fixed ) . replace ( /\0+$/g , '' ) ;
} catch ( _ ) {
return ethers . utils . toUtf8String ( fixed ) . replace ( /\0+$/g , '' ) ;
}
}
return '' ;
}
async function fetchTokenMetadataFromChain ( address ) {
if ( typeof window . ethereum === 'undefined' || typeof ethers === 'undefined' ) return null ;
var prov = new ethers . providers . Web3Provider ( window . ethereum ) ;
try {
var contract = new ethers . Contract ( address , ERC20 _META _ABI , prov ) ;
var sym = await contract . symbol ( ) ;
var nam = await contract . name ( ) ;
var dec = await contract . decimals ( ) ;
var decimalsNum = ( typeof dec === 'number' ) ? dec : ( dec && dec . toNumber ? dec . toNumber ( ) : 18 ) ;
var symbolStr = ( sym != null && sym !== undefined ) ? String ( sym ) : '' ;
var nameStr = ( nam != null && nam !== undefined ) ? String ( nam ) : '' ;
return { symbol : symbolStr , name : nameStr , decimals : decimalsNum } ;
} catch ( e ) {
try {
var outSymbol = await window . ethereum . request ( { method : 'eth_call' , params : [ { to : address , data : SYMBOL _SELECTOR } ] } ) ;
var outName = await window . ethereum . request ( { method : 'eth_call' , params : [ { to : address , data : NAME _SELECTOR } ] } ) ;
var outDecimals = await window . ethereum . request ( { method : 'eth_call' , params : [ { to : address , data : DECIMALS _SELECTOR } ] } ) ;
if ( outSymbol && outSymbol !== '0x' ) {
var symbolStr = decodeBytes32OrString ( outSymbol ) ;
var nameStr = outName && outName !== '0x' ? decodeBytes32OrString ( outName ) : '' ;
var decimalsNum = 18 ;
if ( outDecimals && outDecimals . length >= 66 ) {
decimalsNum = parseInt ( outDecimals . slice ( 2 , 66 ) , 16 ) ;
if ( isNaN ( decimalsNum ) ) decimalsNum = 18 ;
}
return { symbol : symbolStr , name : nameStr , decimals : decimalsNum } ;
}
} catch ( _ ) { }
return null ;
}
}
2026-02-22 15:35:45 -08:00
async function addTokenToWallet ( address , symbol , decimals , name ) {
if ( ! address || ! /^0x[a-fA-F0-9]{40}$/i . test ( address ) ) {
if ( typeof showToast === 'function' ) showToast ( 'Invalid token address' , 'error' ) ;
return ;
}
if ( typeof window . ethereum === 'undefined' ) {
if ( typeof showToast === 'function' ) showToast ( 'No wallet detected. Install MetaMask or another Web3 wallet.' , 'error' ) ;
return ;
}
try {
2026-03-02 12:14:13 -08:00
var meta = await fetchTokenMetadataFromChain ( address ) ;
if ( ! meta ) {
if ( typeof showToast === 'function' ) showToast ( 'Could not read token from chain. Switch to the correct network and try again.' , 'error' ) ;
return ;
}
var useSymbol = ( meta . symbol !== undefined && meta . symbol !== null ) ? meta . symbol : 'TOKEN' ;
var useName = ( meta . name !== undefined && meta . name !== null ) ? meta . name : ( name || '' ) ;
var useDecimals = ( typeof meta . decimals === 'number' ) ? meta . decimals : ( typeof decimals === 'number' ? decimals : 18 ) ;
if ( useSymbol === '' || ( typeof useSymbol === 'string' && useSymbol . trim ( ) === '' ) ) {
if ( typeof showToast === 'function' ) showToast ( 'This token has no symbol on-chain. Add it manually in MetaMask: use this contract address and set symbol to WETH.' , 'info' ) ;
return ;
}
2026-02-22 15:35:45 -08:00
var added = await window . ethereum . request ( {
method : 'wallet_watchAsset' ,
params : {
type : 'ERC20' ,
options : {
address : address ,
2026-03-02 12:14:13 -08:00
symbol : ( useSymbol !== undefined && useSymbol !== null ) ? useSymbol : 'TOKEN' ,
decimals : useDecimals ,
name : useName || undefined ,
2026-02-22 15:35:45 -08:00
image : undefined
}
}
} ) ;
if ( typeof showToast === 'function' ) {
2026-03-02 12:14:13 -08:00
var displaySym = useSymbol || symbol || 'Token' ;
showToast ( added ? ( displaySym ? displaySym + ' added to wallet' : 'Token added to wallet' ) : 'Add token was cancelled' , added ? 'success' : 'info' ) ;
2026-02-22 15:35:45 -08:00
}
} catch ( e ) {
2026-03-02 12:14:13 -08:00
var msg = ( e && e . message ) ? String ( e . message ) : '' ;
var friendly = ( e && e . code === 4001 ) || /not been authorized|rejected|denied/i . test ( msg )
? 'Please approve the MetaMask popup to add the token.'
: ( msg || 'Could not add token to wallet.' ) ;
if ( typeof showToast === 'function' ) showToast ( friendly , 'error' ) ;
2026-02-22 15:35:45 -08:00
}
}
window . addTokenToWallet = addTokenToWallet ;
2026-02-16 03:09:53 -08:00
let connectingMetaMask = false ;
async function connectMetaMask ( ) {
// Prevent multiple simultaneous connections
if ( connectingMetaMask ) {
console . log ( 'connectMetaMask already in progress, skipping...' ) ;
return ;
}
connectingMetaMask = true ;
try {
if ( typeof window . ethereum === 'undefined' ) {
alert ( 'MetaMask is not installed! Please install MetaMask to use WETH utilities.' ) ;
return ;
}
// Wait for ethers to be loaded
if ( typeof ethers === 'undefined' ) {
try {
await ensureEthers ( ) ;
// Wait a bit more to ensure ethers is fully initialized
await new Promise ( resolve => setTimeout ( resolve , 100 ) ) ;
} catch ( error ) {
alert ( 'Ethers library is not loaded. Please refresh the page and try again.' ) ;
console . error ( 'ethers loading error:' , error ) ;
return ;
}
}
// Double-check ethers is available
if ( typeof ethers === 'undefined' ) {
alert ( 'Ethers library is not loaded. Please refresh the page and try again.' ) ;
console . error ( 'Ethers still not available after ensureEthers()' ) ;
return ;
}
try {
// Request account access
const accounts = await window . ethereum . request ( { method : 'eth_requestAccounts' } ) ;
userAddress = accounts [ 0 ] ;
// Connect to Chain 138
await switchToChain138 ( ) ;
// Setup provider and signer
provider = new ethers . providers . Web3Provider ( window . ethereum ) ;
signer = provider . getSigner ( ) ;
// Update UI
const statusEl = document . getElementById ( 'metamaskStatus' ) ;
statusEl . className = 'metamask-status connected' ;
statusEl . innerHTML = `
< i class = "fas fa-check-circle" > < / i >
< span > Connected : $ { escapeHtml ( shortenHash ( userAddress ) ) } < / s p a n >
< button class = "btn btn-warning" onclick = "disconnectMetaMask()" style = "margin-left: auto;" aria - label = "Disconnect MetaMask wallet" > Disconnect < / b u t t o n >
` ;
// Enable buttons
document . getElementById ( 'weth9WrapBtn' ) . disabled = false ;
document . getElementById ( 'weth9UnwrapBtn' ) . disabled = false ;
document . getElementById ( 'weth10WrapBtn' ) . disabled = false ;
document . getElementById ( 'weth10UnwrapBtn' ) . disabled = false ;
// Load balances
await refreshWETHBalances ( ) ;
// Listen for account changes
window . ethereum . on ( 'accountsChanged' , ( accounts ) => {
if ( accounts . length === 0 ) {
disconnectMetaMask ( ) ;
} else {
connectMetaMask ( ) ;
}
} ) ;
// Listen for chain changes
window . ethereum . on ( 'chainChanged' , ( ) => {
switchToChain138 ( ) ;
} ) ;
} catch ( error ) {
2026-03-02 12:14:13 -08:00
var errMsg = ( error && error . message ) ? String ( error . message ) : '' ;
var friendly = ( error && error . code === 4001 ) || /not been authorized|rejected|denied/i . test ( errMsg )
? 'Connection was rejected. Click Connect Wallet and approve access when MetaMask asks.'
: ( errMsg . includes ( 'ethers is not defined' ) || typeof ethers === 'undefined' )
? 'Ethers library failed to load. Please refresh the page.'
: ( 'Failed to connect MetaMask: ' + ( errMsg || 'Unknown error' ) ) ;
alert ( friendly ) ;
2026-02-16 03:09:53 -08:00
}
} finally {
connectingMetaMask = false ;
}
}
async function switchToChain138 ( ) {
const chainId = '0x8A' ; // 138 in hex
try {
await window . ethereum . request ( {
method : 'wallet_switchEthereumChain' ,
params : [ { chainId } ] ,
} ) ;
} catch ( switchError ) {
// If chain doesn't exist, add it
if ( switchError . code === 4902 ) {
try {
await window . ethereum . request ( {
method : 'wallet_addEthereumChain' ,
params : [ {
chainId ,
chainName : 'Chain 138' ,
nativeCurrency : {
name : 'ETH' ,
symbol : 'ETH' ,
decimals : 18
} ,
rpcUrls : RPC _URLS . length > 0 ? RPC _URLS : [ RPC _URL ] ,
blockExplorerUrls : [ window . location . origin || 'https://explorer.d-bis.org' ]
} ] ,
} ) ;
} catch ( addError ) {
throw addError ;
}
} else {
throw switchError ;
}
}
}
function disconnectMetaMask ( ) {
provider = null ;
signer = null ;
userAddress = null ;
const statusEl = document . getElementById ( 'metamaskStatus' ) ;
statusEl . className = 'metamask-status disconnected' ;
statusEl . innerHTML = `
< i class = "fas fa-wallet" > < / i >
< span > MetaMask not connected < / s p a n >
< button class = "btn btn-success" onclick = "connectMetaMask()" style = "margin-left: auto;" aria - label = "Connect MetaMask wallet" > Connect MetaMask < / b u t t o n >
` ;
document . getElementById ( 'weth9WrapBtn' ) . disabled = true ;
document . getElementById ( 'weth9UnwrapBtn' ) . disabled = true ;
document . getElementById ( 'weth10WrapBtn' ) . disabled = true ;
document . getElementById ( 'weth10UnwrapBtn' ) . disabled = true ;
}
async function refreshWETHBalances ( ) {
if ( ! userAddress ) return ;
try {
await ensureEthers ( ) ;
// Checksum addresses when ethers is available
if ( typeof ethers !== 'undefined' && ethers . utils ) {
try {
// Convert to lowercase first, then checksum
const lowerAddress = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
WETH10 _ADDRESS = ethers . utils . getAddress ( lowerAddress ) ;
} catch ( e ) {
console . warn ( 'Could not checksum WETH10 address:' , e ) ;
// Fallback to lowercase version
WETH10 _ADDRESS = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
}
} else {
// Fallback to lowercase if ethers not available
WETH10 _ADDRESS = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
}
// Get ETH balance
const ethBalance = await provider . getBalance ( userAddress ) ;
const ethBalanceFormatted = formatEther ( ethBalance ) ;
// Get WETH9 balance
const weth9Contract = new ethers . Contract ( WETH9 _ADDRESS , WETH _ABI , provider ) ;
const weth9Balance = await weth9Contract . balanceOf ( userAddress ) ;
const weth9BalanceFormatted = formatEther ( weth9Balance ) ;
// Get WETH10 balance - use checksummed address
const weth10Contract = new ethers . Contract ( WETH10 _ADDRESS , WETH _ABI , provider ) ;
const weth10Balance = await weth10Contract . balanceOf ( userAddress ) ;
const weth10BalanceFormatted = formatEther ( weth10Balance ) ;
// Update UI
document . getElementById ( 'weth9EthBalance' ) . textContent = ethBalanceFormatted + ' ETH' ;
document . getElementById ( 'weth9TokenBalance' ) . textContent = weth9BalanceFormatted + ' WETH9' ;
document . getElementById ( 'weth10EthBalance' ) . textContent = ethBalanceFormatted + ' ETH' ;
document . getElementById ( 'weth10TokenBalance' ) . textContent = weth10BalanceFormatted + ' WETH10' ;
} catch ( error ) {
console . error ( 'Error refreshing balances:' , error ) ;
}
}
function wrapUnwrapErrorMessage ( op , error ) {
if ( error && ( error . code === 4001 || error . code === 'ACTION_REJECTED' || ( error . message && /user rejected|user denied/i . test ( error . message ) ) ) ) return 'Transaction cancelled.' ;
if ( error && error . reason ) return error . reason ;
return ( error && error . message ) ? error . message : 'Unknown error' ;
}
function setMaxWETH9 ( type ) {
if ( type === 'wrap' ) {
const ethBalance = document . getElementById ( 'weth9EthBalance' ) . textContent . replace ( ' ETH' , '' ) ;
document . getElementById ( 'weth9WrapAmount' ) . value = parseFloat ( ethBalance ) . toFixed ( 6 ) ;
} else {
const wethBalance = document . getElementById ( 'weth9TokenBalance' ) . textContent . replace ( ' WETH9' , '' ) ;
document . getElementById ( 'weth9UnwrapAmount' ) . value = parseFloat ( wethBalance ) . toFixed ( 6 ) ;
}
}
function setMaxWETH10 ( type ) {
if ( type === 'wrap' ) {
const ethBalance = document . getElementById ( 'weth10EthBalance' ) . textContent . replace ( ' ETH' , '' ) ;
document . getElementById ( 'weth10WrapAmount' ) . value = parseFloat ( ethBalance ) . toFixed ( 6 ) ;
} else {
const wethBalance = document . getElementById ( 'weth10TokenBalance' ) . textContent . replace ( ' WETH10' , '' ) ;
document . getElementById ( 'weth10UnwrapAmount' ) . value = parseFloat ( wethBalance ) . toFixed ( 6 ) ;
}
}
async function wrapWETH9 ( ) {
const amount = document . getElementById ( 'weth9WrapAmount' ) . value ;
if ( ! amount || parseFloat ( amount ) <= 0 ) {
alert ( 'Please enter a valid amount' ) ;
return ;
}
if ( ! signer ) {
alert ( 'Please connect MetaMask first' ) ;
return ;
}
try {
await ensureEthers ( ) ;
2026-03-02 12:14:13 -08:00
const amountWei = parseWETHAmount ( amount ) ;
if ( ! amountWei || amountWei . isZero ( ) ) {
alert ( 'Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).' ) ;
return ;
}
2026-02-16 03:09:53 -08:00
const ethBalance = await provider . getBalance ( userAddress ) ;
if ( ethBalance . lt ( amountWei ) ) {
alert ( 'Insufficient ETH balance. You have ' + formatEther ( ethBalance ) + ' ETH.' ) ;
return ;
}
const weth9Contract = new ethers . Contract ( WETH9 _ADDRESS , WETH _ABI , signer ) ;
try {
await weth9Contract . callStatic . deposit ( { value : amountWei } ) ;
} catch ( e ) {
alert ( 'Simulation failed: ' + ( e . reason || e . message || 'Unknown error' ) ) ;
return ;
}
const btn = document . getElementById ( 'weth9WrapBtn' ) ;
btn . disabled = true ;
btn . innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...' ;
const tx = await weth9Contract . deposit ( { value : amountWei } ) ;
const receipt = await tx . wait ( ) ;
btn . innerHTML = '<i class="fas fa-check"></i> Success!' ;
document . getElementById ( 'weth9WrapAmount' ) . value = '' ;
await refreshWETHBalances ( ) ;
setTimeout ( ( ) => {
btn . innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH9' ;
btn . disabled = false ;
} , 3000 ) ;
} catch ( error ) {
console . error ( 'Error wrapping WETH9:' , error ) ;
alert ( 'Wrap WETH9: ' + wrapUnwrapErrorMessage ( 'wrap' , error ) ) ;
document . getElementById ( 'weth9WrapBtn' ) . innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH9' ;
document . getElementById ( 'weth9WrapBtn' ) . disabled = false ;
}
}
async function unwrapWETH9 ( ) {
const amount = document . getElementById ( 'weth9UnwrapAmount' ) . value ;
if ( ! amount || parseFloat ( amount ) <= 0 ) {
alert ( 'Please enter a valid amount' ) ;
return ;
}
if ( ! signer ) {
alert ( 'Please connect MetaMask first' ) ;
return ;
}
try {
await ensureEthers ( ) ;
2026-03-02 12:14:13 -08:00
const amountWei = parseWETHAmount ( amount ) ;
if ( ! amountWei || amountWei . isZero ( ) ) {
alert ( 'Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).' ) ;
return ;
}
2026-02-16 03:09:53 -08:00
const weth9Contract = new ethers . Contract ( WETH9 _ADDRESS , WETH _ABI , signer ) ;
const wethBalance = await weth9Contract . balanceOf ( userAddress ) ;
if ( wethBalance . lt ( amountWei ) ) {
alert ( 'Insufficient WETH9 balance. You have ' + formatEther ( wethBalance ) + ' WETH9.' ) ;
return ;
}
try {
await weth9Contract . callStatic . withdraw ( amountWei ) ;
} catch ( e ) {
alert ( 'Simulation failed: ' + ( e . reason || e . message || 'Unknown error' ) ) ;
return ;
}
const btn = document . getElementById ( 'weth9UnwrapBtn' ) ;
btn . disabled = true ;
btn . innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...' ;
const tx = await weth9Contract . withdraw ( amountWei ) ;
const receipt = await tx . wait ( ) ;
btn . innerHTML = '<i class="fas fa-check"></i> Success!' ;
document . getElementById ( 'weth9UnwrapAmount' ) . value = '' ;
await refreshWETHBalances ( ) ;
setTimeout ( ( ) => {
btn . innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH9 to ETH' ;
btn . disabled = false ;
} , 3000 ) ;
} catch ( error ) {
console . error ( 'Error unwrapping WETH9:' , error ) ;
alert ( 'Unwrap WETH9: ' + wrapUnwrapErrorMessage ( 'unwrap' , error ) ) ;
document . getElementById ( 'weth9UnwrapBtn' ) . innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH9 to ETH' ;
document . getElementById ( 'weth9UnwrapBtn' ) . disabled = false ;
}
}
async function wrapWETH10 ( ) {
const amount = document . getElementById ( 'weth10WrapAmount' ) . value ;
if ( ! amount || parseFloat ( amount ) <= 0 ) {
alert ( 'Please enter a valid amount' ) ;
return ;
}
if ( ! signer ) {
alert ( 'Please connect MetaMask first' ) ;
return ;
}
try {
await ensureEthers ( ) ;
// Ensure address is checksummed
if ( typeof ethers !== 'undefined' && ethers . utils ) {
try {
const lowerAddress = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
WETH10 _ADDRESS = ethers . utils . getAddress ( lowerAddress ) ;
} catch ( e ) {
console . warn ( 'Could not checksum WETH10 address:' , e ) ;
WETH10 _ADDRESS = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
}
} else {
WETH10 _ADDRESS = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
}
2026-03-02 12:14:13 -08:00
const amountWei = parseWETHAmount ( amount ) ;
if ( ! amountWei || amountWei . isZero ( ) ) {
alert ( 'Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).' ) ;
return ;
}
2026-02-16 03:09:53 -08:00
const ethBalance = await provider . getBalance ( userAddress ) ;
if ( ethBalance . lt ( amountWei ) ) {
alert ( 'Insufficient ETH balance. You have ' + formatEther ( ethBalance ) + ' ETH.' ) ;
return ;
}
const weth10Contract = new ethers . Contract ( WETH10 _ADDRESS , WETH _ABI , signer ) ;
try {
await weth10Contract . callStatic . deposit ( { value : amountWei } ) ;
} catch ( e ) {
alert ( 'Simulation failed: ' + ( e . reason || e . message || 'Unknown error' ) ) ;
return ;
}
const btn = document . getElementById ( 'weth10WrapBtn' ) ;
btn . disabled = true ;
btn . innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...' ;
const tx = await weth10Contract . deposit ( { value : amountWei } ) ;
const receipt = await tx . wait ( ) ;
btn . innerHTML = '<i class="fas fa-check"></i> Success!' ;
document . getElementById ( 'weth10WrapAmount' ) . value = '' ;
await refreshWETHBalances ( ) ;
setTimeout ( ( ) => {
btn . innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH10' ;
btn . disabled = false ;
} , 3000 ) ;
} catch ( error ) {
console . error ( 'Error wrapping WETH10:' , error ) ;
alert ( 'Wrap WETH10: ' + wrapUnwrapErrorMessage ( 'wrap' , error ) ) ;
document . getElementById ( 'weth10WrapBtn' ) . innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH10' ;
document . getElementById ( 'weth10WrapBtn' ) . disabled = false ;
}
}
async function unwrapWETH10 ( ) {
const amount = document . getElementById ( 'weth10UnwrapAmount' ) . value ;
if ( ! amount || parseFloat ( amount ) <= 0 ) {
alert ( 'Please enter a valid amount' ) ;
return ;
}
if ( ! signer ) {
alert ( 'Please connect MetaMask first' ) ;
return ;
}
try {
await ensureEthers ( ) ;
// Ensure address is checksummed
if ( typeof ethers !== 'undefined' && ethers . utils ) {
try {
const lowerAddress = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
WETH10 _ADDRESS = ethers . utils . getAddress ( lowerAddress ) ;
} catch ( e ) {
console . warn ( 'Could not checksum WETH10 address:' , e ) ;
WETH10 _ADDRESS = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
}
} else {
WETH10 _ADDRESS = WETH10 _ADDRESS _RAW . toLowerCase ( ) ;
}
2026-03-02 12:14:13 -08:00
const amountWei = parseWETHAmount ( amount ) ;
if ( ! amountWei || amountWei . isZero ( ) ) {
alert ( 'Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).' ) ;
return ;
}
2026-02-16 03:09:53 -08:00
const weth10Contract = new ethers . Contract ( WETH10 _ADDRESS , WETH _ABI , signer ) ;
const wethBalance = await weth10Contract . balanceOf ( userAddress ) ;
if ( wethBalance . lt ( amountWei ) ) {
alert ( 'Insufficient WETH10 balance. You have ' + formatEther ( wethBalance ) + ' WETH10.' ) ;
return ;
}
try {
await weth10Contract . callStatic . withdraw ( amountWei ) ;
} catch ( e ) {
alert ( 'Simulation failed: ' + ( e . reason || e . message || 'Unknown error' ) ) ;
return ;
}
const btn = document . getElementById ( 'weth10UnwrapBtn' ) ;
btn . disabled = true ;
btn . innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...' ;
const tx = await weth10Contract . withdraw ( amountWei ) ;
const receipt = await tx . wait ( ) ;
btn . innerHTML = '<i class="fas fa-check"></i> Success!' ;
document . getElementById ( 'weth10UnwrapAmount' ) . value = '' ;
await refreshWETHBalances ( ) ;
setTimeout ( ( ) => {
btn . innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH10 to ETH' ;
btn . disabled = false ;
} , 3000 ) ;
} catch ( error ) {
console . error ( 'Error unwrapping WETH10:' , error ) ;
alert ( 'Unwrap WETH10: ' + wrapUnwrapErrorMessage ( 'unwrap' , error ) ) ;
document . getElementById ( 'weth10UnwrapBtn' ) . innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH10 to ETH' ;
document . getElementById ( 'weth10UnwrapBtn' ) . disabled = false ;
}
}
function showWETHTab ( tab , clickedElement ) {
document . querySelectorAll ( '.weth-tab-content' ) . forEach ( el => el . style . display = 'none' ) ;
document . querySelectorAll ( '.weth-tab' ) . forEach ( el => el . classList . remove ( 'active' ) ) ;
const tabElement = document . getElementById ( ` ${ tab } Tab ` ) ;
if ( tabElement ) {
tabElement . style . display = 'block' ;
}
// Update active tab - use clickedElement if provided, otherwise find by tab name
if ( clickedElement ) {
clickedElement . classList . add ( 'active' ) ;
} else {
// Find the button that corresponds to this tab
const tabButtons = document . querySelectorAll ( '.weth-tab' ) ;
tabButtons . forEach ( btn => {
if ( btn . getAttribute ( 'onclick' ) ? . includes ( ` ' ${ tab } ' ` ) ) {
btn . classList . add ( 'active' ) ;
}
} ) ;
}
}
window . showWETHTab = showWETHTab ;
2026-03-27 12:02:36 -07:00
async function renderWETHUtilitiesView ( ) {
2026-02-16 03:09:53 -08:00
showView ( 'weth' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'weth' ) updatePath ( '/weth' ) ;
if ( userAddress ) {
await refreshWETHBalances ( ) ;
}
}
2026-03-27 12:02:36 -07:00
window . _showWETHUtilities = renderWETHUtilitiesView ;
2026-02-16 03:09:53 -08:00
2026-03-27 12:02:36 -07:00
async function renderBridgeView ( ) {
2026-02-16 03:09:53 -08:00
showView ( 'bridge' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'bridge' ) updatePath ( '/bridge' ) ;
await refreshBridgeData ( ) ;
}
2026-03-27 12:02:36 -07:00
window . _showBridgeMonitoring = renderBridgeView ;
2026-02-16 03:09:53 -08:00
2026-03-27 12:02:36 -07:00
async function renderHomeView ( ) {
2026-02-16 03:09:53 -08:00
showView ( 'home' ) ;
2026-03-28 00:21:18 -07:00
if ( ( window . location . pathname || '' ) . replace ( /\/$/ , '' ) !== '' ) updatePath ( '/' ) ;
2026-02-16 03:09:53 -08:00
await loadStats ( ) ;
await loadLatestBlocks ( ) ;
await loadLatestTransactions ( ) ;
// Start real-time transaction updates
startTransactionUpdates ( ) ;
}
2026-03-27 12:02:36 -07:00
window . _showHome = renderHomeView ;
2026-02-16 03:09:53 -08:00
2026-03-27 12:02:36 -07:00
async function renderBlocksView ( ) {
2026-02-16 03:09:53 -08:00
showView ( 'blocks' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'blocks' ) updatePath ( '/blocks' ) ;
await loadAllBlocks ( ) ;
}
2026-03-27 12:02:36 -07:00
window . _showBlocks = renderBlocksView ;
2026-02-16 03:09:53 -08:00
2026-03-27 12:02:36 -07:00
async function renderTransactionsView ( ) {
2026-02-16 03:09:53 -08:00
showView ( 'transactions' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'transactions' ) updatePath ( '/transactions' ) ;
await loadAllTransactions ( ) ;
}
2026-03-27 12:02:36 -07:00
window . _showTransactions = renderTransactionsView ;
async function renderAddressesView ( ) {
showView ( 'addresses' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'addresses' ) updatePath ( '/addresses' ) ;
await loadAllAddresses ( ) ;
}
window . _showAddresses = renderAddressesView ;
2026-02-16 03:09:53 -08:00
2026-03-28 15:15:23 -07:00
function buildAnalyticsViewHtml ( ) {
var html = '' ;
html += '<div style="display:grid; gap:1rem;">' ;
html += '<div class="card" style="margin:0; box-shadow:none;">' ;
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">' ;
html += '<div>' ;
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-chart-line"></i> Live Network Analytics</h3>' ;
html += '<div style="color:var(--text-light); line-height:1.55;">Analytics surfaces are consolidated into the live explorer dashboards instead of a separate unfinished panel. Use this page as a hub to the active gas, block, bridge, and route monitoring views.</div>' ;
html += '</div>' ;
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">' ;
html += '<button type="button" class="btn btn-primary" onclick="showHome(); updatePath(\'/\')" aria-label="Open network dashboard"><i class="fas fa-gauge-high"></i> Network dashboard</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showBridgeMonitoring(); updatePath(\'/bridge\')" aria-label="Open bridge monitoring"><i class="fas fa-bridge"></i> Bridge monitoring</button>' ;
html += '</div>' ;
html += '</div>' ;
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem;">' ;
html += '<a href="/" onclick="event.preventDefault(); showHome(); updatePath(\'/\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Gas & Network</div><div style="color:var(--text-light); line-height:1.5;">Open the live home dashboard for gas price, TPS, block time, validator count, and latest-chain activity.</div></a>' ;
html += '<a href="/blocks" onclick="event.preventDefault(); showBlocks(); updatePath(\'/blocks\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Block cadence</div><div style="color:var(--text-light); line-height:1.5;">Inspect live block production, miner attribution, gas usage, and exportable block history.</div></a>' ;
html += '<a href="/transactions" onclick="event.preventDefault(); showTransactions(); updatePath(\'/transactions\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Transaction flow</div><div style="color:var(--text-light); line-height:1.5;">Review the recent transaction stream and drill into decoded execution details and internal calls.</div></a>' ;
html += '<a href="/routes" onclick="event.preventDefault(); showRoutes(); updatePath(\'/routes\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Route coverage</div><div style="color:var(--text-light); line-height:1.5;">Open the dedicated route-decision tree for swap-path coverage, bridge branches, and missing quote-token diagnostics.</div></a>' ;
html += '</div>' ;
html += '</div>' ;
return html ;
}
// Analytics view
2026-03-27 12:02:36 -07:00
function renderAnalyticsView ( ) {
2026-03-28 15:15:23 -07:00
showView ( 'analytics' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'analytics' ) updatePath ( '/analytics' ) ;
var container = document . getElementById ( 'analyticsContent' ) ;
if ( ! container ) return ;
container . innerHTML = buildAnalyticsViewHtml ( ) ;
2026-02-16 03:09:53 -08:00
}
2026-03-27 12:02:36 -07:00
window . _showAnalytics = renderAnalyticsView ;
2026-02-16 03:09:53 -08:00
2026-03-28 15:15:23 -07:00
function buildOperatorViewHtml ( ) {
var html = '' ;
html += '<div style="display:grid; gap:1rem;">' ;
html += '<div class="card" style="margin:0; box-shadow:none;">' ;
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">' ;
html += '<div>' ;
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-server"></i> Operator Access Hub</h3>' ;
html += '<div style="color:var(--text-light); line-height:1.55;">The explorer does not expose raw privileged controls here. Instead, this page collects the live operator-facing observability and execution surfaces that are safe to browse from the public UI.</div>' ;
html += '</div>' ;
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">' ;
html += '<button type="button" class="btn btn-primary" onclick="showBridgeMonitoring(); updatePath(\'/bridge\')" aria-label="Open bridge monitoring"><i class="fas fa-bridge"></i> Bridge status</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\')" aria-label="Open liquidity access"><i class="fas fa-wave-square"></i> Liquidity APIs</button>' ;
html += '</div>' ;
html += '</div>' ;
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem;">' ;
html += '<a href="/bridge" onclick="event.preventDefault(); showBridgeMonitoring(); updatePath(\'/bridge\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Bridge monitoring</div><div style="color:var(--text-light); line-height:1.5;">Inspect bridge balances, fee-token posture, destination configuration, and live contract references.</div></a>' ;
html += '<a href="/liquidity" onclick="event.preventDefault(); showLiquidityAccess(); updatePath(\'/liquidity\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Liquidity access</div><div style="color:var(--text-light); line-height:1.5;">Jump to partner payload routes, ingestion APIs, and public execution-plan endpoints without leaving the explorer.</div></a>' ;
html += '<a href="/pools" onclick="event.preventDefault(); showPools(); updatePath(\'/pools\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Pool inventory</div><div style="color:var(--text-light); line-height:1.5;">Review canonical PMM addresses, funding state, registry status, and exportable pool snapshots.</div></a>' ;
html += '<a href="/weth" onclick="event.preventDefault(); showWETHUtilities(); updatePath(\'/weth\');" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">WETH utilities</div><div style="color:var(--text-light); line-height:1.5;">Open the WETH9/WETH10 utilities, bridge contract references, and balance tools that operators often need during support.</div></a>' ;
html += '</div>' ;
html += '</div>' ;
return html ;
}
// Operator view
2026-03-27 12:02:36 -07:00
function renderOperatorView ( ) {
2026-03-28 15:15:23 -07:00
showView ( 'operator' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'operator' ) updatePath ( '/operator' ) ;
var container = document . getElementById ( 'operatorContent' ) ;
if ( ! container ) return ;
container . innerHTML = buildOperatorViewHtml ( ) ;
2026-02-16 03:09:53 -08:00
}
2026-03-27 12:02:36 -07:00
window . _showOperator = renderOperatorView ;
2026-02-16 03:09:53 -08:00
function showView ( viewName ) {
currentView = viewName ;
2026-03-28 15:15:23 -07:00
var detailViews = [ 'blockDetail' , 'transactionDetail' , 'addressDetail' , 'tokenDetail' , 'nftDetail' , 'watchlist' , 'searchResults' , 'tokens' , 'addresses' , 'pools' , 'routes' , 'liquidity' , 'more' , 'analytics' , 'operator' ] ;
2026-02-16 03:09:53 -08:00
if ( detailViews . indexOf ( viewName ) === - 1 ) currentDetailKey = '' ;
document . querySelectorAll ( '.detail-view' ) . forEach ( v => v . classList . remove ( 'active' ) ) ;
const homeView = document . getElementById ( 'homeView' ) ;
if ( homeView ) homeView . style . display = viewName === 'home' ? 'block' : 'none' ;
if ( viewName !== 'home' ) {
const targetView = document . getElementById ( ` ${ viewName } View ` ) ;
if ( targetView ) targetView . classList . add ( 'active' ) ;
stopTransactionUpdates ( ) ;
}
}
window . showView = showView ;
function toggleDarkMode ( ) {
document . body . classList . toggle ( 'dark-theme' ) ;
var isDark = document . body . classList . contains ( 'dark-theme' ) ;
try { localStorage . setItem ( 'explorerTheme' , isDark ? 'dark' : 'light' ) ; } catch ( e ) { }
var icon = document . getElementById ( 'themeIcon' ) ;
if ( icon ) {
icon . className = isDark ? 'fas fa-sun' : 'fas fa-moon' ;
}
}
function applyStoredTheme ( ) {
try {
var theme = localStorage . getItem ( 'explorerTheme' ) ;
if ( theme === 'dark' ) {
document . body . classList . add ( 'dark-theme' ) ;
var icon = document . getElementById ( 'themeIcon' ) ;
if ( icon ) icon . className = 'fas fa-sun' ;
}
} catch ( e ) { }
}
function updatePath ( path ) {
if ( typeof history !== 'undefined' && history . pushState ) {
history . pushState ( null , '' , path ) ;
}
}
window . updatePath = updatePath ;
function applyHashRoute ( ) {
var route = '' ;
var fromPath = ( window . location . pathname || '/' ) . replace ( /^\// , '' ) . replace ( /\/$/ , '' ) . replace ( /^index\.html$/i , '' ) ;
var fromHash = ( window . location . hash || '' ) . replace ( /^#/ , '' ) ;
if ( fromPath && fromPath !== '' ) {
route = fromPath ;
} else if ( fromHash ) {
route = fromHash ;
}
2026-03-28 00:21:18 -07:00
if ( ! route || route === 'home' ) { if ( currentView !== 'home' ) showHome ( ) ; return ; }
2026-02-16 03:09:53 -08:00
var parts = route . split ( '/' ) . filter ( Boolean ) ;
var decode = function ( s ) { try { return decodeURIComponent ( s ) ; } catch ( e ) { return s ; } } ;
2026-03-02 12:14:13 -08:00
if ( parts [ 0 ] === 'block' && parts [ 1 ] ) { var p1 = decode ( parts [ 1 ] ) ; var key = 'block:' + p1 ; if ( currentDetailKey === key ) return ; currentDetailKey = key ; setTimeout ( function ( ) { showBlockDetail ( p1 ) ; } , 0 ) ; return ; }
if ( parts [ 0 ] === 'tx' && parts [ 1 ] ) { var p1 = decode ( parts [ 1 ] ) ; var txKey = 'tx:' + ( p1 && typeof p1 === 'string' ? p1 . toLowerCase ( ) : String ( p1 ) ) ; if ( currentDetailKey === txKey ) return ; currentDetailKey = txKey ; setTimeout ( function ( ) { showTransactionDetail ( p1 ) ; } , 0 ) ; return ; }
if ( parts [ 0 ] === 'address' && parts [ 1 ] ) { var p1 = decode ( parts [ 1 ] ) ; var addrKey = 'address:' + ( p1 && typeof p1 === 'string' ? p1 . toLowerCase ( ) : String ( p1 ) ) ; if ( currentDetailKey === addrKey ) return ; currentDetailKey = addrKey ; setTimeout ( function ( ) { showAddressDetail ( p1 ) ; } , 0 ) ; return ; }
if ( parts [ 0 ] === 'token' && parts [ 1 ] ) { var p1 = decode ( parts [ 1 ] ) ; var tokKey = 'token:' + ( p1 && typeof p1 === 'string' ? p1 . toLowerCase ( ) : String ( p1 ) ) ; if ( currentDetailKey === tokKey ) return ; currentDetailKey = tokKey ; setTimeout ( function ( ) { showTokenDetail ( p1 ) ; } , 0 ) ; return ; }
if ( parts [ 0 ] === 'nft' && parts [ 1 ] && parts [ 2 ] ) { var p1 = decode ( parts [ 1 ] ) , p2 = decode ( parts [ 2 ] ) ; var nftKey = 'nft:' + ( p1 && typeof p1 === 'string' ? p1 . toLowerCase ( ) : String ( p1 ) ) + ':' + p2 ; if ( currentDetailKey === nftKey ) return ; currentDetailKey = nftKey ; setTimeout ( function ( ) { showNftDetail ( p1 , p2 ) ; } , 0 ) ; return ; }
2026-02-16 03:09:53 -08:00
if ( parts [ 0 ] === 'home' ) { if ( currentView !== 'home' ) showHome ( ) ; return ; }
if ( parts [ 0 ] === 'blocks' ) { if ( currentView !== 'blocks' ) showBlocks ( ) ; return ; }
if ( parts [ 0 ] === 'transactions' ) { if ( currentView !== 'transactions' ) showTransactions ( ) ; return ; }
2026-03-27 12:02:36 -07:00
if ( parts [ 0 ] === 'addresses' ) { if ( currentView !== 'addresses' ) showAddresses ( ) ; return ; }
2026-02-16 03:09:53 -08:00
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 ; }
2026-03-27 12:02:36 -07:00
if ( parts [ 0 ] === 'pools' ) { if ( currentView !== 'pools' ) openPoolsView ( ) ; return ; }
2026-03-28 15:04:42 -07:00
if ( parts [ 0 ] === 'routes' ) { if ( currentView !== 'routes' ) showRoutes ( ) ; return ; }
2026-03-27 12:02:36 -07:00
if ( parts [ 0 ] === 'liquidity' ) { if ( currentView !== 'liquidity' ) showLiquidityAccess ( ) ; return ; }
2026-03-24 18:11:08 -07:00
if ( parts [ 0 ] === 'more' ) { if ( currentView !== 'more' ) showMore ( ) ; return ; }
2026-02-16 03:09:53 -08:00
if ( parts [ 0 ] === 'tokens' ) { if ( typeof showTokensList === 'function' ) showTokensList ( ) ; else focusSearchWithHint ( 'token' ) ; return ; }
if ( parts [ 0 ] === 'analytics' ) { if ( currentView !== 'analytics' ) showAnalytics ( ) ; return ; }
if ( parts [ 0 ] === 'operator' ) { if ( currentView !== 'operator' ) showOperator ( ) ; return ; }
}
window . applyHashRoute = applyHashRoute ;
var hasRouteOnReady = window . location . hash || ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . replace ( /\/$/ , '' ) ) ;
if ( document . readyState !== 'loading' && hasRouteOnReady ) { applyHashRoute ( ) ; }
window . toggleDarkMode = toggleDarkMode ;
function focusSearchWithHint ( kind ) {
2026-03-27 12:02:36 -07:00
var prefill = kind === 'token' ? 'token' : '' ;
if ( kind === 'token' ) {
showToast ( 'Enter token contract address (0x...) or search by name/symbol' , 'info' , 4000 ) ;
2026-02-16 03:09:53 -08:00
}
2026-03-27 12:02:36 -07:00
openSmartSearchModal ( prefill ) ;
2026-02-16 03:09:53 -08:00
}
window . focusSearchWithHint = focusSearchWithHint ;
function toggleNavMenu ( ) {
var links = document . getElementById ( 'navLinks' ) ;
var btn = document . getElementById ( 'navToggle' ) ;
var icon = document . getElementById ( 'navToggleIcon' ) ;
if ( ! links || ! btn ) return ;
var isOpen = links . classList . toggle ( 'nav-open' ) ;
btn . setAttribute ( 'aria-expanded' , isOpen ? 'true' : 'false' ) ;
if ( icon ) icon . className = isOpen ? 'fas fa-times' : 'fas fa-bars' ;
}
function closeNavMenu ( ) {
var links = document . getElementById ( 'navLinks' ) ;
var btn = document . getElementById ( 'navToggle' ) ;
var icon = document . getElementById ( 'navToggleIcon' ) ;
if ( links ) links . classList . remove ( 'nav-open' ) ;
if ( btn ) btn . setAttribute ( 'aria-expanded' , 'false' ) ;
if ( icon ) icon . className = 'fas fa-bars' ;
closeAllNavDropdowns ( ) ;
}
function closeAllNavDropdowns ( ) {
document . querySelectorAll ( '.nav-dropdown.open' ) . forEach ( function ( el ) {
el . classList . remove ( 'open' ) ;
var trigger = el . querySelector ( '.nav-dropdown-trigger' ) ;
if ( trigger ) trigger . setAttribute ( 'aria-expanded' , 'false' ) ;
} ) ;
}
function initNavDropdowns ( ) {
document . querySelectorAll ( '.nav-dropdown-trigger' ) . forEach ( function ( trigger ) {
trigger . addEventListener ( 'click' , function ( e ) {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
var dropdown = trigger . closest ( '.nav-dropdown' ) ;
var wasOpen = dropdown && dropdown . classList . contains ( 'open' ) ;
closeAllNavDropdowns ( ) ;
if ( dropdown && ! wasOpen ) {
dropdown . classList . add ( 'open' ) ;
trigger . setAttribute ( 'aria-expanded' , 'true' ) ;
}
} ) ;
} ) ;
document . addEventListener ( 'click' , function ( ) {
closeAllNavDropdowns ( ) ;
} ) ;
document . getElementById ( 'navLinks' ) . addEventListener ( 'click' , function ( e ) {
if ( e . target . closest ( '.nav-dropdown-menu' ) ) e . stopPropagation ( ) ;
} ) ;
}
window . toggleNavMenu = toggleNavMenu ;
window . closeNavMenu = closeNavMenu ;
window . closeAllNavDropdowns = closeAllNavDropdowns ;
// Update breadcrumb navigation
function updateBreadcrumb ( type , identifier , identifierExtra ) {
let breadcrumbContainer ;
2026-03-28 00:21:18 -07:00
let breadcrumbHTML = '<a href="/">Home</a>' ;
2026-02-16 03:09:53 -08:00
switch ( type ) {
case 'block' :
breadcrumbContainer = document . getElementById ( 'blockDetailBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<a href="/blocks">Blocks</a>' ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">Block #' + escapeHtml ( String ( identifier ) ) + '</span>' ;
break ;
case 'transaction' :
breadcrumbContainer = document . getElementById ( 'transactionDetailBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<a href="/transactions">Transactions</a>' ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">' + escapeHtml ( shortenHash ( identifier ) ) + '</span>' ;
break ;
case 'address' :
breadcrumbContainer = document . getElementById ( 'addressDetailBreadcrumb' ) ;
2026-03-28 00:21:18 -07:00
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><a href="/addresses">Addresses</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">' + escapeHtml ( shortenHash ( identifier ) ) + '</span>' ;
2026-02-16 03:09:53 -08:00
break ;
case 'token' :
breadcrumbContainer = document . getElementById ( 'tokenDetailBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<a href="/tokens">Tokens</a>' ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">Token ' + escapeHtml ( shortenHash ( identifier ) ) + '</span>' ;
break ;
2026-03-24 18:11:08 -07:00
case 'pools' :
breadcrumbContainer = document . getElementById ( 'poolsBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">Pools</span>' ;
break ;
2026-03-28 15:04:42 -07:00
case 'routes' :
breadcrumbContainer = document . getElementById ( 'routesBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">Routes</span>' ;
break ;
2026-03-24 18:11:08 -07:00
case 'more' :
breadcrumbContainer = document . getElementById ( 'moreBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">More</span>' ;
break ;
2026-02-16 03:09:53 -08:00
case 'nft' :
breadcrumbContainer = document . getElementById ( 'nftDetailBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<a href="/address/' + encodeURIComponent ( identifier ) + '">' + escapeHtml ( shortenHash ( identifier ) ) + '</a>' ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">Token ID ' + ( identifierExtra != null ? escapeHtml ( String ( identifierExtra ) ) : '' ) + '</span>' ;
break ;
default :
return ;
}
if ( breadcrumbContainer ) {
breadcrumbContainer . innerHTML = breadcrumbHTML ;
}
}
// Retry logic with exponential backoff
async function fetchAPIWithRetry ( url , maxRetries = FETCH _MAX _RETRIES , retryDelay = RETRY _DELAY _MS ) {
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
return await fetchAPI ( url ) ;
} catch ( error ) {
const isLastAttempt = attempt === maxRetries - 1 ;
const isRetryable = error . name === 'AbortError' ||
( error . message && ( error . message . includes ( 'timeout' ) ||
error . message . includes ( '500' ) ||
error . message . includes ( '502' ) ||
error . message . includes ( '503' ) ||
error . message . includes ( '504' ) ||
error . message . includes ( 'NetworkError' ) ) ) ;
if ( isLastAttempt || ! isRetryable ) {
throw error ;
}
// Exponential backoff: 1s, 2s, 4s
const delay = retryDelay * Math . pow ( 2 , attempt ) ;
console . warn ( ` ⚠️ API call failed (attempt ${ attempt + 1 } / ${ maxRetries } ), retrying in ${ delay } ms... ` , error . message ) ;
await new Promise ( resolve => setTimeout ( resolve , delay ) ) ;
}
}
}
async function fetchAPI ( url ) {
const controller = new AbortController ( ) ;
const timeoutId = setTimeout ( ( ) => controller . abort ( ) , FETCH _TIMEOUT _MS ) ;
try {
const response = await fetch ( url , {
method : 'GET' ,
headers : { 'Accept' : 'application/json' } ,
credentials : 'omit' ,
signal : controller . signal
} ) ;
clearTimeout ( timeoutId ) ;
if ( ! response . ok ) {
let errorText = '' ;
try {
errorText = await response . text ( ) ;
} catch ( e ) {
errorText = response . statusText ;
}
// Log detailed error for debugging
const errorInfo = {
status : response . status ,
statusText : response . statusText ,
errorText : errorText . substring ( 0 , 500 ) ,
url : url ,
headers : Object . fromEntries ( response . headers . entries ( ) )
} ;
console . error ( ` ❌ API Error: ` , errorInfo ) ;
2026-02-22 15:35:45 -08:00
if ( response . status === 502 || response . status === 503 ) {
showAPIUnavailableBanner ( response . status ) ;
}
2026-02-16 03:09:53 -08:00
// For 400 errors, provide more context
if ( response . status === 400 ) {
console . error ( '🔍 HTTP 400 Bad Request Details:' ) ;
console . error ( 'URL:' , url ) ;
console . error ( 'Response Headers:' , errorInfo . headers ) ;
console . error ( 'Error Body:' , errorText ) ;
console . error ( 'Possible causes:' ) ;
console . error ( '1. Invalid query parameters' ) ;
console . error ( '2. Missing required parameters' ) ;
console . error ( '3. API endpoint format incorrect' ) ;
console . error ( '4. CORS preflight failed' ) ;
console . error ( '5. Request method not allowed' ) ;
// Try to parse error if it's JSON
try {
const errorJson = JSON . parse ( errorText ) ;
console . error ( 'Parsed Error:' , errorJson ) ;
} catch ( e ) {
// Not JSON, that's fine
}
}
throw new Error ( ` HTTP ${ response . status } : ${ errorText || response . statusText } ` ) ;
}
const contentType = response . headers . get ( 'content-type' ) ;
if ( contentType && contentType . includes ( 'application/json' ) ) {
return await response . json ( ) ;
}
const text = await response . text ( ) ;
try {
return JSON . parse ( text ) ;
} catch ( e ) {
throw new Error ( ` Invalid JSON response: ${ text . substring ( 0 , 100 ) } ` ) ;
}
} catch ( error ) {
clearTimeout ( timeoutId ) ;
if ( error . name === 'AbortError' ) {
throw new Error ( 'Request timeout. Please try again.' ) ;
}
throw error ;
}
}
2026-03-27 13:37:53 -07:00
async function postJSON ( url , payload ) {
const controller = new AbortController ( ) ;
const timeoutId = setTimeout ( ( ) => controller . abort ( ) , FETCH _TIMEOUT _MS * 2 ) ;
try {
const response = await fetch ( url , {
method : 'POST' ,
headers : {
'Accept' : 'application/json' ,
'Content-Type' : 'application/json'
} ,
credentials : 'omit' ,
signal : controller . signal ,
body : JSON . stringify ( payload || { } )
} ) ;
clearTimeout ( timeoutId ) ;
const text = await response . text ( ) ;
let parsed = { } ;
if ( text ) {
try {
parsed = JSON . parse ( text ) ;
} catch ( e ) {
parsed = { reply : text } ;
}
}
if ( ! response . ok ) {
var message = ( parsed && parsed . error && ( parsed . error . message || parsed . error . code ) ) || text || response . statusText || 'Request failed' ;
throw new Error ( 'HTTP ' + response . status + ': ' + message ) ;
}
return parsed ;
} catch ( error ) {
clearTimeout ( timeoutId ) ;
if ( error . name === 'AbortError' ) {
throw new Error ( 'Request timeout. Please try again.' ) ;
}
throw error ;
}
}
2026-02-16 03:09:53 -08:00
async function loadStats ( ) {
const statsGrid = document . getElementById ( 'statsGrid' ) ;
if ( ! statsGrid ) return ;
// Show skeleton loader
statsGrid . innerHTML = createSkeletonLoader ( 'stats' ) ;
try {
let stats ;
// For ChainID 138, use Blockscout API
if ( CHAIN _ID === 138 ) {
// Blockscout doesn't have a single stats endpoint, so we'll fetch from our API
// or use Blockscout's individual endpoints
try {
// Try our API first
stats = await fetchAPIWithRetry ( ` ${ API _BASE } /v2/stats ` ) ;
} catch ( e ) {
// Fallback: fetch from Blockscout and calculate
const [ blocksRes , txsRes ] = await Promise . all ( [
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/blocks?page=1&page_size=1 ` ) . catch ( ( ) => null ) ,
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions?page=1&page_size=1 ` ) . catch ( ( ) => null )
] ) ;
stats = {
total _blocks : blocksRes ? . items ? . [ 0 ] ? . number || 0 ,
total _transactions : txsRes ? . items ? . length ? 'N/A' : 0 ,
total _addresses : 0
} ;
}
} else {
// For other networks, use v2 API
stats = await fetchAPIWithRetry ( ` ${ API _BASE } /v2/stats ` ) ;
}
2026-03-27 12:02:36 -07:00
const activeBridgeContracts = getActiveBridgeContractCount ( ) ;
2026-02-16 03:09:53 -08:00
statsGrid . innerHTML = `
< div class = "stat-card" >
< div class = "stat-label" > Total Blocks < / d i v >
< div class = "stat-value" > $ { formatNumber ( stats . total _blocks || 0 ) } < / d i v >
< / d i v >
< div class = "stat-card" >
< div class = "stat-label" > Total Transactions < / d i v >
< div class = "stat-value" > $ { formatNumber ( stats . total _transactions || 0 ) } < / d i v >
< / d i v >
< div class = "stat-card" >
< div class = "stat-label" > Total Addresses < / d i v >
< div class = "stat-value" > $ { formatNumber ( stats . total _addresses || 0 ) } < / d i v >
< / d i v >
< div class = "stat-card bridge-card" >
< div class = "stat-label" > Bridge Contracts < / d i v >
2026-03-27 12:02:36 -07:00
< div class = "stat-value bridge-value" > $ { activeBridgeContracts } Active < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "stat-card" id = "networkStatCard" >
< div class = "stat-label" > Network < / d i v >
< div class = "stat-value" id = "networkStatValue" style = "font-size: 1rem;" > Loading ... < / d i v >
< / d i v >
` ;
if ( CHAIN _ID === 138 ) loadGasAndNetworkStats ( ) ;
} catch ( error ) {
console . error ( 'Failed to load stats:' , error ) ;
statsGrid . innerHTML = `
< div class = "stat-card" >
< div class = "stat-label" > Total Blocks < / d i v >
< div class = "stat-value" > - < / d i v >
< / d i v >
< div class = "stat-card" >
< div class = "stat-label" > Total Transactions < / d i v >
< div class = "stat-value" > - < / d i v >
< / d i v >
< div class = "stat-card" >
< div class = "stat-label" > Total Addresses < / d i v >
< div class = "stat-value" > - < / d i v >
< / d i v >
< div class = "stat-card bridge-card" >
< div class = "stat-label" > Bridge Contracts < / d i v >
2026-03-27 12:02:36 -07:00
< div class = "stat-value bridge-value" > $ { activeBridgeContracts } Active < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "stat-card" > < div class = "stat-label" > Network < / d i v > < d i v c l a s s = " s t a t - v a l u e " s t y l e = " f o n t - s i z e : 1 r e m ; " > - < / d i v > < / d i v >
` ;
}
}
async function loadGasAndNetworkStats ( ) {
var el = document . getElementById ( 'networkStatValue' ) ;
var gasCard = document . getElementById ( 'gasNetworkCard' ) ;
2026-03-27 12:02:36 -07:00
var summaryEl = document . getElementById ( 'gasNetworkSummary' ) ;
2026-02-16 03:09:53 -08:00
try {
var blocksResp = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/blocks?page=1&page_size=20' ) ;
var blocks = blocksResp . items || blocksResp || [ ] ;
var statsResp = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/stats' ) . catch ( function ( ) { return { } ; } ) ;
var gasGwei = '-' ;
var blockTimeSec = '-' ;
var tps = '-' ;
if ( blocks . length > 0 ) {
var b = blocks [ 0 ] ;
var baseFee = b . base _fee _per _gas || b . base _fee ;
if ( baseFee != null ) gasGwei = ( Number ( baseFee ) / 1e9 ) . toFixed ( 2 ) + ' Gwei' ;
}
if ( blocks . length >= 2 ) {
var t0 = blocks [ 0 ] . timestamp ;
var t1 = blocks [ 1 ] . timestamp ;
if ( t0 && t1 ) {
var d = ( new Date ( t0 ) - new Date ( t1 ) ) / 1000 ;
if ( d > 0 ) blockTimeSec = d . toFixed ( 1 ) + 's' ;
}
}
if ( statsResp . average _block _time != null ) blockTimeSec = Number ( statsResp . average _block _time ) . toFixed ( 1 ) + 's' ;
if ( statsResp . transactions _per _second != null ) tps = Number ( statsResp . transactions _per _second ) . toFixed ( 2 ) ;
if ( el ) el . innerHTML = ( gasGwei !== '-' ? 'Gas: ' + escapeHtml ( gasGwei ) + '<br/>' : '' ) + ( blockTimeSec !== '-' ? 'Block: ' + escapeHtml ( blockTimeSec ) + '<br/>' : '' ) + ( tps !== '-' ? 'TPS: ' + escapeHtml ( tps ) : '' ) || 'Gas / TPS' ;
2026-03-27 12:02:36 -07:00
if ( summaryEl ) {
summaryEl . textContent = 'Live chain health for Chain 138 updated ' + new Date ( ) . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } ) + '.' ;
}
if ( gasCard && CHAIN _ID === 138 ) {
2026-02-16 03:09:53 -08:00
var curEl = document . getElementById ( 'gasCurrentValue' ) ;
var tpsEl = document . getElementById ( 'gasTpsValue' ) ;
var btEl = document . getElementById ( 'gasBlockTimeValue' ) ;
var failedEl = document . getElementById ( 'gasFailedRateValue' ) ;
var barsEl = document . getElementById ( 'gasHistoryBars' ) ;
if ( curEl ) curEl . textContent = gasGwei !== '-' ? gasGwei : '—' ;
if ( tpsEl ) tpsEl . textContent = tps !== '-' ? tps : '—' ;
if ( btEl ) btEl . textContent = blockTimeSec !== '-' ? blockTimeSec : '—' ;
if ( failedEl ) {
var txsResp = await fetch ( BLOCKSCOUT _API + '/v2/transactions?page=1&page_size=100' ) . then ( function ( r ) { return r . json ( ) ; } ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
var txs = txsResp . items || txsResp || [ ] ;
var failed = txs . filter ( function ( t ) { var s = t . status ; return s === 0 || s === '0' || ( t . block && t . block . success === false ) ; } ) . length ;
failedEl . textContent = txs . length > 0 ? ( 100 * failed / txs . length ) . toFixed ( 2 ) + '%' : '—' ;
}
if ( barsEl ) {
var recent = blocks . slice ( 0 , 10 ) ;
var fees = recent . map ( function ( bl ) { var f = bl . base _fee _per _gas || bl . base _fee ; return f != null ? Number ( f ) / 1e9 : 0 ; } ) ;
var maxFee = Math . max . apply ( null , fees ) || 1 ;
barsEl . innerHTML = fees . map ( function ( g , i ) {
var pct = maxFee > 0 ? ( g / maxFee * 100 ) : 0 ;
2026-03-27 12:02:36 -07:00
return '<span title="Block ' + ( recent [ i ] && ( recent [ i ] . height != null ? recent [ i ] . height : recent [ i ] . number ) || '' ) + ': ' + g . toFixed ( 2 ) + ' Gwei" style="width: 8px; height: ' + ( pct + 5 ) + '%; min-height: 4px; background: var(--primary); border-radius: 4px 4px 0 0;"></span>' ;
2026-02-16 03:09:53 -08:00
} ) . join ( '' ) ;
}
}
} catch ( e ) {
if ( el ) el . textContent = '-' ;
2026-03-27 12:02:36 -07:00
if ( summaryEl ) summaryEl . textContent = 'Live chain health unavailable right now.' ;
2026-02-16 03:09:53 -08:00
}
}
function normalizeBlockDisplay ( block ) {
var blockNum = block . number ;
var hash = block . hash || '' ;
var txCount = block . transaction _count || 0 ;
var date = null ;
if ( block . timestamp ) {
if ( typeof block . timestamp === 'string' && block . timestamp . indexOf ( '0x' ) === 0 ) {
date = new Date ( parseInt ( block . timestamp , 16 ) * 1000 ) ;
} else {
date = new Date ( block . timestamp ) ;
}
}
var timestampFormatted = date && ! isNaN ( date . getTime ( ) ) ? date . toLocaleString ( ) : 'N/A' ;
var timeAgo = date && ! isNaN ( date . getTime ( ) ) ? getTimeAgo ( date ) : 'N/A' ;
return { blockNum : blockNum , hash : hash , txCount : txCount , timestampFormatted : timestampFormatted , timeAgo : timeAgo } ;
}
function createBlockCardHtml ( block , options ) {
options = options || { } ;
var d = normalizeBlockDisplay ( block ) ;
var animationClass = options . animationClass || '' ;
return '<div class="block-card ' + escapeHtml ( animationClass ) + '" onclick="showBlockDetail(\'' + escapeHtml ( String ( d . blockNum ) ) + '\')">' +
'<div class="block-number">#' + escapeHtml ( String ( d . blockNum ) ) + '</div>' +
'<div class="block-hash">' + escapeHtml ( shortenHash ( d . hash ) ) + '</div>' +
'<div class="block-info">' +
'<div class="block-info-item"><span class="block-info-label">Transactions</span><span class="block-info-value">' + escapeHtml ( String ( d . txCount ) ) + '</span></div>' +
'<div class="block-info-item"><span class="block-info-label">Time</span><span class="block-info-value">' + escapeHtml ( d . timeAgo ) + '</span></div>' +
'</div></div>' ;
}
// Prevent multiple simultaneous calls
let loadingBlocks = false ;
async function loadLatestBlocks ( ) {
const container = document . getElementById ( 'latestBlocks' ) ;
if ( ! container ) return ;
// Prevent multiple simultaneous calls
if ( loadingBlocks ) {
console . log ( 'loadLatestBlocks already in progress, skipping...' ) ;
return ;
}
loadingBlocks = true ;
try {
let blocks = [ ] ;
// For ChainID 138, use Blockscout API
if ( CHAIN _ID === 138 ) {
try {
const blockscoutUrl = ` ${ BLOCKSCOUT _API } /v2/blocks?page=1&page_size=10 ` ;
console . log ( 'Fetching blocks from Blockscout:' , blockscoutUrl ) ;
const response = await fetchAPIWithRetry ( blockscoutUrl ) ;
const raw = ( response && ( response . items || response . data || response . blocks ) ) || ( Array . isArray ( response ) ? response : null ) ;
if ( raw && Array . isArray ( raw ) ) {
blocks = raw . slice ( 0 , 10 ) . map ( normalizeBlock ) . filter ( b => b !== null ) ;
console . log ( ` ✅ Loaded ${ blocks . length } blocks from Blockscout ` ) ;
} else if ( response && typeof response === 'object' ) {
blocks = [ ] ;
console . warn ( 'Blockscout blocks response empty or unexpected shape:' , Object . keys ( response || { } ) ) ;
}
} catch ( blockscoutError ) {
console . warn ( 'Blockscout API failed, trying RPC fallback:' , blockscoutError . message ) ;
try {
const blockNumHex = await rpcCall ( 'eth_blockNumber' ) ;
const latestBlock = parseInt ( blockNumHex , 16 ) ;
if ( ! isNaN ( latestBlock ) && latestBlock >= 0 ) {
for ( let i = 0 ; i < Math . min ( 10 , latestBlock + 1 ) ; i ++ ) {
const bn = latestBlock - i ;
const b = await rpcCall ( 'eth_getBlockByNumber' , [ '0x' + bn . toString ( 16 ) , false ] ) ;
if ( b && b . number ) blocks . push ( { number : parseInt ( b . number , 16 ) , hash : b . hash , timestamp : b . timestamp ? ( typeof b . timestamp === 'string' ? parseInt ( b . timestamp , 16 ) * 1000 : b . timestamp ) : null , transaction _count : b . transactions ? b . transactions . length : 0 } ) ;
}
if ( blocks . length > 0 ) console . log ( 'Loaded ' + blocks . length + ' blocks via RPC fallback' ) ;
}
} catch ( rpcErr ) {
console . error ( 'RPC fallback also failed:' , rpcErr ) ;
if ( container ) {
container . innerHTML = '<div class="error">API temporarily unavailable. ' + escapeHtml ( ( blockscoutError . message || 'Unknown error' ) . substring ( 0 , 150 ) ) + ' <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>' ;
}
return ;
}
if ( blocks . length === 0 && container ) {
container . innerHTML = '<div class="error">Could not load blocks. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>' ;
return ;
}
}
} else {
// For other networks, use Etherscan-compatible API
const blockData = await fetchAPI ( ` ${ API _BASE } ?module=block&action=eth_block_number ` ) ;
if ( ! blockData || ! blockData . result ) {
throw new Error ( 'Invalid response from API' ) ;
}
const latestBlock = parseInt ( blockData . result , 16 ) ;
if ( isNaN ( latestBlock ) || latestBlock < 0 ) {
throw new Error ( 'Invalid block number' ) ;
}
// Fetch blocks one by one
for ( let i = 0 ; i < 10 && latestBlock - i >= 0 ; i ++ ) {
const blockNum = latestBlock - i ;
try {
const block = await fetchAPI ( ` ${ API _BASE } ?module=block&action=eth_get_block_by_number&tag=0x ${ blockNum . toString ( 16 ) } &boolean=false ` ) ;
if ( block && block . result ) {
blocks . push ( {
number : blockNum ,
hash : block . result . hash ,
timestamp : block . result . timestamp ,
transaction _count : block . result . transactions ? block . result . transactions . length : 0
} ) ;
}
} catch ( e ) {
console . warn ( ` Failed to load block ${ blockNum } : ` , e ) ;
}
}
}
const limitedBlocks = blocks . slice ( 0 , 10 ) ;
2026-03-24 18:11:08 -07:00
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()' ) ;
2026-02-16 03:09:53 -08:00
if ( limitedBlocks . length === 0 ) {
2026-03-24 18:11:08 -07:00
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>' ;
2026-02-16 03:09:53 -08:00
} else {
// Create HTML with duplicated blocks for seamless infinite loop
2026-03-24 18:11:08 -07:00
let html = filterBar + '<div class="blocks-scroll-container" id="blocksScrollContainer">' ;
2026-02-16 03:09:53 -08:00
html += '<div class="blocks-scroll-content">' ;
// First set of blocks (with animations for first 3)
2026-03-24 18:11:08 -07:00
filteredBlocks . forEach ( function ( block , index ) {
2026-02-16 03:09:53 -08:00
var animationClass = index < 3 ? 'new-block' : '' ;
html += createBlockCardHtml ( block , { animationClass : animationClass } ) ;
} ) ;
// Duplicate blocks for seamless infinite loop
2026-03-24 18:11:08 -07:00
filteredBlocks . forEach ( function ( block ) {
2026-02-16 03:09:53 -08:00
html += createBlockCardHtml ( block , { } ) ;
} ) ;
html += '</div></div>' ;
if ( container ) container . innerHTML = html ;
// Setup auto-scroll animation
const scrollContainer = document . getElementById ( 'blocksScrollContainer' ) ;
const scrollContent = scrollContainer ? . querySelector ( '.blocks-scroll-content' ) ;
if ( scrollContainer && scrollContent ) {
const cardWidth = 200 + 16 ; // card width (200px) + gap (16px = 1rem)
2026-03-24 18:11:08 -07:00
const singleSetWidth = filteredBlocks . length * cardWidth ;
2026-02-16 03:09:53 -08:00
// Use CSS transform for smooth animation
let scrollPosition = 0 ;
let isPaused = false ;
const scrollSpeed = 0.4 ; // pixels per frame (adjust for speed)
scrollContainer . addEventListener ( 'mouseenter' , ( ) => {
isPaused = true ;
} ) ;
scrollContainer . addEventListener ( 'mouseleave' , ( ) => {
isPaused = false ;
} ) ;
if ( _blocksScrollAnimationId != null ) {
cancelAnimationFrame ( _blocksScrollAnimationId ) ;
_blocksScrollAnimationId = null ;
}
function animateScroll ( ) {
if ( ! isPaused && scrollContent ) {
scrollPosition += scrollSpeed ;
if ( scrollPosition >= singleSetWidth ) scrollPosition = 0 ;
scrollContent . style . transform = 'translateX(-' + scrollPosition + 'px)' ;
}
_blocksScrollAnimationId = requestAnimationFrame ( animateScroll ) ;
}
setTimeout ( function ( ) {
_blocksScrollAnimationId = requestAnimationFrame ( animateScroll ) ;
} , 500 ) ;
}
// Remove animation classes after animation completes
setTimeout ( ( ) => {
container . querySelectorAll ( '.new-block' ) . forEach ( card => {
card . classList . remove ( 'new-block' ) ;
} ) ;
} , 1000 ) ;
}
} catch ( error ) {
console . error ( 'Failed to load latest blocks:' , error ) ;
if ( container ) container . innerHTML = '<div class="error">Failed to load blocks: ' + escapeHtml ( ( error . message || 'Unknown error' ) . substring ( 0 , 200 ) ) + '. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>' ;
} finally {
loadingBlocks = false ;
}
}
// Store previous transaction hashes for real-time updates
let previousTransactionHashes = new Set ( ) ;
let transactionUpdateInterval = null ;
async function loadLatestTransactions ( ) {
const container = document . getElementById ( 'latestTransactions' ) ;
if ( ! container ) return ;
try {
let response ;
let rawTransactions = [ ] ;
// For ChainID 138, use Blockscout API
if ( CHAIN _ID === 138 ) {
try {
2026-02-22 15:35:45 -08:00
response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions?page=1&page_size=10 ` ) ;
2026-02-16 03:09:53 -08:00
rawTransactions = Array . isArray ( response ? . items ) ? response . items : ( Array . isArray ( response ? . data ) ? response . data : [ ] ) ;
} catch ( apiErr ) {
console . warn ( 'Blockscout transactions API failed, trying RPC fallback:' , apiErr . message ) ;
try {
const blockNumHex = await rpcCall ( 'eth_blockNumber' ) ;
const latest = parseInt ( blockNumHex , 16 ) ;
for ( let i = 0 ; i < Math . min ( 5 , latest + 1 ) && rawTransactions . length < 10 ; i ++ ) {
const b = await rpcCall ( 'eth_getBlockByNumber' , [ '0x' + ( latest - i ) . toString ( 16 ) , true ] ) ;
if ( b && b . transactions ) {
b . transactions . forEach ( tx => {
if ( typeof tx === 'object' && rawTransactions . length < 10 ) rawTransactions . push ( { hash : tx . hash , from : tx . from , to : tx . to || null , value : tx . value || '0x0' , block _number : parseInt ( b . number , 16 ) , created _at : b . timestamp ? new Date ( parseInt ( b . timestamp , 16 ) * 1000 ) . toISOString ( ) : null } ) ;
} ) ;
}
}
response = { items : rawTransactions } ;
} catch ( rpcErr ) {
console . error ( 'RPC transactions fallback failed:' , rpcErr ) ;
if ( container ) container . innerHTML = '<div class="error">API temporarily unavailable. <button type="button" class="btn btn-primary" onclick="loadLatestTransactions()" style="margin-top: 0.5rem;">Retry</button></div>' ;
return ;
}
}
} else {
response = await fetchAPIWithRetry ( ` ${ API _BASE } /v2/transactions?page=1&page_size=10 ` ) ;
rawTransactions = Array . isArray ( response ? . items ) ? response . items : ( Array . isArray ( response ? . data ) ? response . data : [ ] ) ;
}
// Normalize transactions using adapter
const transactions = rawTransactions . map ( normalizeTransaction ) . filter ( tx => tx !== null ) ;
// Limit to 10 transactions
const limitedTransactions = transactions . slice ( 0 , 10 ) ;
2026-03-24 18:11:08 -07:00
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()' ) ;
2026-02-16 03:09:53 -08:00
// Check for new transactions
2026-03-24 18:11:08 -07:00
const currentHashes = new Set ( filteredTransactions . map ( tx => String ( tx . hash || '' ) ) ) ;
const newTransactions = filteredTransactions . filter ( tx => ! previousTransactionHashes . has ( String ( tx . hash || '' ) ) ) ;
2026-02-16 03:09:53 -08:00
// Update previous hashes
previousTransactionHashes = currentHashes ;
// Show skeleton loader only on first load
if ( container . innerHTML . includes ( 'skeleton' ) || container . innerHTML . includes ( 'Loading' ) ) {
container . innerHTML = createSkeletonLoader ( 'table' ) ;
}
2026-03-24 18:11:08 -07:00
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>' ;
2026-02-16 03:09:53 -08:00
if ( limitedTransactions . length === 0 ) {
html += '<tr><td colspan="5" style="text-align: center; padding: 1rem;">No transactions found</td></tr>' ;
2026-03-24 18:11:08 -07:00
} else if ( filteredTransactions . length === 0 ) {
html += '<tr><td colspan="5" style="text-align: center; padding: 1rem;">No transactions match the current filter.</td></tr>' ;
2026-02-16 03:09:53 -08:00
} else {
2026-03-24 18:11:08 -07:00
filteredTransactions . forEach ( ( tx , index ) => {
2026-02-16 03:09:53 -08:00
// Transaction is already normalized by adapter
const hash = String ( tx . hash || 'N/A' ) ;
const from = String ( tx . from || 'N/A' ) ;
const to = String ( tx . to || 'N/A' ) ;
const value = tx . value || '0' ;
const blockNumber = tx . block _number || 'N/A' ;
const valueFormatted = formatEther ( value ) ;
// Add animation class for new transactions
const isNew = newTransactions . some ( ntx => String ( ntx . hash || '' ) === hash ) ;
const animationClass = isNew ? 'new-transaction' : '' ;
2026-03-02 12:14:13 -08:00
var fromClick = safeAddress ( from ) ? ' onclick="event.stopPropagation(); showAddressDetail(\'' + escapeHtml ( from ) + '\')" style="cursor: pointer;"' : '' ;
var toClick = safeAddress ( to ) ? ' onclick="event.stopPropagation(); showAddressDetail(\'' + escapeHtml ( to ) + '\')" style="cursor: pointer;"' : '' ;
html += '<tr class="' + animationClass + '" onclick="showTransactionDetail(\'' + escapeHtml ( hash ) + '\')" style="cursor: pointer;"><td class="hash">' + escapeHtml ( shortenHash ( hash ) ) + '</td><td class="hash"' + fromClick + '>' + formatAddressWithLabel ( from ) + '</td><td class="hash"' + toClick + '>' + ( to ? formatAddressWithLabel ( to ) : '-' ) + '</td><td>' + escapeHtml ( valueFormatted ) + ' ETH</td><td>' + escapeHtml ( String ( blockNumber ) ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
}
html += '</tbody></table>' ;
if ( container ) container . innerHTML = html ;
if ( container ) {
setTimeout ( ( ) => {
container . querySelectorAll ( '.new-transaction' ) . forEach ( row => {
row . classList . remove ( 'new-transaction' ) ;
} ) ;
} , 500 ) ;
}
} catch ( error ) {
console . error ( 'Failed to load latest transactions:' , error ) ;
if ( container ) container . innerHTML = '<div class="error">Failed to load transactions: ' + escapeHtml ( ( error . message || 'Unknown error' ) . substring ( 0 , 200 ) ) + '. <button type="button" class="btn btn-primary" onclick="loadLatestTransactions()" style="margin-top: 0.5rem;">Retry</button></div>' ;
}
}
// Real-time transaction updates
function startTransactionUpdates ( ) {
// Clear any existing interval
if ( transactionUpdateInterval ) {
clearInterval ( transactionUpdateInterval ) ;
}
// Update transactions every 5 seconds
transactionUpdateInterval = setInterval ( ( ) => {
if ( currentView === 'home' ) {
loadLatestTransactions ( ) ;
}
} , 5000 ) ;
}
function stopTransactionUpdates ( ) {
if ( transactionUpdateInterval ) {
clearInterval ( transactionUpdateInterval ) ;
transactionUpdateInterval = null ;
}
}
var blocksListPage = 1 ;
var transactionsListPage = 1 ;
2026-03-27 12:02:36 -07:00
var addressesListPage = 1 ;
2026-02-16 03:09:53 -08:00
const LIST _PAGE _SIZE = 25 ;
async function loadAllBlocks ( page ) {
if ( page != null ) blocksListPage = Math . max ( 1 , parseInt ( page , 10 ) || 1 ) ;
const container = document . getElementById ( 'blocksList' ) ;
if ( ! container ) { return ; }
try {
container . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading blocks...</div>' ;
let blocks = [ ] ;
if ( CHAIN _ID === 138 ) {
2026-03-28 13:59:00 -07:00
blocks = await fetchChain138BlocksPage ( blocksListPage , LIST _PAGE _SIZE ) ;
2026-02-16 03:09:53 -08:00
} else {
// For other networks, use Etherscan-compatible API
const blockData = await fetchAPI ( ` ${ API _BASE } ?module=block&action=eth_block_number ` ) ;
const latestBlock = parseInt ( blockData . result , 16 ) ;
// Load last 50 blocks
for ( let i = 0 ; i < 50 && latestBlock - i >= 0 ; i ++ ) {
const blockNum = latestBlock - i ;
try {
const block = await fetchAPI ( ` ${ API _BASE } ?module=block&action=eth_get_block_by_number&tag=0x ${ blockNum . toString ( 16 ) } &boolean=false ` ) ;
if ( block . result ) {
blocks . push ( {
number : blockNum ,
hash : block . result . hash ,
timestamp : new Date ( parseInt ( block . result . timestamp , 16 ) * 1000 ) . toISOString ( ) ,
transaction _count : block . result . transactions ? block . result . transactions . length : 0
} ) ;
}
} catch ( e ) {
// Skip failed blocks
}
}
}
2026-03-24 18:11:08 -07:00
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>' ;
2026-02-16 03:09:53 -08:00
if ( blocks . length === 0 ) {
html += '<tr><td colspan="4" style="text-align: center; padding: 2rem;">No blocks found</td></tr>' ;
2026-03-24 18:11:08 -07:00
} else if ( filteredBlocks . length === 0 ) {
html += '<tr><td colspan="4" style="text-align: center; padding: 2rem;">No blocks match the current filter</td></tr>' ;
2026-02-16 03:09:53 -08:00
} else {
2026-03-24 18:11:08 -07:00
filteredBlocks . forEach ( function ( block ) {
2026-02-16 03:09:53 -08:00
var d = normalizeBlockDisplay ( block ) ;
2026-03-28 13:59:00 -07:00
var blockNumber = escapeHtml ( String ( d . blockNum ) ) ;
var blockHref = '/block/' + encodeURIComponent ( String ( d . blockNum ) ) ;
var blockLink = '<a href="' + blockHref + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + blockNumber + '\')" style="color: inherit; text-decoration: none; font-weight: 600;">' + blockNumber + '</a>' ;
var hashLink = safeBlockNumber ( d . blockNum ) ? '<a class="hash" href="' + blockHref + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + blockNumber + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml ( shortenHash ( d . hash ) ) + '</a>' : '<span class="hash">' + escapeHtml ( shortenHash ( d . hash ) ) + '</span>' ;
html += '<tr onclick="showBlockDetail(\'' + blockNumber + '\')" style="cursor: pointer;"><td>' + blockLink + '</td><td>' + hashLink + '</td><td>' + escapeHtml ( String ( d . txCount ) ) + '</td><td>' + escapeHtml ( d . timestampFormatted ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
}
var pagination = '<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; flex-wrap: wrap; gap: 0.5rem;">' ;
pagination += '<span style="color: var(--text-light);">Page ' + blocksListPage + '</span>' ;
pagination += '<div style="display: flex; gap: 0.5rem;"><button type="button" class="btn btn-secondary" ' + ( blocksListPage <= 1 ? 'disabled' : '' ) + ' onclick="loadAllBlocks(' + ( blocksListPage - 1 ) + ')">Prev</button><button type="button" class="btn btn-secondary" ' + ( blocks . length < LIST _PAGE _SIZE ? 'disabled' : '' ) + ' onclick="loadAllBlocks(' + ( blocksListPage + 1 ) + ')">Next</button></div></div>' ;
html += '</tbody></table>' + pagination ;
container . innerHTML = html ;
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load blocks: ' + escapeHtml ( error . message || 'Unknown error' ) + '. <button onclick="showBlocks()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>' ;
}
}
async function loadAllTransactions ( page ) {
if ( page != null ) transactionsListPage = Math . max ( 1 , parseInt ( page , 10 ) || 1 ) ;
const container = document . getElementById ( 'transactionsList' ) ;
if ( ! container ) { return ; }
try {
container . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading transactions...</div>' ;
let transactions = [ ] ;
if ( CHAIN _ID === 138 ) {
2026-03-28 13:59:00 -07:00
transactions = await fetchChain138TransactionsPage ( transactionsListPage , LIST _PAGE _SIZE ) ;
2026-02-16 03:09:53 -08:00
} else {
// For other networks, use Etherscan-compatible API
const blockData = await fetchAPI ( ` ${ API _BASE } ?module=block&action=eth_block_number ` ) ;
if ( ! blockData || ! blockData . result ) {
throw new Error ( 'Failed to get latest block number' ) ;
}
const latestBlock = parseInt ( blockData . result , 16 ) ;
if ( isNaN ( latestBlock ) || latestBlock < 0 ) {
throw new Error ( 'Invalid block number' ) ;
}
const maxTxs = 50 ;
// Get transactions from recent blocks
for ( let blockNum = latestBlock ; blockNum >= 0 && transactions . length < maxTxs ; blockNum -- ) {
try {
const block = await fetchAPI ( ` ${ API _BASE } ?module=block&action=eth_get_block_by_number&tag=0x ${ blockNum . toString ( 16 ) } &boolean=true ` ) ;
if ( block . result && block . result . transactions ) {
for ( const tx of block . result . transactions ) {
if ( transactions . length >= maxTxs ) break ;
if ( typeof tx === 'object' ) {
transactions . push ( {
hash : tx . hash ,
from : tx . from ,
to : tx . to ,
value : tx . value || '0' ,
block _number : blockNum
} ) ;
}
}
}
} catch ( e ) {
// Skip failed blocks
}
}
}
2026-03-24 18:11:08 -07:00
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>' ;
2026-02-16 03:09:53 -08:00
if ( transactions . length === 0 ) {
html += '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No transactions found</td></tr>' ;
2026-03-24 18:11:08 -07:00
} else if ( filteredTransactions . length === 0 ) {
html += '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No transactions match the current filter</td></tr>' ;
2026-02-16 03:09:53 -08:00
} else {
2026-03-24 18:11:08 -07:00
filteredTransactions . forEach ( tx => {
2026-02-16 03:09:53 -08:00
const hash = String ( tx . hash || 'N/A' ) ;
const from = String ( tx . from || 'N/A' ) ;
const to = String ( tx . to || 'N/A' ) ;
const value = tx . value || '0' ;
const blockNumber = tx . block _number || 'N/A' ;
const valueFormatted = formatEther ( value ) ;
2026-03-28 13:59:00 -07:00
var safeHash = escapeHtml ( hash ) ;
var txHref = '/tx/' + encodeURIComponent ( hash ) ;
var hashLink = '<a class="hash" href="' + txHref + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + safeHash + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml ( shortenHash ( hash ) ) + '</a>' ;
var fromLink = safeAddress ( from ) ? '<a class="hash" href="/address/' + encodeURIComponent ( from ) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml ( from ) + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel ( from ) + '</a>' : formatAddressWithLabel ( from ) ;
var toLink = safeAddress ( to ) ? '<a class="hash" href="/address/' + encodeURIComponent ( to ) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml ( to || '' ) + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel ( to ) + '</a>' : ( to ? formatAddressWithLabel ( to ) : '-' ) ;
var blockLink = safeBlockNumber ( blockNumber ) ? '<a href="/block/' + encodeURIComponent ( String ( blockNumber ) ) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeHtml ( String ( blockNumber ) ) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml ( String ( blockNumber ) ) + '</a>' : escapeHtml ( String ( blockNumber ) ) ;
html += '<tr onclick="showTransactionDetail(\'' + safeHash + '\')" style="cursor: pointer;"><td>' + hashLink + '</td><td>' + fromLink + '</td><td>' + toLink + '</td><td>' + escapeHtml ( valueFormatted ) + ' ETH</td><td>' + blockLink + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
}
var pagination = '<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; flex-wrap: wrap; gap: 0.5rem;">' ;
pagination += '<span style="color: var(--text-light);">Page ' + transactionsListPage + '</span>' ;
pagination += '<div style="display: flex; gap: 0.5rem;"><button type="button" class="btn btn-secondary" ' + ( transactionsListPage <= 1 ? 'disabled' : '' ) + ' onclick="loadAllTransactions(' + ( transactionsListPage - 1 ) + ')">Prev</button><button type="button" class="btn btn-secondary" ' + ( transactions . length < LIST _PAGE _SIZE ? 'disabled' : '' ) + ' onclick="loadAllTransactions(' + ( transactionsListPage + 1 ) + ')">Next</button></div></div>' ;
html += '</tbody></table>' + pagination ;
container . innerHTML = html ;
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load transactions: ' + escapeHtml ( error . message || 'Unknown error' ) + '. <button onclick="showTransactions()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>' ;
}
}
2026-03-27 12:02:36 -07:00
async function loadAllAddresses ( page ) {
if ( page != null ) addressesListPage = Math . max ( 1 , parseInt ( page , 10 ) || 1 ) ;
const container = document . getElementById ( 'addressesList' ) ;
if ( ! container ) { return ; }
try {
container . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading addresses...</div>' ;
const params = new URLSearchParams ( {
page : String ( addressesListPage ) ,
page _size : String ( LIST _PAGE _SIZE )
} ) ;
const q = getExplorerPageFilter ( 'addressesList' ) ;
if ( q ) params . set ( 'q' , q ) ;
let addresses = [ ] ;
if ( CHAIN _ID === 138 ) {
try {
const response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/addresses? ${ params . toString ( ) } ` ) ;
const items = response && ( response . items || response . data ) ;
if ( Array . isArray ( items ) && items . length > 0 ) {
addresses = items . map ( normalizeAddress ) . filter ( function ( addr ) { return addr !== null ; } ) ;
}
} catch ( blockscoutError ) {
console . warn ( 'Blockscout address list failed, trying explorer API:' , blockscoutError . message || blockscoutError ) ;
}
}
if ( addresses . length === 0 ) {
const response = await fetchAPIWithRetry ( ` ${ API _BASE } /v1/addresses? ${ params . toString ( ) } ` ) ;
const items = response && ( response . data || response . items || [ ] ) ;
addresses = Array . isArray ( items ) ? items . map ( normalizeAddress ) . filter ( function ( addr ) { return addr !== null ; } ) : [ ] ;
}
const filter = getExplorerPageFilter ( 'addressesList' ) ;
const filteredAddresses = filter ? addresses . filter ( function ( item ) {
return matchesExplorerFilter ( [
item . address ,
item . label || '' ,
item . is _contract ? 'contract' : 'externally owned' ,
item . tx _sent ,
item . tx _received ,
item . transaction _count ,
item . token _count ,
item . first _seen _at || '' ,
item . last _seen _at || ''
] . join ( ' ' ) , filter ) ;
} ) : addresses ;
const filterBar = renderPageFilterBar ( 'addressesList' , 'Filter addresses by address, label, type, or activity...' , 'Filters the indexed address list below.' , 'loadAllAddresses(' + addressesListPage + ')' ) ;
let html = filterBar + '<table class="table"><thead><tr><th>Address</th><th>Label</th><th>Type</th><th>Tx Sent</th><th>Tx Received</th><th>Tokens</th><th>Last Seen</th></tr></thead><tbody>' ;
if ( addresses . length === 0 ) {
html += '<tr><td colspan="7" style="text-align: center; padding: 2rem;">No addresses found</td></tr>' ;
} else if ( filteredAddresses . length === 0 ) {
html += '<tr><td colspan="7" style="text-align: center; padding: 2rem;">No addresses match the current filter</td></tr>' ;
} else {
filteredAddresses . forEach ( function ( item ) {
var addr = String ( item . address || '' ) ;
var label = String ( item . label || '' ) ;
var isContract = ! ! item . is _contract ;
var type = isContract ? 'Contract' : 'EOA' ;
var txSent = Number ( item . tx _sent || 0 ) ;
var txReceived = Number ( item . tx _received || 0 ) ;
var tokenCount = Number ( item . token _count || 0 ) ;
var lastSeen = String ( item . last _seen _at || '—' ) ;
html += '<tr style="cursor: pointer;" onclick="showAddressDetail(\'' + escapeHtml ( addr ) + '\')">' ;
2026-03-28 13:59:00 -07:00
html += '<td><a class="hash" href="/address/' + encodeURIComponent ( addr ) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml ( addr ) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml ( shortenHash ( addr ) ) + '</a></td>' ;
2026-03-27 12:02:36 -07:00
html += '<td>' + escapeHtml ( label || '—' ) + '</td>' ;
html += '<td>' + escapeHtml ( type ) + '</td>' ;
html += '<td>' + escapeHtml ( String ( txSent ) ) + '</td>' ;
html += '<td>' + escapeHtml ( String ( txReceived ) ) + '</td>' ;
html += '<td>' + escapeHtml ( String ( tokenCount ) ) + '</td>' ;
html += '<td>' + escapeHtml ( lastSeen ) + '</td>' ;
html += '</tr>' ;
} ) ;
}
var pagination = '<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 1rem; flex-wrap: wrap; gap: 0.5rem;">' ;
pagination += '<span style="color: var(--text-light);">Page ' + addressesListPage + '</span>' ;
pagination += '<div style="display: flex; gap: 0.5rem;"><button type="button" class="btn btn-secondary" ' + ( addressesListPage <= 1 ? 'disabled' : '' ) + ' onclick="loadAllAddresses(' + ( addressesListPage - 1 ) + ')">Prev</button><button type="button" class="btn btn-secondary" ' + ( addresses . length < LIST _PAGE _SIZE ? 'disabled' : '' ) + ' onclick="loadAllAddresses(' + ( addressesListPage + 1 ) + ')">Next</button></div></div>' ;
html += '</tbody></table>' + pagination ;
container . innerHTML = html ;
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load addresses: ' + escapeHtml ( error . message || 'Unknown error' ) + '. <button onclick="showAddresses()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>' ;
}
}
window . _loadAllAddresses = loadAllAddresses ;
window . loadAllAddresses = loadAllAddresses ;
2026-02-16 03:09:53 -08:00
async function loadTokensList ( ) {
var container = document . getElementById ( 'tokensListContent' ) ;
if ( ! container ) return ;
try {
container . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading tokens...</div>' ;
if ( CHAIN _ID === 138 ) {
try {
var resp = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/tokens?page=1&page_size=100' ) . catch ( function ( ) { return null ; } ) ;
var items = ( resp && ( resp . items || resp . data ) ) || ( Array . isArray ( resp ) ? resp : null ) ;
if ( items && items . length > 0 ) {
2026-02-22 15:35:45 -08:00
var knownTokens = {
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' : { name : 'Wrapped Ether' , symbol : 'WETH' } ,
'0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f' : { name : 'Wrapped Ether v10' , symbol : 'WETH' }
} ;
2026-03-24 18:11:08 -07:00
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 ) {
2026-02-16 03:09:53 -08:00
var addr = ( t . address && ( t . address . hash || t . address ) ) || t . address _hash || t . token _address || t . contract _address _hash || '' ;
2026-02-22 15:35:45 -08:00
var known = addr ? knownTokens [ addr . toLowerCase ( ) ] : null ;
var name = ( known && known . name ) || t . name || t . symbol || ( known && known . symbol ) || '-' ;
var symbolDisplay = ( known && known . symbol ) || t . symbol ;
var symbol = ( symbolDisplay || 'TOKEN' ) . replace ( /'/g , "\\'" ) ;
var decimals = t . decimals != null ? Number ( t . decimals ) : 18 ;
2026-02-16 03:09:53 -08:00
var type = t . type || 'ERC-20' ;
if ( ! addr ) return ;
2026-02-22 15:35:45 -08:00
var addrEsc = escapeHtml ( addr ) . replace ( /'/g , "\\'" ) ;
2026-03-02 12:14:13 -08:00
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>' ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredItems . length === 0 ) {
html += '<tr><td colspan="4" style="text-align: center; padding: 1.5rem;">No tokens match the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
html += '</tbody></table>' ;
container . innerHTML = html ;
return ;
}
} catch ( e ) { }
}
2026-03-24 18:11:08 -07:00
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>' ;
2026-02-16 03:09:53 -08:00
} catch ( err ) {
2026-03-24 18:11:08 -07:00
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>' ;
2026-02-16 03:09:53 -08:00
}
}
window . _loadTokensList = loadTokensList ;
2026-03-27 12:02:36 -07:00
function normalizeRouteStatus ( status ) {
return status || 'unavailable' ;
}
function renderRouteMetric ( label , value ) {
return '<div style="display:flex; justify-content:space-between; gap:0.75rem; padding:0.35rem 0; border-bottom:1px solid var(--border);"><span style="color:var(--text-light);">' + escapeHtml ( label ) + '</span><strong>' + escapeHtml ( value ) + '</strong></div>' ;
}
function renderRouteNode ( node , depthLevel ) {
var indent = Math . max ( 0 , depthLevel || 0 ) * 1.05 ;
var status = normalizeRouteStatus ( node . status ) ;
var statusColor = status === 'live' ? '#16a34a' : status === 'partial' ? '#f59e0b' : status === 'stale' ? '#d97706' : '#dc2626' ;
var html = '<div style="margin-left:' + indent . toFixed ( 2 ) + 'rem; padding:1rem; border:1px solid var(--border); border-left:4px solid ' + statusColor + '; border-radius:12px; background:var(--light);">' ;
html += '<div style="display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:0.75rem; margin-bottom:0.75rem;">' ;
html += '<div>' ;
html += '<div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--text-light);">' + escapeHtml ( node . kind || 'route' ) + '</div>' ;
html += '<div style="font-size:1rem; font-weight:700; margin-top:0.1rem;">' + escapeHtml ( node . label || 'Untitled route' ) + '</div>' ;
html += '</div>' ;
html += '<span class="badge" style="background:' + statusColor + '; color:#fff; text-transform:uppercase;">' + escapeHtml ( status ) + '</span>' ;
html += '</div>' ;
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:0.5rem; margin-bottom:0.75rem;">' ;
html += renderRouteMetric ( 'Chain' , ( node . chainName || 'Chain' ) + ' (' + ( node . chainId || '-' ) + ')' ) ;
if ( node . dexType ) html += renderRouteMetric ( 'DEX' , node . dexType ) ;
if ( node . poolAddress ) html += renderRouteMetric ( 'Pool' , shortenHash ( node . poolAddress ) ) ;
if ( node . depth ) {
html += renderRouteMetric ( 'TVL' , '$' + Number ( node . depth . tvlUsd || 0 ) . toFixed ( 2 ) ) ;
html += renderRouteMetric ( 'Capacity' , '$' + Number ( node . depth . estimatedTradeCapacityUsd || 0 ) . toFixed ( 2 ) ) ;
html += renderRouteMetric ( 'Freshness' , node . depth . freshnessSeconds == null ? 'n/a' : node . depth . freshnessSeconds + 's' ) ;
}
html += '</div>' ;
if ( node . notes && node . notes . length ) {
html += '<ul style="margin:0 0 0.75rem 1.1rem; color:var(--text-light);">' ;
node . notes . forEach ( function ( note ) {
html += '<li>' + escapeHtml ( note ) + '</li>' ;
} ) ;
html += '</ul>' ;
}
if ( node . children && node . children . length ) {
html += '<div style="display:grid; gap:0.75rem; margin-top:0.75rem; padding-left:0.35rem; border-left:1px dashed var(--border);">' ;
node . children . forEach ( function ( child ) {
html += renderRouteNode ( child , ( depthLevel || 0 ) + 1 ) ;
} ) ;
html += '</div>' ;
}
html += '</div>' ;
return html ;
}
function renderMissingQuotePools ( missingPools ) {
if ( ! missingPools || ! missingPools . length ) {
return '<div style="color:var(--text-light);">No quote-token metadata gaps detected in the current indexed pool set.</div>' ;
}
var html = '<div style="overflow-x:auto;"><table class="table"><thead><tr><th>Pool</th><th>Chain</th><th>Token 0</th><th>Token 1</th><th>Reason</th></tr></thead><tbody>' ;
missingPools . forEach ( function ( pool ) {
html += '<tr>' ;
html += '<td class="hash">' + escapeHtml ( shortenHash ( pool . poolAddress ) ) + '</td>' ;
html += '<td>' + escapeHtml ( String ( pool . chainId ) ) + '</td>' ;
html += '<td>' + escapeHtml ( ( pool . token0Symbol || '' ) + ' ' + shortenHash ( pool . token0Address ) ) + '</td>' ;
html += '<td>' + escapeHtml ( ( pool . token1Symbol || '' ) + ' ' + shortenHash ( pool . token1Address ) ) + '</td>' ;
html += '<td>' + escapeHtml ( pool . reason || 'Missing quote token metadata' ) + '</td>' ;
html += '</tr>' ;
} ) ;
html += '</tbody></table></div>' ;
return html ;
}
function routeTreeQueryParams ( query ) {
var params = new URLSearchParams ( {
chainId : '138' ,
tokenIn : query . tokenIn ,
amountIn : query . amountIn || '1000000'
} ) ;
if ( query . tokenOut ) params . set ( 'tokenOut' , query . tokenOut ) ;
if ( query . destinationChainId != null ) params . set ( 'destinationChainId' , String ( query . destinationChainId ) ) ;
return params . toString ( ) ;
}
async function fetchRouteTree ( query ) {
var response = await fetchAPIWithRetry ( ` ${ TOKEN _AGGREGATION _API _BASE } /v1/routes/tree? ${ routeTreeQueryParams ( query ) } ` ) ;
if ( ( ! response || response . decision === 'unresolved' || ! Array . isArray ( response . tree ) || response . tree . length === 0 ) &&
Number ( query . destinationChainId || query . chainId || 138 ) === 138 &&
safeAddress ( query . tokenOut ) ) {
try {
var ctx = await fetchCurrentPmmContext ( ) ;
var liveFallback = await buildLiveDirectRouteFallback ( query , ctx ) ;
if ( liveFallback ) response = liveFallback ;
} catch ( e ) { }
}
return { query : query , response : response } ;
}
function uniqueMissingQuotePools ( results ) {
var map = { } ;
results . forEach ( function ( entry ) {
var pools = ( entry . response && entry . response . missingQuoteTokenPools ) || [ ] ;
pools . forEach ( function ( pool ) {
var key = String ( pool . chainId ) + ':' + String ( pool . poolAddress || '' ) . toLowerCase ( ) ;
if ( ! map [ key ] ) map [ key ] = pool ;
} ) ;
} ) ;
return Object . keys ( map ) . sort ( ) . map ( function ( key ) { return map [ key ] ; } ) ;
}
function renderRouteSweepSummary ( results ) {
2026-03-27 18:52:03 -07:00
var html = '<div style="overflow-x:auto;"><table class="table"><thead><tr><th>Probe</th><th>Direct Pools</th><th>Missing Quote Pools</th><th>Decision</th><th>Freshest Status</th></tr></thead><tbody>' ;
2026-03-27 12:02:36 -07:00
results . forEach ( function ( entry ) {
var response = entry . response || { } ;
var pools = Array . isArray ( response . pools ) ? response . pools : [ ] ;
var missing = Array . isArray ( response . missingQuoteTokenPools ) ? response . missingQuoteTokenPools : [ ] ;
var freshest = pools . length ? pools [ 0 ] . depth && pools [ 0 ] . depth . status ? pools [ 0 ] . depth . status : 'unknown' : 'none' ;
html += '<tr>' ;
2026-03-27 18:52:03 -07:00
html += '<td>' + escapeHtml ( ( entry . query . pairLabel || entry . query . title || entry . query . symbol || 'probe' ) + ' ' + shortenHash ( entry . query . tokenIn ) ) + '</td>' ;
2026-03-27 12:02:36 -07:00
html += '<td>' + escapeHtml ( String ( pools . length ) ) + '</td>' ;
html += '<td>' + escapeHtml ( String ( missing . length ) ) + '</td>' ;
html += '<td>' + escapeHtml ( response . decision || 'unresolved' ) + '</td>' ;
html += '<td>' + escapeHtml ( freshest ) + '</td>' ;
html += '</tr>' ;
} ) ;
html += '</tbody></table></div>' ;
return html ;
}
function renderPriorityRouteCard ( entry ) {
var response = entry . response || { } ;
var rootNodes = Array . isArray ( response . tree ) ? response . tree : [ ] ;
var decisionLabel = escapeHtml ( response . decision || 'unresolved' ) ;
if ( response . decision === 'bridge-only' && response . destination && Number ( response . destination . chainId ) === 1 ) {
decisionLabel = 'bridge-only (destination completion on Mainnet)' ;
}
var html = '<div class="card" style="margin:0; box-shadow:none;">' ;
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">' ;
html += '<div>' ;
html += '<h3 class="card-title" style="margin-bottom:0.3rem;"><i class="fas fa-diagram-project"></i> ' + escapeHtml ( entry . query . title ) + '</h3>' ;
html += '<div style="color:var(--text-light); font-size:0.9rem;">Decision: <strong>' + decisionLabel + '</strong> | Generated: ' + escapeHtml ( ( response . generatedAt || '' ) . replace ( 'T' , ' ' ) . replace ( 'Z' , ' UTC' ) ) + '</div>' ;
html += '</div>' ;
html += '<button type="button" class="btn btn-secondary" onclick="loadLiveRouteTrees()" aria-label="Refresh live route tree"><i class="fas fa-sync-alt"></i> Refresh</button>' ;
html += '</div>' ;
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem; margin-bottom:1rem;">' ;
if ( response . source ) {
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);">' ;
html += '<div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Source</div>' ;
html += '<div style="font-weight:700;">' + escapeHtml ( response . source . chainName || 'Chain 138' ) + '</div>' ;
html += '<div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">' + escapeHtml ( response . source . tokenIn ? ( response . source . tokenIn . symbol + ' ' + shortenHash ( response . source . tokenIn . address ) ) : entry . query . title ) + '</div>' ;
html += '</div>' ;
}
if ( response . destination ) {
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);">' ;
html += '<div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Destination</div>' ;
html += '<div style="font-weight:700;">' + escapeHtml ( response . destination . chainName || 'Destination' ) + '</div>' ;
html += '<div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Chain ' + escapeHtml ( String ( response . destination . chainId || '-' ) ) + '</div>' ;
html += '</div>' ;
}
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);">' ;
html += '<div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Coverage</div>' ;
html += '<div style="font-weight:700;">' + escapeHtml ( String ( ( response . pools || [ ] ) . length ) ) + ' direct pool(s)</div>' ;
html += '<div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">' + escapeHtml ( String ( ( response . missingQuoteTokenPools || [ ] ) . length ) ) + ' missing quote-token pool(s)</div>' ;
html += '</div>' ;
html += '</div>' ;
if ( rootNodes . length === 0 ) {
html += '<div style="color:var(--text-light);">No live route nodes available for this query yet.</div>' ;
} else {
rootNodes . slice ( 0 , 4 ) . forEach ( function ( node ) {
html += renderRouteNode ( node , 0 ) ;
} ) ;
}
html += '</div>' ;
return html ;
}
function updatePoolsMissingQuoteBadge ( count ) {
var badge = document . getElementById ( 'poolsMissingQuoteBadge' ) ;
if ( ! badge ) return ;
var n = Math . max ( 0 , Number ( count || 0 ) ) ;
badge . textContent = 'Missing quote routes ' + String ( n ) ;
badge . style . display = n > 0 ? 'inline-block' : 'none' ;
}
2026-03-28 15:04:42 -07:00
async function loadLiveRouteTrees ( targetId ) {
var containerId = targetId || ( currentView === 'routes' ? 'routesRouteTreeContent' : 'poolRouteTreeContent' ) ;
var container = document . getElementById ( containerId ) ;
2026-03-27 12:02:36 -07:00
if ( ! container ) return ;
container . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading live route tree...</div>' ;
try {
var ctx = await fetchCurrentPmmContext ( ) ;
var priorityQueries = buildRoutePriorityQueries ( ctx ) ;
2026-03-27 18:52:03 -07:00
var sweepQueries = buildRouteSweepQueries ( ctx ) ;
2026-03-27 12:02:36 -07:00
var priorityResults = await Promise . allSettled ( priorityQueries . map ( fetchRouteTree ) ) ;
2026-03-27 18:52:03 -07:00
var sweepResults = await Promise . allSettled ( sweepQueries . map ( fetchRouteTree ) ) ;
2026-03-27 12:02:36 -07:00
var priorityOkResults = priorityResults . filter ( function ( result ) { return result . status === 'fulfilled' ; } ) . map ( function ( result ) { return result . value ; } ) ;
var priorityErrors = priorityResults . filter ( function ( result ) { return result . status === 'rejected' ; } ) ;
var sweepOkResults = sweepResults . filter ( function ( result ) { return result . status === 'fulfilled' ; } ) . map ( function ( result ) { return result . value ; } ) ;
var allSweepMissing = uniqueMissingQuotePools ( sweepOkResults ) ;
var html = '<div style="display:grid; gap:1rem;">' ;
html += '<div class="card" style="margin:0; box-shadow:none;">' ;
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">' ;
html += '<h3 class="card-title" style="margin-bottom:0;"><i class="fas fa-signal"></i> Route Coverage Sweep</h3>' ;
html += '<button type="button" class="btn btn-secondary" onclick="loadLiveRouteTrees()" aria-label="Refresh live route tree"><i class="fas fa-sync-alt"></i> Refresh all routes</button>' ;
html += '</div>' ;
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:0.75rem; margin-bottom:1rem;">' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Priority routes</div><div style="font-weight:700;">' + escapeHtml ( String ( priorityOkResults . length ) ) + ' ok' + ( priorityErrors . length ? ' / ' + String ( priorityErrors . length ) + ' failed' : '' ) + '</div></div>' ;
2026-03-27 18:52:03 -07:00
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Sweep probes</div><div style="font-weight:700;">' + escapeHtml ( String ( sweepOkResults . length ) ) + ' ok</div></div>' ;
2026-03-27 12:02:36 -07:00
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Missing quote routes</div><div style="font-weight:700;">' + escapeHtml ( String ( allSweepMissing . length ) ) + '</div></div>' ;
html += '</div>' ;
2026-03-27 18:52:03 -07:00
html += '<div style="color:var(--text-light); margin-bottom:0.85rem; line-height:1.5;">This sweep probes explicit local token pairs against compliant and official anchor assets on Chain 138. The priority route cards above remain the bridge-path checks; this table focuses on direct-pair coverage and quote-token metadata gaps.</div>' ;
2026-03-27 12:02:36 -07:00
html += renderRouteSweepSummary ( sweepOkResults ) ;
if ( priorityErrors . length ) {
html += '<div style="margin-top:0.75rem; color:var(--text-light); font-size:0.9rem;">Some priority route requests failed, but the pools table is still available.</div>' ;
}
html += '</div>' ;
html += '<div style="display:grid; gap:1rem;">' ;
priorityOkResults . forEach ( function ( entry ) {
html += renderPriorityRouteCard ( entry ) ;
} ) ;
html += '</div>' ;
html += '<div class="card" style="margin:0; box-shadow:none;">' ;
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">' ;
html += '<h3 class="card-title" style="margin-bottom:0;"><i class="fas fa-bug"></i> Missing Quote-Token Pools</h3>' ;
html += '</div>' ;
html += renderMissingQuotePools ( allSweepMissing ) ;
html += '</div>' ;
container . innerHTML = html ;
updatePoolsMissingQuoteBadge ( allSweepMissing . length ) ;
} catch ( err ) {
container . innerHTML = '<div class="error">Failed to load live route tree: ' + escapeHtml ( err . message || 'Unknown error' ) + '</div>' ;
updatePoolsMissingQuoteBadge ( 0 ) ;
}
}
2026-03-28 15:04:42 -07:00
function buildRoutesLandingHtml ( ) {
var html = '' ;
html += '<div class="card" style="margin:0 0 1rem 0; box-shadow:none;">' ;
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">' ;
html += '<div>' ;
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-diagram-project"></i> Live Route Decision Tree</h3>' ;
html += '<div style="color:var(--text-light); line-height:1.5;">This dedicated view follows the Chain 138 routing graph end-to-end. It keeps the live coverage sweep, direct-pair diagnostics, and bridge-path branches together in one place so route investigations do not get buried inside the pools inventory.</div>' ;
html += '</div>' ;
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">' ;
html += '<button type="button" class="btn btn-primary" onclick="loadLiveRouteTrees(\'routesRouteTreeContent\')" aria-label="Refresh live routes"><i class="fas fa-sync-alt"></i> Refresh</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showPools(); updatePath(\'/pools\')" aria-label="Open pools inventory"><i class="fas fa-water"></i> Pools inventory</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\')" aria-label="Open liquidity access"><i class="fas fa-wave-square"></i> Liquidity access</button>' ;
html += '</div>' ;
html += '</div>' ;
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.75rem;">' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Best for</div><div style="font-weight:700;">Route debugging and operator review</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Use this page when a user route, destination branch, or quote-token path looks wrong.</div></div>' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Includes</div><div style="font-weight:700;">Coverage sweep + priority route cards</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">The pools page now links here instead of embedding the full route tree inline.</div></div>' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Data source</div><div style="font-weight:700;">Live token-aggregation route tree API</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Every refresh re-reads current Chain 138 PMM and bridge state.</div></div>' ;
html += '</div>' ;
html += '</div>' ;
html += '<div id="routesRouteTreeContent"><div class="loading"><i class="fas fa-spinner"></i> Loading live route tree...</div></div>' ;
return html ;
}
function renderRoutesView ( ) {
showView ( 'routes' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'routes' ) updatePath ( '/routes' ) ;
updateBreadcrumb ( 'routes' ) ;
var container = document . getElementById ( 'routesContent' ) ;
if ( ! container ) return ;
container . innerHTML = buildRoutesLandingHtml ( ) ;
setTimeout ( function ( ) {
loadLiveRouteTrees ( 'routesRouteTreeContent' ) ;
} , 0 ) ;
_poolsRouteTreeRefreshTimer = setInterval ( function ( ) {
if ( currentView === 'routes' ) {
loadLiveRouteTrees ( 'routesRouteTreeContent' ) ;
}
} , ROUTE _TREE _REFRESH _MS ) ;
}
window . renderRoutesView = renderRoutesView ;
2026-03-27 12:02:36 -07:00
function summarizePoolRows ( rows ) {
var summary = {
liveLocal : 0 ,
externalMainnet : 0 ,
notYetCreated : 0 ,
missingCode : 0 ,
partial : 0 ,
} ;
( rows || [ ] ) . forEach ( function ( row ) {
var status = String ( ( row && row . status ) || '' ) . toLowerCase ( ) ;
if ( status . indexOf ( 'funded (live)' ) !== - 1 || status . indexOf ( 'deployed (live)' ) !== - 1 ) {
summary . liveLocal += 1 ;
return ;
}
if ( status . indexOf ( 'external / mainnet' ) !== - 1 || status . indexOf ( 'external / not on chain 138' ) !== - 1 ) {
summary . externalMainnet += 1 ;
return ;
}
if ( status . indexOf ( 'not created' ) !== - 1 ) {
summary . notYetCreated += 1 ;
return ;
}
if ( status . indexOf ( 'missing code' ) !== - 1 ) {
summary . missingCode += 1 ;
return ;
}
if ( status . indexOf ( 'partially funded' ) !== - 1 || status . indexOf ( 'created (unfunded)' ) !== - 1 ) {
summary . partial += 1 ;
}
} ) ;
return summary ;
}
function toCsv ( rows ) {
return rows . map ( function ( row ) {
return row . map ( function ( cell ) {
return '"' + String ( cell == null ? '' : cell ) . replace ( /"/g , '""' ) + '"' ;
} ) . join ( ',' ) ;
} ) . join ( '\n' ) ;
}
function exportPoolsCSV ( ) {
if ( ! latestPoolsSnapshot || ! Array . isArray ( latestPoolsSnapshot . rows ) ) {
showToast ( 'Pools data is not ready yet' , 'error' ) ;
return ;
}
var summary = latestPoolsSnapshot . summary || { } ;
var rows = latestPoolsSnapshot . rows || [ ] ;
var csvRows = [
[ 'Section' , 'Metric' , 'Value' ] ,
[ 'Summary' , 'Generated At' , latestPoolsSnapshot . generatedAt || '' ] ,
[ 'Summary' , 'Live local pools' , summary . liveLocal || 0 ] ,
[ 'Summary' , 'External Mainnet-side' , summary . externalMainnet || 0 ] ,
[ 'Summary' , 'Not yet created' , summary . notYetCreated || 0 ] ,
[ 'Summary' , 'Needs attention' , ( summary . missingCode || 0 ) + ( summary . partial || 0 ) ] ,
[ ] ,
[ 'Category' , 'Pool Pair' , 'System' , 'Address' , 'Status' , 'Notes' ]
] ;
rows . forEach ( function ( row ) {
csvRows . push ( [
row . category || '' ,
row . poolPair || '' ,
row . poolType || '' ,
row . address || '' ,
row . status || '' ,
row . notes || ''
] ) ;
} ) ;
var blob = new Blob ( [ toCsv ( csvRows ) ] , { type : 'text/csv' } ) ;
var url = URL . createObjectURL ( blob ) ;
var a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = 'pools-status.csv' ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
showToast ( 'CSV downloaded' , 'success' ) ;
}
function exportPoolsJSON ( ) {
if ( ! latestPoolsSnapshot ) {
showToast ( 'Pools data is not ready yet' , 'error' ) ;
return ;
}
var blob = new Blob ( [ JSON . stringify ( latestPoolsSnapshot , null , 2 ) ] , { type : 'application/json' } ) ;
var url = URL . createObjectURL ( blob ) ;
var a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = 'pools-status.json' ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
showToast ( 'JSON downloaded' , 'success' ) ;
}
window . exportPoolsCSV = exportPoolsCSV ;
window . exportPoolsJSON = exportPoolsJSON ;
async function renderPoolsView ( ) {
2026-03-24 18:11:08 -07:00
showView ( 'pools' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'pools' ) updatePath ( '/pools' ) ;
var container = document . getElementById ( 'poolsContent' ) ;
2026-03-27 12:02:36 -07:00
if ( _poolsRouteTreeRefreshTimer ) {
clearInterval ( _poolsRouteTreeRefreshTimer ) ;
_poolsRouteTreeRefreshTimer = null ;
}
2026-03-24 18:11:08 -07:00
if ( ! container ) return ;
try {
2026-03-27 12:02:36 -07:00
container . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading pools...</div>' ;
var live = await getLivePoolRows ( ) ;
2026-03-24 18:11:08 -07:00
var filter = getExplorerPageFilter ( 'poolsList' ) ;
2026-03-27 12:02:36 -07:00
var filterBar = renderPageFilterBar ( 'poolsList' , 'Filter by category, pair, type, status, address, or notes...' , 'Tracks live Chain 138 pool, reserve, and bridge-linked contract state.' , 'openPoolsView()' ) ;
var summary = summarizePoolRows ( live . rows ) ;
latestPoolsSnapshot = {
generatedAt : new Date ( ) . toISOString ( ) ,
summary : summary ,
rows : live . rows
} ;
var rows = live . rows . map ( function ( row ) {
2026-03-24 18:11:08 -07:00
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 ;
2026-03-27 12:02:36 -07:00
var html = filterBar + '<div style="margin-bottom: 0.15rem; color: var(--text-light); font-size: 0.92rem; line-height: 1.4;">This table is derived from live Chain 138 contract state. Pool addresses, funding status, quote-token readiness, and private-registry registrations are refreshed from the chain each time the page renders. External or mainnet-only systems are labeled explicitly.</div>' ;
html += '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap:0.75rem; margin:0.9rem 0 1rem;">' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Live local pools</div><div style="font-weight:700;">' + escapeHtml ( String ( summary . liveLocal ) ) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Funded or deployed directly on Chain 138</div></div>' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">External Mainnet-side</div><div style="font-weight:700;">' + escapeHtml ( String ( summary . externalMainnet ) ) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Expected to live off Chain 138 by design</div></div>' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Not yet created</div><div style="font-weight:700;">' + escapeHtml ( String ( summary . notYetCreated ) ) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">No live pool mapping currently registered</div></div>' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light);"><div style="font-size:0.82rem; color:var(--text-light); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:0.35rem;">Needs attention</div><div style="font-weight:700;">' + escapeHtml ( String ( summary . missingCode + summary . partial ) ) + '</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.35rem;">Missing code, partial funding, or unfunded rows</div></div>' ;
html += '</div>' ;
2026-03-24 18:11:08 -07:00
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>' ;
2026-03-28 14:09:23 -07:00
html += '<td>' + ( safeAddress ( addr ) ? explorerAddressLink ( addr , escapeHtml ( shortenHash ( addr ) ) , 'color: inherit; text-decoration: none;' ) : '<span style="color: var(--text-light);">—</span>' ) + '</td>' ;
2026-03-24 18:11:08 -07:00
html += '<td>' + escapeHtml ( row . status ) + '</td>' ;
html += '<td>' + escapeHtml ( row . notes ) + '</td>' ;
html += '</tr>' ;
} ) ;
}
html += '</tbody></table></div>' ;
2026-03-27 12:02:36 -07:00
html += '<div class="card" style="margin-top:1.2rem; box-shadow:none;">' ;
html += '<div class="card-header" style="align-items:flex-start; gap:0.75rem;">' ;
2026-03-28 15:04:42 -07:00
html += '<div>' ;
html += '<h3 class="card-title" style="margin-bottom:0.25rem;"><i class="fas fa-diagram-project"></i> Live Route Decision Tree</h3>' ;
html += '<div style="color:var(--text-light); line-height:1.5;">The full route sweep and priority route cards now live on their own dedicated page so investigations can open directly into the routing graph.</div>' ;
html += '</div>' ;
html += '<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-left:auto;">' ;
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\')" aria-label="Open dedicated routes page"><i class="fas fa-arrow-right"></i> Open routes page</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\')" aria-label="Open liquidity access page"><i class="fas fa-wave-square"></i> Liquidity</button>' ;
html += '</div>' ;
html += '</div>' ;
html += '<div style="display:grid; gap:0.75rem;">' ;
html += '<div style="padding:0.9rem; border:1px solid var(--border); border-radius:10px; background:var(--light); color:var(--text-light); line-height:1.5;">Use <strong>Routes</strong> for the live route coverage sweep, bridge-path diagnostics, and missing quote-token review. The pools table above stays focused on pool inventory and funding state.</div>' ;
html += '<div style="display:flex; flex-wrap:wrap; gap:0.6rem;">' ;
html += '<a class="btn btn-secondary" href="/routes" onclick="event.preventDefault(); showRoutes(); updatePath(\'/routes\');" style="text-decoration:none;"><i class="fas fa-diagram-project"></i> Routes</a>' ;
html += '<a class="btn btn-secondary" href="/liquidity" onclick="event.preventDefault(); showLiquidityAccess(); updatePath(\'/liquidity\');" style="text-decoration:none;"><i class="fas fa-plug"></i> Public access points</a>' ;
html += '</div>' ;
2026-03-27 12:02:36 -07:00
html += '</div>' ;
html += '</div>' ;
2026-03-24 18:11:08 -07:00
container . innerHTML = html ;
} catch ( err ) {
container . innerHTML = '<div class="error">Failed to load pools: ' + escapeHtml ( err . message || 'Unknown error' ) + '</div>' ;
}
}
2026-03-27 12:02:36 -07:00
window . renderPoolsView = renderPoolsView ;
function renderLiquidityAccessView ( ) {
showView ( 'liquidity' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'liquidity' ) updatePath ( '/liquidity' ) ;
var container = document . getElementById ( 'liquidityContent' ) ;
if ( ! container ) return ;
var publicApiBase = TOKEN _AGGREGATION _API _BASE + '/v1' ;
var livePools = [
{
pair : 'cUSDT / cUSDC' ,
poolAddress : '0xff8d3b8fDF7B112759F076B69f4271D4209C0849' ,
reserves : '10,000,000 / 10,000,000'
} ,
{
pair : 'cUSDT / USDT' ,
poolAddress : '0x6fc60DEDc92a2047062294488539992710b99D71' ,
reserves : '10,000,000 / 10,000,000'
} ,
{
pair : 'cUSDC / USDC' ,
poolAddress : '0x0309178ae30302D83c76d6Dd402a684eF3160eec' ,
reserves : '10,000,000 / 10,000,000'
} ,
{
pair : 'cUSDT / cXAUC' ,
poolAddress : '0x1AA55E2001E5651349AfF5A63FD7A7Ae44f0F1b0' ,
reserves : '2,666,965 / 519.477000'
} ,
{
pair : 'cUSDC / cXAUC' ,
poolAddress : '0xEA9Ac6357CaCB42a83b9082B870610363B177cBa' ,
reserves : '1,000,000 / 194.782554'
} ,
{
pair : 'cEURT / cXAUC' ,
poolAddress : '0xbA99bc1eAAC164569d5AcA96C806934DDaF970Cf' ,
reserves : '1,000,000 / 225.577676'
}
] ;
var endpointCards = [
{
title : 'Canonical route matrix' ,
method : 'GET' ,
href : publicApiBase + '/routes/matrix' ,
notes : 'Full live-route inventory with optional blocked and planned route visibility.'
} ,
{
title : 'Live ingestion export' ,
method : 'GET' ,
href : publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap' ,
notes : 'Flat export for adapter discovery and route ingestion.'
} ,
{
title : 'Partner payload templates' ,
method : 'GET' ,
href : publicApiBase + '/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true' ,
notes : 'Builds request templates for 1inch, 0x, and LiFi from live routes.'
} ,
{
title : 'Resolve supported partner payloads' ,
method : 'POST' ,
href : publicApiBase + '/routes/partner-payloads/resolve' ,
notes : 'Accepts partner, amount, and addresses and returns supported payloads by default.'
} ,
{
title : 'Dispatch supported partner payload' ,
method : 'POST' ,
href : publicApiBase + '/routes/partner-payloads/dispatch' ,
notes : 'Dispatches one supported partner payload when the chain is publicly supported.'
} ,
{
title : 'Internal Chain 138 execution plan' ,
method : 'POST' ,
href : publicApiBase + '/routes/internal-execution-plan' ,
notes : 'Returns the DODO PMM fallback execution plan when public partner support is unavailable.'
}
] ;
var requestExamples = [
'GET ' + publicApiBase + '/routes/matrix?includeNonLive=true' ,
'GET ' + publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap' ,
'GET ' + publicApiBase + '/routes/partner-payloads?partner=LiFi&amount=1000000&includeUnsupported=true' ,
'POST ' + publicApiBase + '/routes/partner-payloads/resolve' ,
'POST ' + publicApiBase + '/routes/internal-execution-plan'
] ;
var html = '' ;
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); gap:0.9rem; margin-bottom:1rem;">' ;
html += '<div class="stat-card"><div class="stat-label">Live public pools</div><div class="stat-value">6</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.4rem;">Verified DODO PMM pools on Chain 138.</div></div>' ;
html += '<div class="stat-card"><div class="stat-label">Public access path</div><div class="stat-value" style="font-size:1.2rem;">/token-aggregation/api/v1</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.4rem;">Explorer-hosted proxy for route and execution APIs.</div></div>' ;
html += '<div class="stat-card"><div class="stat-label">Partner status</div><div class="stat-value">Fallback Ready</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.4rem;">Templates exist for 1inch, 0x, and LiFi, but Chain 138 execution still falls back internally.</div></div>' ;
html += '</div>' ;
html += '<div class="card" style="margin-bottom:1rem; box-shadow:none;">' ;
html += '<div class="card-header"><h3 class="card-title"><i class="fas fa-water"></i> Live Pool Snapshot</h3><button type="button" class="btn btn-secondary" onclick="openPoolsView()" aria-label="Open pool operations view"><i class="fas fa-diagram-project"></i> Open pools view</button></div>' ;
html += '<div style="display:grid; gap:0.75rem;">' ;
livePools . forEach ( function ( pool ) {
html += '<div style="padding:0.95rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);">' ;
html += '<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:0.75rem; flex-wrap:wrap;">' ;
html += '<div><div style="font-size:1rem; font-weight:700;">' + escapeHtml ( pool . pair ) + '</div><div style="margin-top:0.35rem; color:var(--text-light); font-size:0.85rem;">Pool: ' + escapeHtml ( pool . poolAddress ) + '</div></div>' ;
html += '<div style="font-size:0.92rem; font-weight:600;">Reserves: ' + escapeHtml ( pool . reserves ) + '</div>' ;
html += '</div></div>' ;
} ) ;
html += '</div></div>' ;
html += '<div class="card" style="margin-bottom:1rem; box-shadow:none;">' ;
html += '<div class="card-header"><h3 class="card-title"><i class="fas fa-network-wired"></i> Route and Execution Notes</h3></div>' ;
html += '<div style="display:grid; gap:0.75rem; color:var(--text-light); line-height:1.6;">' ;
html += '<div>Direct live routes today: cUSDT ↔ cUSDC, cUSDT ↔ USDT, cUSDC ↔ USDC, cUSDT ↔ cXAUC, cUSDC ↔ cXAUC, and cEURT ↔ cXAUC.</div>' ;
html += '<div>Multi-hop public paths exist through cXAUC for cEURT ↔ cUSDT, cEURT ↔ cUSDC, and an alternate cUSDT ↔ cUSDC path.</div>' ;
html += '<div>Mainnet bridge discovery is live for cUSDT → USDT and cUSDC → USDC through the configured UniversalCCIPBridge lane.</div>' ;
html += '<div>1inch, 0x, and LiFi request templates are available through the explorer API, but those partners do not publicly support Chain 138 execution today.</div>' ;
html += '<div>When public partner execution is unavailable, the internal DODO PMM execution plan endpoint returns the Chain 138 fallback route instead of a dead end.</div>' ;
html += '</div></div>' ;
html += '<div class="card" style="margin-bottom:1rem; box-shadow:none;">' ;
html += '<div class="card-header"><h3 class="card-title"><i class="fas fa-plug"></i> Public Explorer Access Points</h3></div>' ;
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); gap:0.85rem;">' ;
endpointCards . forEach ( function ( card ) {
2026-03-28 00:21:18 -07:00
html += '<a href="' + escapeAttr ( card . href ) + '" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:16px; padding:1rem; background:var(--muted-surface);">' ;
2026-03-27 12:02:36 -07:00
html += '<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:0.75rem; margin-bottom:0.5rem;">' ;
html += '<div style="font-weight:700; line-height:1.35;">' + escapeHtml ( card . title ) + '</div>' ;
html += '<span class="badge badge-info" style="white-space:nowrap;">' + escapeHtml ( card . method ) + '</span>' ;
html += '</div>' ;
html += '<div style="font-size:0.82rem; color:var(--text-light); word-break:break-all; margin-bottom:0.5rem;">' + escapeHtml ( card . href ) + '</div>' ;
html += '<div style="font-size:0.9rem; color:var(--text-light); line-height:1.5;">' + escapeHtml ( card . notes ) + '</div>' ;
html += '</a>' ;
} ) ;
html += '</div></div>' ;
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(260px, 1fr)); gap:1rem;">' ;
html += '<div class="card" style="margin:0; box-shadow:none;"><div class="card-header"><h3 class="card-title"><i class="fas fa-terminal"></i> Quick Request Examples</h3></div><div style="display:grid; gap:0.75rem;">' ;
requestExamples . forEach ( function ( example ) {
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><code style="display:block; font-size:0.82rem; line-height:1.6; word-break:break-all;">' + escapeHtml ( example ) + '</code></div>' ;
} ) ;
html += '</div></div>' ;
html += '<div class="card" style="margin:0; box-shadow:none;"><div class="card-header"><h3 class="card-title"><i class="fas fa-compass"></i> Related Explorer Tools</h3></div><div style="display:grid; gap:0.75rem; color:var(--text-light); line-height:1.6;">' ;
2026-03-28 15:04:42 -07:00
html += '<div>Use Wallet for network onboarding and the explorer token list URL, then open Routes for live route-tree diagnostics and Pools for contract-state inventory checks.</div>' ;
2026-03-27 12:02:36 -07:00
html += '<div style="display:flex; flex-wrap:wrap; gap:0.6rem; margin-top:0.2rem;">' ;
2026-03-28 15:04:42 -07:00
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\')" aria-label="Open routes view"><i class="fas fa-diagram-project"></i> Routes view</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showPools(); updatePath(\'/pools\')" aria-label="Open pools view"><i class="fas fa-water"></i> Pools view</button>' ;
2026-03-27 12:02:36 -07:00
html += '<button type="button" class="btn btn-secondary" onclick="showWETHUtilities(); updatePath(\'/weth\')" aria-label="Open WETH tools"><i class="fas fa-coins"></i> WETH tools</button>' ;
html += '<a class="btn btn-secondary" href="/docs.html" style="text-decoration:none;"><i class="fas fa-book"></i> Explorer docs</a>' ;
html += '</div></div></div>' ;
html += '</div>' ;
container . innerHTML = html ;
}
window . renderLiquidityAccessView = renderLiquidityAccessView ;
2026-03-24 18:11:08 -07:00
2026-03-27 12:02:36 -07:00
function renderMoreView ( ) {
2026-03-24 18:11:08 -07:00
showView ( 'more' ) ;
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'more' ) updatePath ( '/more' ) ;
var container = document . getElementById ( 'moreContent' ) ;
if ( ! container ) return ;
2026-03-27 12:11:18 -07:00
var groups = [
{
key : 'tools' ,
title : 'Tools' ,
items : [
{ title : 'Input Data Decoder' , icon : 'fa-file-code' , status : 'Live' , badgeClass : 'badge-info' , desc : 'Open transaction detail pages to decode calldata, logs, and contract interactions already exposed by the explorer.' , action : 'showTransactionsList();' , href : '/transactions' } ,
{ title : 'Unit Converter' , icon : 'fa-scale-balanced' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Convert wei, gwei, ether, and common Chain 138 stablecoin units with a quick in-page helper.' , action : 'showUnitConverterModal();' , href : '/more' } ,
{ title : 'CSV Export' , icon : 'fa-file-csv' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Export pool state and route inventory snapshots for operator review and downstream ingestion.' , action : 'showPools(); updatePath(\'/pools\'); setTimeout(function(){ if (typeof exportPoolsCSV === \"function\") exportPoolsCSV(); }, 200);' , href : '/pools' } ,
{ title : 'Account Balance Checker' , icon : 'fa-wallet' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Jump into indexed addresses to inspect balances, token inventory, internal transfers, and recent activity.' , action : 'showAddresses();' , href : '/addresses' }
]
} ,
{
key : 'explore' ,
title : 'Explore' ,
items : [
2026-03-28 15:15:23 -07:00
{ title : 'Gas Tracker' , icon : 'fa-gas-pump' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Review live gas, block time, TPS, and chain health from the home network dashboard.' , action : 'showHome();' , href : '/' } ,
2026-03-28 15:04:42 -07:00
{ title : 'DEX Tracker' , icon : 'fa-chart-line' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Open liquidity discovery, PMM pool status, live route trees, and partner payload access points.' , action : 'showRoutes();' , href : '/routes' } ,
2026-03-28 15:15:23 -07:00
{ title : 'Node Tracker' , icon : 'fa-server' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Inspect bridge balances, destination configuration, and operator-facing chain references from the live bridge monitoring panel.' , action : 'showBridgeMonitoring();' , href : '/bridge' } ,
2026-03-27 12:11:18 -07:00
{ title : 'Label Cloud' , icon : 'fa-tags' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Browse labeled addresses, contracts, and address activity through the explorer address index.' , action : 'showAddresses();' , href : '/addresses' } ,
{ title : 'Domain Name Lookup' , icon : 'fa-magnifying-glass' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Use the smart search launcher to resolve ENS-style names, domains, addresses, hashes, and token symbols.' , action : 'openSmartSearchModal(\'\');' , href : '/more' }
]
} ,
{
key : 'services' ,
title : 'Services' ,
items : [
{ title : 'Token Approvals' , icon : 'fa-shield-halved' , status : 'External' , badgeClass : 'badge-warning' , desc : 'Jump to revoke.cash for wallet approval review. Address detail pages also expose approval shortcuts directly.' , action : 'openExternalMoreLink(\'https://revoke.cash/\');' , href : '#' } ,
{ title : 'Verified Signature' , icon : 'fa-signature' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Use wallet sign-in and verified address flows already built into the explorer authentication surfaces.' , action : 'showWalletModal();' , href : '/more' } ,
{ title : 'Input Data Messages' , icon : 'fa-message' , status : 'Live' , badgeClass : 'badge-info' , desc : 'Transaction detail pages already surface decoded input data, event logs, and contract interaction context.' , action : 'showTransactionsList();' , href : '/transactions' } ,
{ title : 'Advanced Filter' , icon : 'fa-filter' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Block, transaction, address, token, pool, bridge, and watchlist screens all support focused page-level filtering.' , action : 'showTransactionsList();' , href : '/transactions' } ,
2026-03-28 15:15:23 -07:00
{ title : 'MetaMask Snap' , icon : 'fa-wallet' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Open the Chain 138 MetaMask Snap companion for network setup, token list access, and wallet integration guidance.' , action : 'window.location.href=\'/snap/\';' , href : '/snap/' }
2026-03-27 12:11:18 -07:00
]
}
2026-03-24 18:11:08 -07:00
] ;
2026-03-27 12:11:18 -07:00
var html = '<div style="display:grid; grid-template-columns:minmax(240px, 0.9fr) repeat(3, minmax(220px, 1fr)); gap:1rem; align-items:start;">' ;
html += '<div style="border:1px solid var(--border); border-radius:18px; padding:1.25rem; background:linear-gradient(180deg, rgba(59,130,246,0.08), rgba(15,23,42,0.02)); min-height:100%;">' ;
html += '<div style="font-size:1.25rem; font-weight:800; margin-bottom:0.75rem;">Tools & Services</div>' ;
html += '<div style="color:var(--text-light); line-height:1.7; margin-bottom:1rem;">Discover more of SolaceScanScout's explorer tools in one place, grouped the way users expect from Etherscan-style explorers.</div>' ;
html += '<div style="display:grid; gap:0.75rem;">' ;
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Now live</div><div style="font-weight:700;">Route matrix, ingestion APIs, smart search, pool exports, and live Mainnet stable bridge discovery.</div></div>' ;
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Good entry points</div><div style="display:flex; flex-wrap:wrap; gap:0.5rem;">' ;
2026-03-28 15:15:23 -07:00
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\');"><i class="fas fa-diagram-project"></i> Routes</button>' ;
2026-03-27 12:11:18 -07:00
html += '<button type="button" class="btn btn-primary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\');"><i class="fas fa-wave-square"></i> Liquidity</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="openSmartSearchModal(\'\');"><i class="fas fa-magnifying-glass"></i> Search</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showAddresses(); updatePath(\'/addresses\');"><i class="fas fa-address-book"></i> Addresses</button>' ;
html += '</div></div>' ;
html += '</div></div>' ;
groups . forEach ( function ( group ) {
html += '<div style="border:1px solid var(--border); border-radius:18px; padding:1.1rem; background:var(--light); min-height:100%;">' ;
html += '<div style="font-size:1.1rem; font-weight:800; margin-bottom:0.85rem;">' + escapeHtml ( group . title ) + '</div>' ;
html += '<div style="display:grid; gap:0.7rem;">' ;
group . items . forEach ( function ( item ) {
var disabled = ! ! item . disabled ;
var disabledTitle = String ( item . title ) + ' is not exposed in the explorer yet.' ;
var onclick = disabled
? ( 'event.preventDefault(); showToast(' + JSON . stringify ( disabledTitle ) + ', "info");' )
: ( item . href === '#'
? ( 'event.preventDefault(); ' + item . action + ' closeNavMenu();' )
: ( 'event.preventDefault(); ' + item . action + ' updatePath(' + JSON . stringify ( item . href ) + '); closeNavMenu();' ) ) ;
var href = disabled ? '/more' : item . href ;
html += '<a href="' + escapeAttr ( href ) + '" onclick="' + onclick + '" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:0.9rem; background:' + ( disabled ? 'rgba(148,163,184,0.08)' : 'var(--muted-surface)' ) + '; opacity:' + ( disabled ? '0.78' : '1' ) + ';">' ;
html += '<div style="display:flex; justify-content:space-between; gap:0.75rem; align-items:flex-start; margin-bottom:0.45rem;">' ;
html += '<div style="display:flex; align-items:center; gap:0.65rem; min-width:0;">' ;
html += '<span style="width:2rem; height:2rem; border-radius:999px; display:inline-flex; align-items:center; justify-content:center; background:rgba(59,130,246,0.12); color:var(--primary); flex:0 0 auto;"><i class="fas ' + escapeHtml ( item . icon ) + '"></i></span>' ;
html += '<strong style="line-height:1.35;">' + escapeHtml ( item . title ) + '</strong>' ;
html += '</div>' ;
html += '<span class="badge ' + escapeHtml ( item . badgeClass || 'badge-info' ) + '" style="white-space:nowrap;">' + escapeHtml ( item . status ) + '</span>' ;
html += '</div>' ;
html += '<div style="color:var(--text-light); font-size:0.9rem; line-height:1.55;">' + escapeHtml ( item . desc ) + '</div>' ;
html += '</a>' ;
} ) ;
html += '</div></div>' ;
2026-03-24 18:11:08 -07:00
} ) ;
html += '</div>' ;
container . innerHTML = html ;
}
2026-03-27 12:02:36 -07:00
window . _showMore = renderMoreView ;
2026-03-24 18:11:08 -07:00
2026-03-27 12:11:18 -07:00
window . showUnitConverterModal = function ( ) {
var existing = document . getElementById ( 'unitConverterModal' ) ;
if ( existing ) existing . remove ( ) ;
var modal = document . createElement ( 'div' ) ;
modal . id = 'unitConverterModal' ;
modal . style . cssText = 'position:fixed; inset:0; background:rgba(8,15,32,0.68); backdrop-filter:blur(8px); z-index:12000; display:flex; align-items:center; justify-content:center; padding:1rem;' ;
modal . innerHTML = '' +
'<div style="width:min(560px, 100%); border-radius:18px; border:1px solid var(--border); background:var(--background); box-shadow:0 24px 90px rgba(0,0,0,0.35); overflow:hidden;">' +
'<div style="display:flex; justify-content:space-between; align-items:center; padding:1rem 1.1rem; border-bottom:1px solid var(--border);">' +
'<div><div style="font-size:1.1rem; font-weight:800;">Unit Converter</div><div style="color:var(--text-light); font-size:0.9rem; margin-top:0.2rem;">Wei, gwei, ether, and 6-decimal stablecoin units for Chain 138.</div></div>' +
'<button type="button" class="btn btn-secondary" id="unitConverterCloseBtn"><i class="fas fa-times"></i></button>' +
'</div>' +
'<div style="padding:1rem 1.1rem; display:grid; gap:0.9rem;">' +
'<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Amount</span><input id="unitConverterAmount" type="number" min="0" step="any" placeholder="1.0" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"></label>' +
'<label style="display:grid; gap:0.35rem;"><span style="font-weight:700;">Unit</span><select id="unitConverterUnit" style="padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);"><option value="ether">Ether / WETH</option><option value="gwei">Gwei</option><option value="wei">Wei</option><option value="stable">Stablecoin (6 decimals)</option></select></label>' +
'<div id="unitConverterResults" style="display:grid; gap:0.55rem;"></div>' +
'</div>' +
'</div>' ;
document . body . appendChild ( modal ) ;
function renderUnitConverterResults ( ) {
var amountEl = document . getElementById ( 'unitConverterAmount' ) ;
var unitEl = document . getElementById ( 'unitConverterUnit' ) ;
var resultsEl = document . getElementById ( 'unitConverterResults' ) ;
if ( ! amountEl || ! unitEl || ! resultsEl ) return ;
var amount = Number ( amountEl . value || '0' ) ;
var unit = unitEl . value ;
if ( ! isFinite ( amount ) || amount < 0 ) {
resultsEl . innerHTML = '<div style="color:var(--text-light);">Enter a non-negative amount to convert.</div>' ;
return ;
}
var wei = 0 ;
if ( unit === 'ether' ) wei = amount * 1e18 ;
else if ( unit === 'gwei' ) wei = amount * 1e9 ;
else if ( unit === 'wei' ) wei = amount ;
else if ( unit === 'stable' ) wei = amount * 1e6 ;
var etherValue = unit === 'stable' ? 'N/A' : ( wei / 1e18 ) . toLocaleString ( undefined , { maximumFractionDigits : 18 } ) ;
var gweiValue = unit === 'stable' ? 'N/A' : ( wei / 1e9 ) . toLocaleString ( undefined , { maximumFractionDigits : 9 } ) ;
var stableValue = ( unit === 'stable' ? amount : wei / 1e6 ) . toLocaleString ( undefined , { maximumFractionDigits : 6 } ) ;
resultsEl . innerHTML =
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>Wei:</strong> ' + escapeHtml ( Math . round ( wei ) . toString ( ) ) + '</div>' +
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>Gwei:</strong> ' + escapeHtml ( gweiValue ) + '</div>' +
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>Ether / WETH:</strong> ' + escapeHtml ( etherValue ) + '</div>' +
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>6-decimal stable amount:</strong> ' + escapeHtml ( stableValue ) + '</div>' ;
}
var closeBtn = document . getElementById ( 'unitConverterCloseBtn' ) ;
if ( closeBtn ) closeBtn . addEventListener ( 'click' , function ( ) { modal . remove ( ) ; } ) ;
modal . addEventListener ( 'click' , function ( event ) {
if ( event . target === modal ) modal . remove ( ) ;
} ) ;
var amountEl = document . getElementById ( 'unitConverterAmount' ) ;
var unitEl = document . getElementById ( 'unitConverterUnit' ) ;
if ( amountEl ) amountEl . addEventListener ( 'input' , renderUnitConverterResults ) ;
if ( unitEl ) unitEl . addEventListener ( 'change' , renderUnitConverterResults ) ;
renderUnitConverterResults ( ) ;
} ;
window . openExternalMoreLink = function ( url ) {
window . open ( url , '_blank' , 'noopener,noreferrer' ) ;
} ;
2026-02-16 03:09:53 -08:00
async function refreshBridgeData ( ) {
const container = document . getElementById ( 'bridgeContent' ) ;
if ( ! container ) return ;
try {
container . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading bridge data...</div>' ;
// Chain 138 Bridge Contracts
const WETH9 _BRIDGE _138 = '0x971cD9D156f193df8051E48043C476e53ECd4693' ;
const WETH10 _BRIDGE _138 = '0xe0E93247376aa097dB308B92e6Ba36bA015535D0' ;
// Ethereum Mainnet Bridge Contracts
const WETH9 _BRIDGE _MAINNET = '0x2A0840e5117683b11682ac46f5CF5621E67269E3' ;
const WETH10 _BRIDGE _MAINNET = '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03' ;
2026-03-27 12:02:36 -07:00
const explorerLinks = {
'BSC (56)' : { label : 'BscScan' , baseUrl : 'https://bscscan.com/address/' } ,
'Polygon (137)' : { label : 'PolygonScan' , baseUrl : 'https://polygonscan.com/address/' } ,
'Avalanche (43114)' : { label : 'Avalanche Explorer' , baseUrl : 'https://subnets.avax.network/c-chain/address/' } ,
'Base (8453)' : { label : 'BaseScan' , baseUrl : 'https://basescan.org/address/' } ,
'Arbitrum (42161)' : { label : 'Arbiscan' , baseUrl : 'https://arbiscan.io/address/' } ,
'Optimism (10)' : { label : 'Optimistic Etherscan' , baseUrl : 'https://optimistic.etherscan.io/address/' } ,
'Ethereum Mainnet (1)' : { label : 'Etherscan' , baseUrl : 'https://etherscan.io/address/' }
} ;
function renderExplorerLink ( chainLabel , address ) {
const explorer = explorerLinks [ chainLabel ] ;
if ( ! explorer ) return '<span style="color: var(--text-light);">No explorer</span>' ;
const url = explorer . baseUrl + address ;
return '<a href="' + escapeHtml ( url ) + '" target="_blank" rel="noopener noreferrer" style="color: var(--primary); white-space: nowrap;">View on ' + escapeHtml ( explorer . label ) + '</a>' ;
}
2026-02-16 03:09:53 -08:00
// Bridge routes configuration
const routes = {
weth9 : {
'BSC (56)' : '0x8078a09637e47fa5ed34f626046ea2094a5cde5e' ,
'Polygon (137)' : '0xa780ef19a041745d353c9432f2a7f5a241335ffe' ,
'Avalanche (43114)' : '0x8078a09637e47fa5ed34f626046ea2094a5cde5e' ,
'Base (8453)' : '0x8078a09637e47fa5ed34f626046ea2094a5cde5e' ,
'Arbitrum (42161)' : '0x8078a09637e47fa5ed34f626046ea2094a5cde5e' ,
'Optimism (10)' : '0x8078a09637e47fa5ed34f626046ea2094a5cde5e' ,
'Ethereum Mainnet (1)' : WETH9 _BRIDGE _MAINNET
} ,
weth10 : {
'BSC (56)' : '0x105f8a15b819948a89153505762444ee9f324684' ,
'Polygon (137)' : '0xdab0591e5e89295ffad75a71dcfc30c5625c4fa2' ,
'Avalanche (43114)' : '0x105f8a15b819948a89153505762444ee9f324684' ,
'Base (8453)' : '0x105f8a15b819948a89153505762444ee9f324684' ,
'Arbitrum (42161)' : '0x105f8a15b819948a89153505762444ee9f324684' ,
'Optimism (10)' : '0x105f8a15b819948a89153505762444ee9f324684' ,
'Ethereum Mainnet (1)' : WETH10 _BRIDGE _MAINNET
}
} ;
2026-03-27 12:02:36 -07:00
const sourceBridgeCount = 2 ;
const mainnetBridgeCount = 2 ;
const routeBridgeCount = new Set ( [
... Object . values ( routes . weth9 ) ,
... Object . values ( routes . weth10 )
] . map ( function ( addr ) { return String ( addr || '' ) . toLowerCase ( ) ; } ) ) . size ;
const totalBridgeCount = getActiveBridgeContractCount ( ) ;
2026-02-16 03:09:53 -08:00
2026-03-24 18:11:08 -07:00
const bridgeFilter = getExplorerPageFilter ( 'bridgeRoutes' ) ;
const filterBar = renderPageFilterBar ( 'bridgeRoutes' , 'Filter by chain name, chain ID, or bridge address...' , 'Filters the route tables below.' , 'refreshBridgeData()' ) ;
2026-02-16 03:09:53 -08:00
// Build HTML
2026-03-24 18:11:08 -07:00
let html = filterBar + `
2026-02-16 03:09:53 -08:00
< div class = "bridge-chain-card" >
< div class = "chain-name" > < i class = "fas fa-network-wired" > < / i > C C I P B r i d g e E c o s y s t e m < / d i v >
2026-03-27 12:02:36 -07:00
< div class = "badge badge-info" style = "display:inline-flex; margin-top:0.65rem; white-space:nowrap;" > $ { sourceBridgeCount } source contracts / $ { mainnetBridgeCount } mainnet contracts / $ { routeBridgeCount } unique route contracts < / d i v >
2026-02-16 03:09:53 -08:00
< div style = "color: var(--text-light); margin-bottom: 1rem;" >
Cross - chain interoperability powered by Chainlink CCIP
< / d i v >
< / d i v >
<!-- Chain 138 Bridges -- >
< div class = "card" style = "margin-bottom: 1.5rem;" >
< div class = "card-header" >
< h3 class = "card-title" > < i class = "fas fa-home" > < / i > C h a i n 1 3 8 ( S o u r c e C h a i n ) < / h 3 >
2026-03-27 12:02:36 -07:00
< span class = "badge badge-info" style = "white-space: nowrap;" > $ { sourceBridgeCount } source contracts / $ { mainnetBridgeCount } mainnet contracts / $ { routeBridgeCount } unique route contracts < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "chain-info" style = "grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;" >
< div class = "chain-stat" style = "background: var(--light); padding: 1rem; border-radius: 8px;" >
< div class = "chain-stat-label" style = "font-weight: 600; margin-bottom: 0.5rem;" > CCIPWETH9Bridge < / d i v >
< div class = "chain-stat-value" >
2026-03-28 14:09:23 -07:00
$ { explorerAddressLink ( WETH9 _BRIDGE _138 , escapeHtml ( WETH9 _BRIDGE _138 ) , 'color: inherit; text-decoration: none; font-size: 0.9rem;' ) }
2026-02-16 03:09:53 -08:00
< / d i v >
< div style = "margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);" >
2026-03-28 14:09:23 -07:00
Token : $ { explorerAddressLink ( '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' , 'WETH9' , 'color: inherit; text-decoration: none;' ) }
2026-02-16 03:09:53 -08:00
< / d i v >
< / d i v >
< div class = "chain-stat" style = "background: var(--light); padding: 1rem; border-radius: 8px;" >
< div class = "chain-stat-label" style = "font-weight: 600; margin-bottom: 0.5rem;" > CCIPWETH10Bridge < / d i v >
< div class = "chain-stat-value" >
2026-03-28 14:09:23 -07:00
$ { explorerAddressLink ( WETH10 _BRIDGE _138 , escapeHtml ( WETH10 _BRIDGE _138 ) , 'color: inherit; text-decoration: none; font-size: 0.9rem;' ) }
2026-02-16 03:09:53 -08:00
< / d i v >
< div style = "margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);" >
2026-03-28 14:09:23 -07:00
Token : $ { explorerAddressLink ( '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f' , 'WETH10' , 'color: inherit; text-decoration: none;' ) }
2026-02-16 03:09:53 -08:00
< / d i v >
< / d i v >
< / d i v >
< / d i v >
<!-- WETH9 Bridge Routes -- >
< div class = "card" style = "margin-bottom: 1.5rem;" >
< div class = "card-header" >
< h3 class = "card-title" > < i class = "fas fa-route" > < / i > C C I P W E T H 9 B r i d g e R o u t e s < / h 3 >
2026-03-27 12:02:36 -07:00
< span class = "badge badge-success" > $ { Object . keys ( routes . weth9 ) . length } Destinations < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
< div style = "overflow-x: auto;" >
< table class = "table" >
< thead >
< tr >
< th > Destination Chain < / t h >
< th > Chain ID < / t h >
< th > Bridge Address < / t h >
2026-03-27 12:02:36 -07:00
< th > Explorer < / t h >
2026-02-16 03:09:53 -08:00
< / t r >
< / t h e a d >
< tbody >
` ;
// Add WETH9 routes
2026-03-24 18:11:08 -07:00
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 ) {
2026-03-27 12:02:36 -07:00
html += '<tr><td colspan="4" style="text-align:center; padding: 1rem; color: var(--text-light);">No WETH9 routes match the current filter.</td></tr>' ;
2026-03-24 18:11:08 -07:00
}
for ( const [ chain , address ] of weth9Routes ) {
2026-02-16 03:09:53 -08:00
const chainId = chain . match ( /\\((\d+)\\)/ ) ? . [ 1 ] || '' ;
html += `
< tr >
< td > < strong > $ { chain . replace ( /\s*\\(\\d+\\)/ , '' ) } < / s t r o n g > < / t d >
< td > $ { chainId } < / t d >
< td > < span class = "hash" onclick = "showAddressDetail('${escapeHtml(address)}')" style = "cursor: pointer;" > $ { escapeHtml ( shortenHash ( address ) ) } < / s p a n > < / t d >
2026-03-27 12:02:36 -07:00
< td > $ { renderExplorerLink ( chain , address ) } < / t d >
2026-02-16 03:09:53 -08:00
< / t r >
` ;
}
html += `
< / t b o d y >
< / t a b l e >
< / d i v >
< / d i v >
<!-- WETH10 Bridge Routes -- >
< div class = "card" style = "margin-bottom: 1.5rem;" >
< div class = "card-header" >
< h3 class = "card-title" > < i class = "fas fa-route" > < / i > C C I P W E T H 1 0 B r i d g e R o u t e s < / h 3 >
2026-03-27 12:02:36 -07:00
< span class = "badge badge-success" > $ { Object . keys ( routes . weth10 ) . length } Destinations < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
< div style = "overflow-x: auto;" >
< table class = "table" >
< thead >
< tr >
< th > Destination Chain < / t h >
< th > Chain ID < / t h >
< th > Bridge Address < / t h >
2026-03-27 12:02:36 -07:00
< th > Explorer < / t h >
2026-02-16 03:09:53 -08:00
< / t r >
< / t h e a d >
< tbody >
` ;
// Add WETH10 routes
2026-03-24 18:11:08 -07:00
if ( weth10Routes . length === 0 ) {
2026-03-27 12:02:36 -07:00
html += '<tr><td colspan="4" style="text-align:center; padding: 1rem; color: var(--text-light);">No WETH10 routes match the current filter.</td></tr>' ;
2026-03-24 18:11:08 -07:00
}
for ( const [ chain , address ] of weth10Routes ) {
2026-02-16 03:09:53 -08:00
const chainId = chain . match ( /\\((\d+)\\)/ ) ? . [ 1 ] || '' ;
html += `
< tr >
< td > < strong > $ { chain . replace ( /\s*\\(\\d+\\)/ , '' ) } < / s t r o n g > < / t d >
< td > $ { chainId } < / t d >
< td > < span class = "hash" onclick = "showAddressDetail('${escapeHtml(address)}')" style = "cursor: pointer;" > $ { escapeHtml ( shortenHash ( address ) ) } < / s p a n > < / t d >
2026-03-27 12:02:36 -07:00
< td > $ { renderExplorerLink ( chain , address ) } < / t d >
2026-02-16 03:09:53 -08:00
< / t r >
` ;
}
html += `
< / t b o d y >
< / t a b l e >
< / d i v >
< / d i v >
<!-- Ethereum Mainnet Bridges -- >
< div class = "card" style = "margin-bottom: 1.5rem;" >
< div class = "card-header" >
< h3 class = "card-title" > < i class = "fas fa-ethereum" > < / i > E t h e r e u m M a i n n e t B r i d g e s < / h 3 >
2026-03-27 12:02:36 -07:00
< span class = "badge badge-info" style = "white-space: nowrap;" > $ { sourceBridgeCount } source contracts / $ { mainnetBridgeCount } mainnet contracts / $ { routeBridgeCount } unique route contracts < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "chain-info" style = "grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;" >
< div class = "chain-stat" style = "background: var(--light); padding: 1rem; border-radius: 8px;" >
< div class = "chain-stat-label" style = "font-weight: 600; margin-bottom: 0.5rem;" > CCIPWETH9Bridge < / d i v >
< div class = "chain-stat-value" >
2026-03-28 00:21:18 -07:00
< span class = "hash" onclick = "window.open('https://etherscan.io/address/${WETH9_BRIDGE_MAINNET}', '_blank', 'noopener,noreferrer')" style = "cursor: pointer; font-size: 0.9rem;" > $ { WETH9 _BRIDGE _MAINNET } < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
< div style = "margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);" >
2026-03-28 00:21:18 -07:00
< a href = "https://etherscan.io/address/${WETH9_BRIDGE_MAINNET}" target = "_blank" rel = "noopener noreferrer" style = "color: var(--primary);" > View on Etherscan < / a >
2026-02-16 03:09:53 -08:00
< / d i v >
< / d i v >
< div class = "chain-stat" style = "background: var(--light); padding: 1rem; border-radius: 8px;" >
< div class = "chain-stat-label" style = "font-weight: 600; margin-bottom: 0.5rem;" > CCIPWETH10Bridge < / d i v >
< div class = "chain-stat-value" >
2026-03-28 00:21:18 -07:00
< span class = "hash" onclick = "window.open('https://etherscan.io/address/${WETH10_BRIDGE_MAINNET}', '_blank', 'noopener,noreferrer')" style = "cursor: pointer; font-size: 0.9rem;" > $ { WETH10 _BRIDGE _MAINNET } < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
< div style = "margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);" >
2026-03-28 00:21:18 -07:00
< a href = "https://etherscan.io/address/${WETH10_BRIDGE_MAINNET}" target = "_blank" rel = "noopener noreferrer" style = "color: var(--primary);" > View on Etherscan < / a >
2026-02-16 03:09:53 -08:00
< / d i v >
< / d i v >
< / d i v >
< / d i v >
<!-- Bridge Information -- >
< div class = "card" >
2026-03-27 12:02:36 -07:00
< div class = "card-header" >
< h3 class = "card-title" > < i class = "fas fa-info-circle" > < / i > B r i d g e I n f o r m a t i o n < / h 3 >
< span class = "badge badge-info" style = "white-space: nowrap;" > $ { totalBridgeCount } Active Bridge Contracts < / s p a n >
< / d i v >
2026-02-16 03:09:53 -08:00
< div style = "line-height: 1.8;" >
< p > < strong > CCIP Bridge Ecosystem < / s t r o n g > e n a b l e s c r o s s - c h a i n t r a n s f e r s o f W E T H 9 a n d W E T H 1 0 t o k e n s u s i n g C h a i n l i n k C C I P ( C r o s s - C h a i n I n t e r o p e r a b i l i t y P r o t o c o l ) . < / p >
< h4 style = "margin-top: 1.5rem; margin-bottom: 0.5rem;" > Supported Networks : < / h 4 >
< ul style = "margin-left: 2rem; margin-top: 0.5rem;" >
< li > < strong > Chain 138 < / s t r o n g > - S o u r c e c h a i n w i t h b o t h b r i d g e c o n t r a c t s < / l i >
< li > < strong > Ethereum Mainnet < / s t r o n g > - D e s t i n a t i o n w i t h d e d i c a t e d b r i d g e c o n t r a c t s < / l i >
< li > < strong > BSC < / s t r o n g > - B i n a n c e S m a r t C h a i n < / l i >
< li > < strong > Polygon < / s t r o n g > - P o l y g o n P o S < / l i >
< li > < strong > Avalanche < / s t r o n g > - A v a l a n c h e C - C h a i n < / l i >
< li > < strong > Base < / s t r o n g > - B a s e L 2 < / l i >
< li > < strong > Arbitrum < / s t r o n g > - A r b i t r u m O n e < / l i >
< li > < strong > Optimism < / s t r o n g > - O p t i m i s m M a i n n e t < / l i >
2026-02-22 15:35:45 -08:00
< li > < strong > Cronos < / s t r o n g > - C r o n o s ( 2 5 ) ; s e e r o u t i n g t a b l e f o r f u l l l i s t a n d c o n f i g - r e a d y c h a i n s ( G n o s i s , C e l o , W e m i x ) < / l i >
2026-02-16 03:09:53 -08:00
< / u l >
< h4 style = "margin-top: 1.5rem; margin-bottom: 0.5rem;" > How to Use : < / h 4 >
< ol style = "margin-left: 2rem; margin-top: 0.5rem;" >
< li > Click on any bridge address to view detailed information and transaction history < / l i >
< li > Use the bridge contracts to transfer WETH9 or WETH10 tokens between supported chains < / l i >
< li > All transfers are secured by Chainlink CCIP infrastructure < / l i >
< / o l >
< h4 style = "margin-top: 1.5rem; margin-bottom: 0.5rem;" > CCIP Infrastructure : < / h 4 >
< ul style = "margin-left: 2rem; margin-top: 0.5rem;" >
2026-03-28 14:09:23 -07:00
< li > < strong > CCIP Router ( Chain 138 ) < / s t r o n g > : $ { e x p l o r e r A d d r e s s L i n k ( ' 0 x 8 0 7 8 A 0 9 6 3 7 e 4 7 F a 5 E d 3 4 F 6 2 6 0 4 6 E a 2 0 9 4 a 5 C D E 5 e ' , ' 0 x 8 0 7 8 A 0 9 6 3 7 e 4 7 F a 5 E d 3 4 F 6 2 6 0 4 6 E a 2 0 9 4 a 5 C D E 5 e ' , ' c o l o r : i n h e r i t ; t e x t - d e c o r a t i o n : n o n e ; ' ) } < / l i >
< li > < strong > CCIP Sender ( Chain 138 ) < / s t r o n g > : $ { e x p l o r e r A d d r e s s L i n k ( ' 0 x 1 0 5 F 8 A 1 5 b 8 1 9 9 4 8 a 8 9 1 5 3 5 0 5 7 6 2 4 4 4 E e 9 f 3 2 4 6 8 4 ' , ' 0 x 1 0 5 F 8 A 1 5 b 8 1 9 9 4 8 a 8 9 1 5 3 5 0 5 7 6 2 4 4 4 E e 9 f 3 2 4 6 8 4 ' , ' c o l o r : i n h e r i t ; t e x t - d e c o r a t i o n : n o n e ; ' ) } < / l i >
2026-02-16 03:09:53 -08:00
< / u l >
< / d i v >
< / d i v >
` ;
container . innerHTML = html ;
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load bridge data: ' + escapeHtml ( error . message ) + '</div>' ;
}
}
function safeBlockNumber ( v ) { const n = String ( v ) . replace ( /[^0-9]/g , '' ) ; return n ? n : null ; }
function safeTxHash ( v ) { const s = String ( v ) ; return /^0x[a-fA-F0-9]{64}$/ . test ( s ) ? s : null ; }
2026-03-27 12:02:36 -07:00
function safeAddress ( v ) {
const s = String ( v || '' ) ;
if ( ! /^0x[a-fA-F0-9]{40}$/i . test ( s ) ) return null ;
if ( /^0x0{40}$/i . test ( s ) ) return null ;
return s ;
}
2026-03-28 14:09:23 -07:00
function escapeJsSingleQuoted ( value ) {
return String ( value == null ? '' : value ) . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) ;
}
function explorerAddressLink ( address , content , style ) {
var safe = safeAddress ( address ) ;
if ( ! safe ) return content || 'N/A' ;
return '<a class="hash" href="/address/' + encodeURIComponent ( safe ) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeJsSingleQuoted ( safe ) + '\')" style="' + escapeAttr ( style || 'color: inherit; text-decoration: none;' ) + '">' + ( content || escapeHtml ( shortenHash ( safe ) ) ) + '</a>' ;
}
function explorerTransactionLink ( txHash , content , style ) {
var safe = safeTxHash ( txHash ) ;
if ( ! safe ) return content || 'N/A' ;
return '<a class="hash" href="/tx/' + encodeURIComponent ( safe ) + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + escapeJsSingleQuoted ( safe ) + '\')" style="' + escapeAttr ( style || 'color: inherit; text-decoration: none;' ) + '">' + ( content || escapeHtml ( shortenHash ( safe ) ) ) + '</a>' ;
}
function explorerBlockLink ( blockNumber , content , style ) {
var safe = safeBlockNumber ( blockNumber ) ;
if ( ! safe ) return content || 'N/A' ;
return '<a href="/block/' + encodeURIComponent ( safe ) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeJsSingleQuoted ( safe ) + '\')" style="' + escapeAttr ( style || 'color: inherit; text-decoration: none;' ) + '">' + ( content || escapeHtml ( String ( safe ) ) ) + '</a>' ;
}
2026-03-28 13:59:00 -07:00
async function renderBlockDetail ( blockNumber ) {
2026-02-16 03:09:53 -08:00
const bn = safeBlockNumber ( blockNumber ) ;
if ( ! bn ) { showToast ( 'Invalid block number' , 'error' ) ; return ; }
blockNumber = bn ;
currentDetailKey = 'block:' + blockNumber ;
showView ( 'blockDetail' ) ;
updatePath ( '/block/' + blockNumber ) ;
const container = document . getElementById ( 'blockDetail' ) ;
updateBreadcrumb ( 'block' , blockNumber ) ;
container . innerHTML = createSkeletonLoader ( 'detail' ) ;
try {
let b ;
// For ChainID 138, use Blockscout API directly
var rawBlockResponse = null ;
if ( CHAIN _ID === 138 ) {
try {
2026-03-28 13:59:00 -07:00
var detailResult = await fetchChain138BlockDetail ( blockNumber ) ;
rawBlockResponse = detailResult . rawBlockResponse ;
b = detailResult . block ;
2026-02-16 03:09:53 -08:00
if ( ! b ) {
throw new Error ( 'Block not found' ) ;
}
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load block: ' + escapeHtml ( error . message || 'Unknown error' ) + '. <button onclick="showBlockDetail(\'' + escapeHtml ( String ( blockNumber ) ) + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>' ;
return ;
}
} else {
const block = await fetchAPIWithRetry ( ` ${ API _BASE } /v1/blocks/138/ ${ blockNumber } ` ) ;
if ( block . data ) {
b = block . data ;
} else {
throw new Error ( 'Block not found' ) ;
}
}
if ( b ) {
const timestamp = new Date ( b . timestamp ) . toLocaleString ( ) ;
const gasUsedPercent = b . gas _limit ? ( ( parseInt ( b . gas _used || 0 ) / parseInt ( b . gas _limit ) ) * 100 ) . toFixed ( 2 ) : '0' ;
const baseFeeGwei = b . base _fee _per _gas ? ( parseInt ( b . base _fee _per _gas ) / 1e9 ) . toFixed ( 2 ) : 'N/A' ;
const burntFeesEth = b . burnt _fees ? formatEther ( b . burnt _fees ) : '0' ;
container . innerHTML = `
< div style = "display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;" >
< h2 style = "margin: 0;" > Block # $ { b . number } < / h 2 >
< button onclick = "exportBlockData(${b.number})" class = "btn btn-primary" style = "padding: 0.5rem 1rem;" >
< i class = "fas fa-download" > < / i > E x p o r t
< / b u t t o n >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Block Number < / d i v >
< div class = "info-value" > $ { b . number } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Hash < / d i v >
< div class = "info-value hash" > $ { escapeHtml ( b . hash || '' ) } < button type = "button" class = "btn-copy" onclick = "event.stopPropagation(); copyToClipboard(\'' + String(b.hash || '').replace(/\\/g, '\\\\').replace(/'/g, " \ \ '") + ' \ ', \'Copied\' ) ; " aria-label=" Copy hash "><i class=" fas fa - copy " > < / i > < / b u t t o n > < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Parent Hash < / d i v >
2026-03-28 14:09:23 -07:00
< div class = "info-value" > $ { explorerBlockLink ( String ( parseInt ( b . number ) - 1 ) , escapeHtml ( b . parent _hash || '' ) , 'color: inherit; text-decoration: none;' ) } < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Timestamp < / d i v >
< div class = "info-value" > $ { escapeHtml ( timestamp ) } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Miner < / d i v >
2026-03-28 14:09:23 -07:00
< div class = "info-value" > $ { explorerAddressLink ( b . miner || '' , formatAddressWithLabel ( b . miner || '' ) || 'N/A' , 'color: inherit; text-decoration: none;' ) } < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Transaction Count < / d i v >
< div class = "info-value" > $ { b . transaction _count || 0 } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Gas Used < / d i v >
< div class = "info-value" > $ { formatNumber ( b . gas _used || 0 ) } / $ { formatNumber ( b . gas _limit || 0 ) } ( $ { gasUsedPercent } % ) < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Gas Limit < / d i v >
< div class = "info-value" > $ { formatNumber ( b . gas _limit || 0 ) } < / d i v >
< / d i v >
$ { b . base _fee _per _gas ? `
< div class = "info-row" >
< div class = "info-label" > Base Fee < / d i v >
< div class = "info-value" > $ { baseFeeGwei } Gwei < / d i v >
< / d i v >
` : ''}
$ { b . burnt _fees && parseInt ( b . burnt _fees ) > 0 ? `
< div class = "info-row" >
< div class = "info-label" > Burnt Fees < / d i v >
< div class = "info-value" > $ { burntFeesEth } ETH < / d i v >
< / d i v >
` : ''}
< div class = "info-row" >
< div class = "info-label" > Size < / d i v >
< div class = "info-value" > $ { formatNumber ( b . size || 0 ) } bytes < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Difficulty < / d i v >
< div class = "info-value" > $ { formatNumber ( b . difficulty || 0 ) } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Nonce < / d i v >
< div class = "info-value" > $ { escapeHtml ( String ( b . nonce || '0x0' ) ) } < / d i v >
< / d i v >
$ { ( rawBlockResponse && ( rawBlockResponse . consensus !== undefined || rawBlockResponse . finality !== undefined || rawBlockResponse . validated !== undefined ) ) ? '<div class="info-row"><div class="info-label">Finality / Consensus</div><div class="info-value">' + escapeHtml ( String ( rawBlockResponse . consensus != null ? rawBlockResponse . consensus : ( rawBlockResponse . finality != null ? rawBlockResponse . finality : rawBlockResponse . validated ) ) ) + '</div></div>' : '' }
` ;
} else {
container . innerHTML = '<div class="error">Block not found</div>' ;
}
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load block: ' + escapeHtml ( error . message ) + '</div>' ;
}
}
2026-03-28 13:59:00 -07:00
window . _showBlockDetail = renderBlockDetail ;
2026-03-02 12:14:13 -08:00
// Keep wrapper (do not overwrite) so all calls go through setTimeout and avoid stack overflow
2026-02-16 03:09:53 -08:00
2026-03-28 13:59:00 -07:00
async function renderTransactionDetail ( txHash ) {
2026-02-16 03:09:53 -08:00
const th = safeTxHash ( txHash ) ;
if ( ! th ) { showToast ( 'Invalid transaction hash' , 'error' ) ; return ; }
txHash = th ;
2026-03-02 12:14:13 -08:00
currentDetailKey = 'tx:' + txHash . toLowerCase ( ) ;
2026-02-16 03:09:53 -08:00
showView ( 'transactionDetail' ) ;
updatePath ( '/tx/' + txHash ) ;
const container = document . getElementById ( 'transactionDetail' ) ;
updateBreadcrumb ( 'transaction' , txHash ) ;
container . innerHTML = createSkeletonLoader ( 'detail' ) ;
try {
let t ;
let rawTx = null ;
if ( CHAIN _ID === 138 ) {
try {
2026-03-28 13:59:00 -07:00
var detailResult = await fetchChain138TransactionDetail ( txHash ) ;
rawTx = detailResult . rawTransaction ;
t = detailResult . transaction ;
2026-02-16 03:09:53 -08:00
if ( ! t ) throw new Error ( 'Transaction not found' ) ;
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load transaction: ' + escapeHtml ( error . message || 'Unknown error' ) + '. <button onclick="showTransactionDetail(\'' + escapeHtml ( String ( txHash ) ) + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>' ;
return ;
}
} else {
const tx = await fetchAPIWithRetry ( ` ${ API _BASE } /v1/transactions/138/ ${ txHash } ` ) ;
if ( tx . data ) t = tx . data ;
else throw new Error ( 'Transaction not found' ) ;
}
if ( ! t ) {
container . innerHTML = '<div class="error">Transaction not found</div>' ;
return ;
}
const timestamp = new Date ( t . created _at ) . toLocaleString ( ) ;
const valueEth = formatEther ( t . value || '0' ) ;
const gasPriceGwei = t . gas _price ? ( parseInt ( t . gas _price ) / 1e9 ) . toFixed ( 2 ) : 'N/A' ;
const maxFeeGwei = t . max _fee _per _gas ? ( parseInt ( t . max _fee _per _gas ) / 1e9 ) . toFixed ( 2 ) : 'N/A' ;
const priorityFeeGwei = t . max _priority _fee _per _gas ? ( parseInt ( t . max _priority _fee _per _gas ) / 1e9 ) . toFixed ( 2 ) : 'N/A' ;
const burntFeeEth = t . tx _burnt _fee ? formatEther ( t . tx _burnt _fee ) : '0' ;
const totalFee = t . gas _used && t . gas _price ? formatEther ( ( BigInt ( t . gas _used ) * BigInt ( t . gas _price ) ) . toString ( ) ) : '0' ;
const txType = t . type === 2 ? 'EIP-1559' : t . type === 1 ? 'EIP-2930' : 'Legacy' ;
const revertReason = t . revert _reason || ( rawTx && ( rawTx . revert _reason || rawTx . error || rawTx . result ) ) ;
const inputHex = ( t . input && t . input !== '0x' ) ? t . input : null ;
const decodedInput = t . decoded _input || ( rawTx && rawTx . decoded _input ) ;
2026-03-28 14:09:23 -07:00
const toCellContent = t . to ? explorerAddressLink ( t . to , formatAddressWithLabel ( t . to ) , 'color: inherit; text-decoration: none;' ) + ' <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard(\'' + String ( t . to ) . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) + '\', \'Copied\');" aria-label="Copy address"><i class="fas fa-copy"></i></button>' : 'N/A' ;
2026-02-16 03:09:53 -08:00
let mainHtml = `
< div style = "display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;" >
< h2 style = "margin: 0;" > Transaction < / h 2 >
< div style = "display: flex; gap: 0.5rem;" >
< button onclick = "exportTransactionData('${t.hash}')" class = "btn btn-primary" style = "padding: 0.5rem 1rem;" >
< i class = "fas fa-download" > < / i > J S O N
< / b u t t o n >
< button onclick = "exportTransactionCSV('${t.hash}')" class = "btn btn-primary" style = "padding: 0.5rem 1rem;" >
< i class = "fas fa-file-csv" > < / i > C S V
< / b u t t o n >
< / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Transaction Hash < / d i v >
< div class = "info-value hash" > $ { escapeHtml ( t . hash ) } < button type = "button" class = "btn-copy" onclick = "copyToClipboard('${escapeHtml(t.hash)}', 'Copied');" aria - label = "Copy hash" > < i class = "fas fa-copy" > < / i > < / b u t t o n > < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Type < / d i v >
< div class = "info-value" > < span class = "badge badge-primary" > $ { txType } < / s p a n > < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Status < / d i v >
< div class = "info-value" >
< span class = "badge ${t.status === 1 ? 'badge-success' : 'badge-danger'}" >
$ { t . status === 1 ? 'Success' : t . status === 0 ? 'Failed' : 'Pending' }
< / s p a n >
< / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Block Number < / d i v >
2026-03-28 14:09:23 -07:00
< div class = "info-value" > $ { explorerBlockLink ( String ( t . block _number || '' ) , escapeHtml ( String ( t . block _number || 'N/A' ) ) , 'color: inherit; text-decoration: none;' ) } < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Block Hash < / d i v >
< div class = "info-value hash" > $ { escapeHtml ( t . block _hash || 'N/A' ) } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > From < / d i v >
2026-03-28 14:09:23 -07:00
< div class = "info-value" > $ { explorerAddressLink ( t . from || '' , formatAddressWithLabel ( t . from || '' ) , 'color: inherit; text-decoration: none;' ) } < button type = "button" class = "btn-copy" onclick = "event.stopPropagation(); copyToClipboard('${escapeHtml(t.from || '')}', 'Copied');" aria - label = "Copy address" > < i class = "fas fa-copy" > < / i > < / b u t t o n > < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > To < / d i v >
2026-03-28 14:09:23 -07:00
< div class = "info-value" > $ { toCellContent } < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Value < / d i v >
< div class = "info-value" > $ { valueEth } ETH < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Gas Used < / d i v >
< div class = "info-value" > $ { t . gas _used ? formatNumber ( t . gas _used ) : 'N/A' } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Gas Limit < / d i v >
< div class = "info-value" > $ { t . gas _limit ? formatNumber ( t . gas _limit ) : 'N/A' } < / d i v >
< / d i v >
$ { t . max _fee _per _gas ? ` <div class="info-row"><div class="info-label">Max Fee Per Gas</div><div class="info-value"> ${ maxFeeGwei } Gwei</div></div> ` : '' }
$ { t . max _priority _fee _per _gas ? ` <div class="info-row"><div class="info-label">Max Priority Fee</div><div class="info-value"> ${ priorityFeeGwei } Gwei</div></div> ` : '' }
$ { ! t . max _fee _per _gas && t . gas _price ? ` <div class="info-row"><div class="info-label">Gas Price</div><div class="info-value"> ${ gasPriceGwei } Gwei</div></div> ` : '' }
< div class = "info-row" >
< div class = "info-label" > Total Fee < / d i v >
< div class = "info-value" > $ { totalFee } ETH < / d i v >
< / d i v >
$ { t . tx _burnt _fee && parseInt ( t . tx _burnt _fee ) > 0 ? ` <div class="info-row"><div class="info-label">Burnt Fee</div><div class="info-value"> ${ burntFeeEth } ETH</div></div> ` : '' }
< div class = "info-row" > < div class = "info-label" > Nonce < / d i v > < d i v c l a s s = " i n f o - v a l u e " > $ { t . n o n c e | | ' N / A ' } < / d i v > < / d i v >
< div class = "info-row" > < div class = "info-label" > Timestamp < / d i v > < d i v c l a s s = " i n f o - v a l u e " > $ { t i m e s t a m p } < / d i v > < / d i v >
2026-03-28 14:09:23 -07:00
$ { t . contract _address ? ` <div class="info-row"><div class="info-label">Contract Address</div><div class="info-value"> ${ explorerAddressLink ( t . contract _address , escapeHtml ( t . contract _address ) , 'color: inherit; text-decoration: none;' ) } </div></div> ` : '' }
2026-02-16 03:09:53 -08:00
` ;
if ( revertReason && t . status !== 1 ) {
const reasonStr = typeof revertReason === 'string' ? revertReason : ( revertReason . message || JSON . stringify ( revertReason ) ) ;
mainHtml += `
< div class = "card" style = "margin-top: 1rem; border-left: 4px solid var(--danger);" >
< h3 style = "color: var(--danger); margin-bottom: 0.5rem;" > < i class = "fas fa-exclamation-triangle" > < / i > R e v e r t R e a s o n < / h 3 >
< pre style = "background: var(--light); padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.875rem;" > $ { escapeHtml ( reasonStr ) } < / p r e >
< / d i v >
` ;
}
if ( inputHex || decodedInput ) {
mainHtml += ` <div class="card" style="margin-top: 1rem;"><h3>Input Data</h3> ` ;
if ( decodedInput && ( decodedInput . method || decodedInput . params ) ) {
const method = decodedInput . method || decodedInput . name || 'Unknown' ;
mainHtml += ` <p style="margin-bottom: 0.5rem;"><strong>Method:</strong> ${ escapeHtml ( method ) } </p> ` ;
if ( decodedInput . params && Array . isArray ( decodedInput . params ) ) {
mainHtml += '<table class="table"><thead><tr><th>Param</th><th>Value</th></tr></thead><tbody>' ;
decodedInput . params . forEach ( function ( p ) {
const name = ( p . name || p . type || '' ) ;
const val = typeof p . value !== 'undefined' ? String ( p . value ) : ( p . type || '' ) ;
mainHtml += '<tr><td>' + escapeHtml ( name ) + '</td><td class="hash" style="word-break: break-all;">' + escapeHtml ( val ) + '</td></tr>' ;
} ) ;
mainHtml += '</tbody></table>' ;
}
}
if ( inputHex ) {
mainHtml += ` <p style="margin-top: 0.5rem;"><strong>Hex:</strong> <code id="txInputHex" style="word-break: break-all; font-size: 0.8rem;"> ${ escapeHtml ( inputHex ) } </code> <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById( \' txInputHex \' ).textContent); showToast( \' Copied \' , \' success \' );">Copy</button></p> ` ;
}
mainHtml += '</div>' ;
}
container . innerHTML = mainHtml ;
if ( CHAIN _ID === 138 ) {
const internalCard = document . createElement ( 'div' ) ;
internalCard . className = 'card' ;
internalCard . style . marginTop = '1rem' ;
internalCard . innerHTML = '<h3><i class="fas fa-sitemap"></i> Internal Transactions</h3><div id="txInternalTxs" class="loading">Loading...</div>' ;
container . appendChild ( internalCard ) ;
const logsCard = document . createElement ( 'div' ) ;
logsCard . className = 'card' ;
logsCard . style . marginTop = '1rem' ;
logsCard . innerHTML = '<h3><i class="fas fa-list"></i> Event Logs</h3><div id="txLogs" class="loading">Loading...</div>' ;
container . appendChild ( logsCard ) ;
Promise . all ( [
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions/ ${ txHash } /internal-transactions ` ) . catch ( function ( ) { return { items : [ ] } ; } ) ,
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions/ ${ txHash } /internal_transactions ` ) . catch ( function ( ) { return { items : [ ] } ; } ) ,
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions/ ${ txHash } /logs ` ) . catch ( function ( ) { return { items : [ ] } ; } ) ,
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions/ ${ txHash } /log_entries ` ) . catch ( function ( ) { return { items : [ ] } ; } )
] ) . then ( function ( results ) {
const internalResp = results [ 0 ] . items ? results [ 0 ] : results [ 1 ] ;
const logsResp = results [ 2 ] . items ? results [ 2 ] : results [ 3 ] ;
const internals = internalResp . items || [ ] ;
const logs = logsResp . items || logsResp . log _entries || [ ] ;
const internalEl = document . getElementById ( 'txInternalTxs' ) ;
if ( internalEl ) {
2026-03-24 18:11:08 -07:00
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 ) ;
2026-02-16 03:09:53 -08:00
if ( internals . length === 0 ) {
2026-03-24 18:11:08 -07:00
internalEl . innerHTML = internalFilterBar + '<p style="color: var(--text-light);">No internal transactions</p>' ;
2026-02-16 03:09:53 -08:00
} else {
2026-03-24 18:11:08 -07:00
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 ) {
2026-02-16 03:09:53 -08:00
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' ;
2026-03-28 14:09:23 -07:00
tbl += '<tr><td>' + escapeHtml ( type ) + '</td><td>' + explorerAddressLink ( from , escapeHtml ( shortenHash ( from ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + explorerAddressLink ( to , escapeHtml ( shortenHash ( to ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + escapeHtml ( val ) + ' ETH</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredInternals . length === 0 ) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No internal transactions match the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
tbl += '</tbody></table>' ;
internalEl . innerHTML = tbl ;
}
}
const logsEl = document . getElementById ( 'txLogs' ) ;
if ( logsEl ) {
2026-03-24 18:11:08 -07:00
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 ) ;
2026-02-16 03:09:53 -08:00
if ( logs . length === 0 ) {
2026-03-24 18:11:08 -07:00
logsEl . innerHTML = logsFilterBar + '<p style="color: var(--text-light);">No event logs</p>' ;
2026-02-16 03:09:53 -08:00
} else {
2026-03-24 18:11:08 -07:00
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 ) {
2026-02-16 03:09:53 -08:00
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' ;
2026-03-28 14:09:23 -07:00
tbl += '<tr id="txLogRow' + idx + '"><td>' + explorerAddressLink ( addr , escapeHtml ( shortenHash ( addr ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td style="word-break: break-all; font-size: 0.75rem;">' + escapeHtml ( String ( topicsStr ) . substring ( 0 , 80 ) ) + ( String ( topicsStr ) . length > 80 ? '...' : '' ) + '</td><td style="word-break: break-all; font-size: 0.75rem;">' + escapeHtml ( String ( data ) . substring ( 0 , 66 ) ) + ( String ( data ) . length > 66 ? '...' : '' ) + '</td><td id="txLogDecoded' + idx + '" style="font-size: 0.8rem;">—</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredLogs . length === 0 ) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No event logs match the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
tbl += '</tbody></table>' ;
logsEl . innerHTML = tbl ;
if ( typeof ethers !== 'undefined' && ethers . utils ) {
( function ( logsList , txHash ) {
var addrs = [ ] ;
logsList . forEach ( function ( l ) { var a = l . address && ( l . address . hash || l . address ) || l . address ; if ( a && addrs . indexOf ( a ) === - 1 ) addrs . push ( a ) ; } ) ;
var abiCache = { } ;
Promise . all ( addrs . map ( function ( addr ) {
if ( ! /^0x[a-f0-9]{40}$/i . test ( addr ) ) return Promise . resolve ( ) ;
return fetch ( BLOCKSCOUT _API + '/v2/smart-contracts/' + addr ) . then ( function ( r ) { return r . json ( ) ; } ) . catch ( function ( ) { return null ; } ) . then ( function ( res ) {
var abi = res && ( res . abi || res . abi _json ) ;
if ( abi ) abiCache [ addr . toLowerCase ( ) ] = Array . isArray ( abi ) ? abi : ( typeof abi === 'string' ? JSON . parse ( abi ) : abi ) ;
} ) ;
} ) ) . then ( function ( ) {
logsList . forEach ( function ( log , idx ) {
var addr = ( log . address && ( log . address . hash || log . address ) ) || log . address ;
var topics = log . topics && Array . isArray ( log . topics ) ? log . topics : ( log . topic0 ? [ log . topic0 ] : [ ] ) ;
var data = log . data || log . raw _data || '0x' ;
var abi = addr ? abiCache [ ( addr + '' ) . toLowerCase ( ) ] : null ;
var decodedEl = document . getElementById ( 'txLogDecoded' + idx ) ;
if ( ! decodedEl || ! abi ) return ;
try {
var iface = new ethers . utils . Interface ( abi ) ;
var parsed = iface . parseLog ( { topics : topics , data : data } ) ;
if ( parsed && parsed . name ) {
var args = parsed . args && parsed . args . length ? parsed . args . map ( function ( a ) { return String ( a ) ; } ) . join ( ', ' ) : '' ;
decodedEl . textContent = parsed . name + '(' + args + ')' ;
decodedEl . title = parsed . signature || '' ;
}
} catch ( e ) { }
} ) ;
} ) ;
} ) ( logs , txHash ) ;
}
}
}
} ) ;
}
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load transaction: ' + escapeHtml ( error . message ) + '</div>' ;
}
}
function escapeHtml ( str ) {
if ( str == null ) return '' ;
const s = String ( str ) ;
const div = document . createElement ( 'div' ) ;
div . textContent = s ;
return div . innerHTML ;
}
function exportTransactionCSV ( txHash ) {
fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/transactions/' + txHash ) . then ( function ( r ) {
var t = normalizeTransaction ( r ) ;
if ( ! t ) return ;
var rows = [ [ 'Field' , 'Value' ] , [ 'hash' , t . hash ] , [ 'from' , t . from ] , [ 'to' , t . to || '' ] , [ 'value' , t . value || '0' ] , [ 'block_number' , t . block _number || '' ] , [ 'status' , t . status ] , [ 'gas_used' , t . gas _used || '' ] , [ 'gas_limit' , t . gas _limit || '' ] ] ;
var csv = rows . map ( function ( row ) { return row . map ( function ( cell ) { return '"' + String ( cell ) . replace ( /"/g , '""' ) + '"' ; } ) . join ( ',' ) ; } ) . join ( '\n' ) ;
var blob = new Blob ( [ csv ] , { type : 'text/csv' } ) ;
var url = URL . createObjectURL ( blob ) ;
var a = document . createElement ( 'a' ) ; a . href = url ; a . download = 'transaction-' + txHash . substring ( 0 , 10 ) + '.csv' ; a . click ( ) ;
URL . revokeObjectURL ( url ) ;
} ) . catch ( function ( e ) { showToast ( 'Export failed: ' + ( e . message || 'Unknown' ) , 'error' ) ; } ) ;
}
function exportBlocksCSV ( ) {
2026-03-28 13:59:00 -07:00
Promise . resolve ( CHAIN _ID === 138 ? fetchChain138BlocksPage ( 1 , 50 ) : fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/blocks?page=1&page_size=50' ) . then ( function ( r ) {
return ( r . items || r || [ ] ) . map ( normalizeBlock ) . filter ( function ( block ) { return block !== null ; } ) ;
} ) ) . then ( function ( items ) {
2026-02-16 03:09:53 -08:00
var rows = [ [ 'Block' , 'Hash' , 'Transactions' , 'Timestamp' ] ] ;
items . forEach ( function ( b ) {
var bn = b . height || b . number || b . block _number ;
var h = b . hash || b . block _hash || '' ;
var tc = b . transaction _count || b . transactions _count || 0 ;
var ts = b . timestamp || '' ;
rows . push ( [ String ( bn ) , h , String ( tc ) , String ( ts ) ] ) ;
} ) ;
var csv = rows . map ( function ( row ) { return row . map ( function ( cell ) { return '"' + String ( cell ) . replace ( /"/g , '""' ) + '"' ; } ) . join ( ',' ) ; } ) . join ( '\n' ) ;
var blob = new Blob ( [ csv ] , { type : 'text/csv' } ) ;
var a = document . createElement ( 'a' ) ; a . href = URL . createObjectURL ( blob ) ; a . download = 'blocks.csv' ; a . click ( ) ;
URL . revokeObjectURL ( a . href ) ;
} ) . catch ( function ( e ) { showToast ( 'Export failed: ' + ( e . message || 'Unknown' ) , 'error' ) ; } ) ;
}
function exportTransactionsListCSV ( ) {
2026-03-28 13:59:00 -07:00
Promise . resolve ( CHAIN _ID === 138 ? fetchChain138TransactionsPage ( 1 , 50 ) : fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/transactions?page=1&page_size=50' ) . then ( function ( r ) {
return ( r . items || r || [ ] ) . map ( normalizeTransaction ) . filter ( function ( tx ) { return tx !== null ; } ) ;
} ) ) . then ( function ( items ) {
2026-02-16 03:09:53 -08:00
var rows = [ [ 'Hash' , 'From' , 'To' , 'Value' , 'Block' ] ] ;
items . forEach ( function ( tx ) {
2026-03-28 13:59:00 -07:00
var t = tx && tx . hash ? tx : normalizeTransaction ( tx ) ;
2026-02-16 03:09:53 -08:00
if ( t ) rows . push ( [ t . hash || '' , t . from || '' , t . to || '' , t . value || '0' , String ( t . block _number || '' ) ] ) ;
} ) ;
var csv = rows . map ( function ( row ) { return row . map ( function ( cell ) { return '"' + String ( cell ) . replace ( /"/g , '""' ) + '"' ; } ) . join ( ',' ) ; } ) . join ( '\n' ) ;
var blob = new Blob ( [ csv ] , { type : 'text/csv' } ) ;
var a = document . createElement ( 'a' ) ; a . href = URL . createObjectURL ( blob ) ; a . download = 'transactions.csv' ; a . click ( ) ;
URL . revokeObjectURL ( a . href ) ;
} ) . catch ( function ( e ) { showToast ( 'Export failed: ' + ( e . message || 'Unknown' ) , 'error' ) ; } ) ;
}
function exportAddressTransactionsCSV ( addr ) {
if ( ! addr || ! /^0x[a-fA-F0-9]{40}$/i . test ( addr ) ) { showToast ( 'Invalid address' , 'error' ) ; return ; }
fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/transactions?address=' + encodeURIComponent ( addr ) + '&page=1&page_size=100' ) . then ( function ( r ) {
var items = r . items || r || [ ] ;
var rows = [ [ 'Hash' , 'From' , 'To' , 'Value' , 'Block' , 'Status' ] ] ;
items . forEach ( function ( tx ) {
var t = normalizeTransaction ( tx ) ;
if ( t ) rows . push ( [ t . hash || '' , t . from || '' , t . to || '' , t . value || '0' , String ( t . block _number || '' ) , t . status === 1 ? 'Success' : 'Failed' ] ) ;
} ) ;
var csv = rows . map ( function ( row ) { return row . map ( function ( cell ) { return '"' + String ( cell ) . replace ( /"/g , '""' ) + '"' ; } ) . join ( ',' ) ; } ) . join ( '\n' ) ;
var blob = new Blob ( [ csv ] , { type : 'text/csv' } ) ;
var a = document . createElement ( 'a' ) ; a . href = URL . createObjectURL ( blob ) ; a . download = 'address-' + addr . substring ( 0 , 10 ) + '-transactions.csv' ; a . click ( ) ;
URL . revokeObjectURL ( a . href ) ;
showToast ( 'CSV downloaded' , 'success' ) ;
} ) . catch ( function ( e ) { showToast ( 'Export failed: ' + ( e . message || 'Unknown' ) , 'error' ) ; } ) ;
}
window . exportAddressTransactionsCSV = exportAddressTransactionsCSV ;
function exportAddressTokenBalancesCSV ( addr ) {
if ( ! addr || ! /^0x[a-fA-F0-9]{40}$/i . test ( addr ) ) { showToast ( 'Invalid address' , 'error' ) ; return ; }
fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/token-balances' ) . catch ( function ( ) { return { items : [ ] } ; } ) . then ( function ( r ) {
var items = Array . isArray ( r ) ? r : ( r . items || r || [ ] ) ;
var rows = [ [ 'Token' , 'Contract' , 'Balance' , 'Type' ] ] ;
( items || [ ] ) . forEach ( function ( b ) {
var token = b . token || b ;
var contract = token . address ? . hash || token . address || b . token _contract _address _hash || 'N/A' ;
var symbol = token . symbol || token . name || '-' ;
var balance = b . value || b . balance || '0' ;
var decimals = token . decimals != null ? token . decimals : 18 ;
var divisor = Math . pow ( 10 , parseInt ( decimals , 10 ) ) ;
var displayBalance = ( Number ( balance ) / divisor ) . toLocaleString ( undefined , { maximumFractionDigits : 6 } ) ;
var type = token . type || b . token _type || 'ERC-20' ;
rows . push ( [ symbol , contract , displayBalance , type ] ) ;
} ) ;
var csv = rows . map ( function ( row ) { return row . map ( function ( cell ) { return '"' + String ( cell ) . replace ( /"/g , '""' ) + '"' ; } ) . join ( ',' ) ; } ) . join ( '\n' ) ;
var blob = new Blob ( [ csv ] , { type : 'text/csv' } ) ;
var a = document . createElement ( 'a' ) ; a . href = URL . createObjectURL ( blob ) ; a . download = 'address-' + addr . substring ( 0 , 10 ) + '-token-balances.csv' ; a . click ( ) ;
URL . revokeObjectURL ( a . href ) ;
showToast ( 'CSV downloaded' , 'success' ) ;
} ) . catch ( function ( e ) { showToast ( 'Export failed: ' + ( e . message || 'Unknown' ) , 'error' ) ; } ) ;
}
window . exportAddressTokenBalancesCSV = exportAddressTokenBalancesCSV ;
2026-03-28 13:59:00 -07:00
window . _showTransactionDetail = renderTransactionDetail ;
2026-03-02 12:14:13 -08:00
// Keep wrapper (do not overwrite) so all calls go through setTimeout and avoid stack overflow
2026-02-16 03:09:53 -08:00
2026-03-28 13:59:00 -07:00
async function renderAddressDetail ( address ) {
2026-02-16 03:09:53 -08:00
const addr = safeAddress ( address ) ;
if ( ! addr ) { showToast ( 'Invalid address' , 'error' ) ; return ; }
address = addr ;
currentDetailKey = 'address:' + address . toLowerCase ( ) ;
showView ( 'addressDetail' ) ;
updatePath ( '/address/' + address ) ;
const container = document . getElementById ( 'addressDetail' ) ;
updateBreadcrumb ( 'address' , address ) ;
container . innerHTML = createSkeletonLoader ( 'detail' ) ;
try {
// Validate address format
if ( ! /^0x[a-fA-F0-9]{40}$/ . test ( address ) ) {
container . innerHTML = '<div class="error">Invalid address format</div>' ;
return ;
}
let a ;
// For ChainID 138, use Blockscout API directly
if ( CHAIN _ID === 138 ) {
try {
const response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/addresses/ ${ address } ` ) ;
var raw = response && ( response . data !== undefined ? response . data : response . address !== undefined ? response . address : response . items && response . items [ 0 ] !== undefined ? response . items [ 0 ] : response ) ;
a = normalizeAddress ( raw ) ;
if ( ! a || ! a . hash ) {
throw new Error ( 'Address not found' ) ;
}
} catch ( error ) {
2026-03-28 00:21:18 -07:00
var retryAddress = String ( address || '' ) . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) ;
container . innerHTML = '<div class="error">Failed to load address: ' + escapeHtml ( error . message || 'Unknown error' ) + '. <button onclick="showAddressDetail(\'' + retryAddress + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>' ;
2026-02-16 03:09:53 -08:00
return ;
}
} else {
const addr = await fetchAPIWithRetry ( ` ${ API _BASE } /v1/addresses/138/ ${ address } ` ) ;
if ( addr . data ) {
a = addr . data ;
} else {
throw new Error ( 'Address not found' ) ;
}
}
if ( a ) {
const balanceEth = formatEther ( a . balance || '0' ) ;
const isContract = ! ! a . is _contract ;
const verifiedBadge = a . is _verified ? '<span class="badge badge-success" style="margin-left: 0.5rem;">Verified</span>' : '' ;
2026-03-28 00:21:18 -07:00
const encodedAddress = encodeURIComponent ( address ) ;
const escapedAddress = escapeHtml ( address ) ;
const addressForJs = address . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) ;
const contractLink = isContract ? ` <a href=" ${ EXPLORER _ORIGIN } /address/ ${ encodedAddress } /contract" target="_blank" rel="noopener noreferrer" style="color: var(--primary); font-size: 0.875rem;">View contract on Blockscout</a> ` : '' ;
2026-02-16 03:09:53 -08:00
const savedLabel = getAddressLabel ( address ) ;
const inWatchlist = isInWatchlist ( address ) ;
container . innerHTML = `
< div class = "info-row" >
< div class = "info-label" > Address < / d i v >
2026-03-28 00:21:18 -07:00
< div class = "info-value hash" > $ { escapedAddress } < button type = "button" class = "btn-copy" onclick = "copyToClipboard('${addressForJs}', 'Copied');" aria - label = "Copy address" > < i class = "fas fa-copy" > < / i > < / b u t t o n > < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Label < / d i v >
2026-03-28 00:21:18 -07:00
< div class = "info-value" > < input type = "text" id = "addressLabelInput" value = "${escapeHtml(savedLabel)}" placeholder = "Optional label" style = "padding: 0.35rem; border-radius: 6px; width: 200px; max-width: 100%;" > < button type = "button" class = "btn btn-primary" style = "padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick = "var v=document.getElementById(\'addressLabelInput\').value; setAddressLabel(\'${addressForJs}\', v); showToast(\'Label saved\', \'success\');" > Save < / b u t t o n > < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Watchlist < / d i v >
2026-03-28 00:21:18 -07:00
< div class = "info-value" > < button type = "button" id = "addressWatchlistBtn" class = "btn btn-primary" style = "padding: 0.35rem 0.75rem;" onclick = "(function(addr){ if(isInWatchlist(addr)){ removeFromWatchlist(addr); showToast(\'Removed from watchlist\', \'success\'); } else { addToWatchlist(addr); showToast(\'Added to watchlist\', \'success\'); } var b=document.getElementById(\'addressWatchlistBtn\'); if(b) b.innerHTML=isInWatchlist(addr)?\'<i class=\'fas fa-star\'></i> Remove from watchlist\':\'<i class=\'fas fa-star-o\'></i> Add to watchlist\'; })(\'${addressForJs}\')" > < i class = "fas fa-star${inWatchlist ? '' : '-o'}" > < / i > $ { i n W a t c h l i s t ? ' R e m o v e f r o m w a t c h l i s t ' : ' A d d t o w a t c h l i s t ' } < / b u t t o n > < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Token approvals < / d i v >
2026-03-28 00:21:18 -07:00
< div class = "info-value" > < a href = "https://revoke.cash/address/${encodedAddress}${CHAIN_ID === 138 ? '?chainId=138' : ''}" target = "_blank" rel = "noopener noreferrer" style = "color: var(--primary);" > Check token approvals < / a > < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Balance < / d i v >
< div class = "info-value" > $ { balanceEth } ETH < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Transaction Count < / d i v >
< div class = "info-value" > $ { a . transaction _count || 0 } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Token Count < / d i v >
< div class = "info-value" > $ { a . token _count || 0 } < / d i v >
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Type < / d i v >
< div class = "info-value" > $ { a . is _contract ? '<span class="badge badge-success">Contract</span>' + verifiedBadge + ( contractLink ? '<br/>' + contractLink : '' ) : '<span class="badge badge-primary">EOA</span>' } < / d i v >
< / d i v >
2026-03-28 14:09:23 -07:00
$ { a . creation _tx _hash ? ` <div class="info-row"><div class="info-label">Contract created in</div><div class="info-value"> ${ explorerTransactionLink ( a . creation _tx _hash , escapeHtml ( shortenHash ( a . creation _tx _hash ) ) , 'color: inherit; text-decoration: none;' ) } <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard(' ${ escapeHtml ( a . creation _tx _hash ) . replace ( /'/g , "\\'" ) } ', 'Copied');" aria-label="Copy"><i class="fas fa-copy"></i></button></div></div> ` : '' }
2026-02-16 03:09:53 -08:00
$ { a . first _seen _at ? ` <div class="info-row"><div class="info-label">First seen</div><div class="info-value"> ${ escapeHtml ( typeof a . first _seen _at === 'string' ? a . first _seen _at : new Date ( a . first _seen _at ) . toISOString ( ) ) } </div></div> ` : '' }
$ { a . last _seen _at ? ` <div class="info-row"><div class="info-label">Last seen</div><div class="info-value"> ${ escapeHtml ( typeof a . last _seen _at === 'string' ? a . last _seen _at : new Date ( a . last _seen _at ) . toISOString ( ) ) } </div></div> ` : '' }
< div class = "tabs" style = "margin-top: 1.5rem;" >
< button class = "tab active" onclick = "switchAddressTab('transactions', '${address}')" id = "addrTabTxs" aria - selected = "true" > Transactions < / b u t t o n >
< button class = "tab" onclick = "switchAddressTab('tokens', '${address}')" id = "addrTabTokens" > Token Balances < / b u t t o n >
< button class = "tab" onclick = "switchAddressTab('internal', '${address}')" id = "addrTabInternal" > Internal Txns < / b u t t o n >
< button class = "tab" onclick = "switchAddressTab('nfts', '${address}')" id = "addrTabNfts" > NFTs < / b u t t o n >
$ { isContract ? '<button class="tab" onclick="switchAddressTab(\'contract\', \'' + address + '\')" id="addrTabContract">Contract (ABI / Bytecode)</button>' : '' }
< / d i v >
< div id = "addressTabTransactions" class = "address-tab-content card" style = "margin-top: 1rem;" >
< div style = "display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;" >
< h3 style = "margin: 0;" > Recent Transactions < / h 3 >
< button type = "button" class = "btn btn-primary" onclick = "exportAddressTransactionsCSV('${address}')" style = "padding: 0.5rem 1rem;" > < i class = "fas fa-file-csv" > < / i > E x p o r t C S V < / b u t t o n >
< / d i v >
< div id = "addressTransactions" class = "loading" > Loading transactions ... < / d i v >
< / d i v >
< div id = "addressTabTokens" class = "address-tab-content card" style = "margin-top: 1rem; display: none;" >
< div style = "display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.5rem;" >
< h3 style = "margin: 0;" > Token Balances < / h 3 >
< button type = "button" class = "btn btn-primary" onclick = "exportAddressTokenBalancesCSV('${address}')" style = "padding: 0.5rem 1rem;" > < i class = "fas fa-file-csv" > < / i > E x p o r t C S V < / b u t t o n >
< / d i v >
< div id = "addressTokenBalances" class = "loading" > Loading ... < / d i v >
< / d i v >
< div id = "addressTabInternal" class = "address-tab-content card" style = "margin-top: 1rem; display: none;" >
< h3 > Internal Transactions < / h 3 >
< div id = "addressInternalTxns" class = "loading" > Loading ... < / d i v >
< / d i v >
< div id = "addressTabNfts" class = "address-tab-content card" style = "margin-top: 1rem; display: none;" >
< h3 > NFT Inventory < / h 3 >
< div id = "addressNftInventory" class = "loading" > Loading ... < / d i v >
< / d i v >
$ { isContract ? '<div id="addressTabContract" class="address-tab-content card" style="margin-top: 1rem; display: none;"><h3>Contract ABI & Bytecode</h3><div id="addressContractInfo" class="loading">Loading...</div></div>' : '' }
` ;
function switchAddressTab ( tabName , addr ) {
document . querySelectorAll ( '.address-tab-content' ) . forEach ( function ( el ) { el . style . display = 'none' ; } ) ;
document . querySelectorAll ( '.tabs .tab' ) . forEach ( function ( t ) { t . classList . remove ( 'active' ) ; } ) ;
if ( tabName === 'transactions' ) {
document . getElementById ( 'addressTabTransactions' ) . style . display = 'block' ;
document . getElementById ( 'addrTabTxs' ) . classList . add ( 'active' ) ;
} else if ( tabName === 'tokens' ) {
document . getElementById ( 'addressTabTokens' ) . style . display = 'block' ;
document . getElementById ( 'addrTabTokens' ) . classList . add ( 'active' ) ;
loadAddressTokenBalances ( addr ) ;
} else if ( tabName === 'internal' ) {
document . getElementById ( 'addressTabInternal' ) . style . display = 'block' ;
document . getElementById ( 'addrTabInternal' ) . classList . add ( 'active' ) ;
loadAddressInternalTxns ( addr ) ;
} else if ( tabName === 'nfts' ) {
document . getElementById ( 'addressTabNfts' ) . style . display = 'block' ;
document . getElementById ( 'addrTabNfts' ) . classList . add ( 'active' ) ;
loadAddressNftInventory ( addr ) ;
} else if ( tabName === 'contract' ) {
var contractPanel = document . getElementById ( 'addressTabContract' ) ;
var contractTab = document . getElementById ( 'addrTabContract' ) ;
if ( contractPanel && contractTab ) {
contractPanel . style . display = 'block' ;
contractTab . classList . add ( 'active' ) ;
loadAddressContractInfo ( addr ) ;
}
}
}
window . switchAddressTab = switchAddressTab ;
async function loadAddressTokenBalances ( addr ) {
const el = document . getElementById ( 'addressTokenBalances' ) ;
if ( ! el || el . dataset . loaded === '1' ) return ;
try {
const r = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/token-balances' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
const r2 = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/token_balances' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
const items = ( r . items || r ) . length ? ( r . items || r ) : ( r2 . items || r2 ) ;
el . dataset . loaded = '1' ;
2026-03-24 18:11:08 -07:00
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 ) ;
2026-02-16 03:09:53 -08:00
if ( ! items || items . length === 0 ) {
2026-03-24 18:11:08 -07:00
el . innerHTML = filterBar + '<p style="color: var(--text-light);">No token balances</p>' ;
2026-02-16 03:09:53 -08:00
return ;
}
2026-03-24 18:11:08 -07:00
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 ) {
2026-02-16 03:09:53 -08:00
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' ;
2026-03-28 14:09:23 -07:00
tbl += '<tr><td><a href="/token/' + encodeURIComponent ( contract ) + '">' + escapeHtml ( symbol ) + '</a></td><td>' + explorerAddressLink ( contract , escapeHtml ( shortenHash ( contract ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + escapeHtml ( displayBalance ) + '</td><td>' + escapeHtml ( type ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredItems . length === 0 ) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No token balances match the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
tbl += '</tbody></table>' ;
el . innerHTML = tbl ;
} catch ( e ) {
el . innerHTML = '<p class="error">Failed to load token balances</p>' ;
}
}
async function loadAddressNftInventory ( addr ) {
const el = document . getElementById ( 'addressNftInventory' ) ;
if ( ! el || el . dataset . loaded === '1' ) return ;
try {
const r = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/token-balances' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
const r2 = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/nft-inventory' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
const r3 = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/nft_tokens' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
var items = ( r2 . items || r2 ) . length ? ( r2 . items || r2 ) : ( r3 . items || r3 ) ;
if ( ! items || items . length === 0 ) {
var allBalances = ( r . items || r ) || [ ] ;
items = Array . isArray ( allBalances ) ? allBalances . filter ( function ( b ) {
var t = ( b . token || b ) . type || ( b . token _type || '' ) ;
return t === 'ERC-721' || t === 'ERC-1155' || String ( t ) . toLowerCase ( ) . indexOf ( 'nft' ) !== - 1 ;
} ) : [ ] ;
}
el . dataset . loaded = '1' ;
2026-03-24 18:11:08 -07:00
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 ) ;
2026-02-16 03:09:53 -08:00
if ( ! items || items . length === 0 ) {
2026-03-24 18:11:08 -07:00
el . innerHTML = filterBar + '<p style="color: var(--text-light);">No NFT tokens</p>' ;
2026-02-16 03:09:53 -08:00
return ;
}
2026-03-24 18:11:08 -07:00
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 ) {
2026-02-16 03:09:53 -08:00
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' ) ;
2026-03-28 14:09:23 -07:00
tbl += '<tr><td>' + explorerAddressLink ( contract , escapeHtml ( shortenHash ( contract ) ) , 'color: inherit; text-decoration: none;' ) + '</td>' ;
2026-02-16 03:09:53 -08:00
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>' ;
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredItems . length === 0 ) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No NFT inventory matches the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
tbl += '</tbody></table>' ;
el . innerHTML = tbl ;
} catch ( e ) {
el . innerHTML = '<p class="error">Failed to load NFT inventory</p>' ;
}
}
async function loadAddressInternalTxns ( addr ) {
const el = document . getElementById ( 'addressInternalTxns' ) ;
if ( ! el || el . dataset . loaded === '1' ) return ;
try {
const r = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/internal-transactions' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
const r2 = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/addresses/' + addr + '/internal_transactions' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
const items = ( r . items || r ) . length ? ( r . items || r ) : ( r2 . items || r2 ) ;
el . dataset . loaded = '1' ;
2026-03-24 18:11:08 -07:00
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 ) ;
2026-02-16 03:09:53 -08:00
if ( ! items || items . length === 0 ) {
2026-03-24 18:11:08 -07:00
el . innerHTML = filterBar + '<p style="color: var(--text-light);">No internal transactions</p>' ;
2026-02-16 03:09:53 -08:00
return ;
}
2026-03-24 18:11:08 -07:00
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 ) {
2026-02-16 03:09:53 -08:00
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 || '-' ;
2026-03-28 14:09:23 -07:00
tbl += '<tr><td>' + explorerBlockLink ( block , escapeHtml ( block ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + explorerAddressLink ( from , escapeHtml ( shortenHash ( from ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + explorerAddressLink ( to , escapeHtml ( shortenHash ( to ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + escapeHtml ( val ) + ' ETH</td><td>' + ( txHash !== '-' ? explorerTransactionLink ( txHash , escapeHtml ( shortenHash ( txHash ) ) , 'color: inherit; text-decoration: none;' ) : '-' ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredItems . length === 0 ) {
tbl += '<tr><td colspan="5" style="text-align:center; padding: 1rem;">No internal transactions match the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
tbl += '</tbody></table>' ;
el . innerHTML = tbl ;
} catch ( e ) {
el . innerHTML = '<p class="error">Failed to load internal transactions</p>' ;
}
}
async function loadAddressContractInfo ( addr ) {
const el = document . getElementById ( 'addressContractInfo' ) ;
if ( ! el || el . dataset . loaded === '1' ) return ;
try {
const urls = [
BLOCKSCOUT _API + '/v2/smart-contracts/' + addr ,
BLOCKSCOUT _API + '/v2/contracts/' + addr
] ;
let data = null ;
for ( var i = 0 ; i < urls . length ; i ++ ) {
try {
const r = await fetchAPIWithRetry ( urls [ i ] ) ;
if ( r && ( r . abi || r . bytecode || r . deployed _bytecode ) ) { data = r ; break ; }
} catch ( e ) { }
}
el . dataset . loaded = '1' ;
if ( ! data ) {
2026-03-28 00:21:18 -07:00
el . innerHTML = '<p style="color: var(--text-light);">Contract source not indexed. <a href="' + EXPLORER _ORIGIN + '/address/' + encodeURIComponent ( addr ) + '/contract" target="_blank" rel="noopener noreferrer">Verify on Blockscout</a></p>' ;
2026-02-16 03:09:53 -08:00
return ;
}
const abi = data . abi || data . abi _interface || [ ] ;
const abiStr = typeof abi === 'string' ? abi : JSON . stringify ( abi , null , 2 ) ;
const bytecode = data . bytecode || data . deployed _bytecode || data . creation _bytecode || '-' ;
let html = '<p><strong>Verification:</strong> ' + ( data . is _verified ? '<span class="badge badge-success">Verified</span>' : '<span class="badge badge-warning">Unverified</span>' ) + '</p>' ;
html += '<div style="margin-top: 1rem;"><h4>ABI <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'contractAbiText\').textContent); showToast(\'Copied\', \'success\');">Copy</button> <a href="javascript:void(0)" onclick="var blob=new Blob([document.getElementById(\'contractAbiText\').textContent],{type:\'application/json\'}); var a=document.createElement(\'a\'); a.href=URL.createObjectURL(blob); a.download=\'abi-' + addr . substring ( 0 , 10 ) + '.json\'; a.click(); URL.revokeObjectURL(a.href); return false;">Download</a></h4>' ;
html += '<pre id="contractAbiText" style="background: var(--light); padding: 1rem; border-radius: 8px; overflow: auto; max-height: 300px; font-size: 0.75rem;">' + escapeHtml ( abiStr ) + '</pre></div>' ;
html += '<div style="margin-top: 1rem;"><h4>Bytecode <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'contractBytecodeText\').textContent); showToast(\'Copied\', \'success\');">Copy</button></h4>' ;
html += '<pre id="contractBytecodeText" style="background: var(--light); padding: 1rem; border-radius: 8px; overflow: auto; max-height: 150px; font-size: 0.7rem; word-break: break-all;">' + escapeHtml ( String ( bytecode ) . substring ( 0 , 500 ) ) + ( String ( bytecode ) . length > 500 ? '...' : '' ) + '</pre></div>' ;
var viewFns = ( Array . isArray ( abi ) ? abi : [ ] ) . filter ( function ( x ) { return x . type === 'function' && ( x . stateMutability === 'view' || x . stateMutability === 'pure' || ( x . constant === true ) ) ; } ) ;
if ( viewFns . length > 0 ) {
html += '<div style="margin-top: 1.5rem;"><h4><i class="fas fa-readme"></i> Read contract</h4><p style="color: var(--text-light); font-size: 0.875rem;">Call view/pure functions (requires ethers.js).</p>' ;
html += '<div style="margin-top: 0.5rem;"><label for="readContractSelect">Function:</label><select id="readContractSelect" style="margin-left: 0.5rem; padding: 0.35rem; border-radius: 6px; min-width: 200px;">' ;
viewFns . forEach ( function ( fn ) { html += '<option value="' + escapeHtml ( fn . name ) + '">' + escapeHtml ( fn . name ) + '(' + ( fn . inputs || [ ] ) . map ( function ( i ) { return i . type ; } ) . join ( ',' ) + ')</option>' ; } ) ;
html += '</select></div><div id="readContractInputs" style="margin-top: 0.5rem;"></div>' ;
html += '<button type="button" class="btn btn-primary" style="margin-top: 0.5rem;" id="readContractBtn">Query</button><pre id="readContractResult" style="background: var(--light); padding: 1rem; border-radius: 8px; margin-top: 0.5rem; min-height: 2rem; font-size: 0.8rem; display: none;"></pre></div>' ;
}
var writeFns = ( Array . isArray ( abi ) ? abi : [ ] ) . filter ( function ( x ) { return x . type === 'function' && x . stateMutability !== 'view' && x . stateMutability !== 'pure' && ! x . constant ; } ) ;
if ( writeFns . length > 0 ) {
html += '<div style="margin-top: 1.5rem;"><h4><i class="fas fa-pen"></i> Write contract</h4><p style="color: var(--text-light); font-size: 0.875rem;">Connect wallet to send transactions.</p>' ;
html += '<div style="margin-top: 0.5rem;"><label>Function:</label><select id="writeContractSelect" style="margin-left: 0.5rem; padding: 0.35rem; border-radius: 6px; min-width: 200px;">' ;
writeFns . forEach ( function ( fn ) { var pay = ( fn . stateMutability === 'payable' ) ; html += '<option value="' + escapeHtml ( fn . name ) + '" data-payable="' + pay + '">' + escapeHtml ( fn . name ) + '(' + ( fn . inputs || [ ] ) . map ( function ( i ) { return i . type ; } ) . join ( ',' ) + ')' + ( pay ? ' payable' : '' ) + '</option>' ; } ) ;
html += '</select></div><div id="writeContractInputs" style="margin-top: 0.5rem;"></div>' ;
html += '<div id="writeContractValueRow" style="margin-top: 0.5rem; display: none;"><label>Value (ETH):</label><input type="text" id="writeContractValue" placeholder="0" style="margin-left: 0.5rem; padding: 0.35rem; border-radius: 6px; width: 120px;"></div>' ;
html += '<button type="button" class="btn btn-primary" style="margin-top: 0.5rem;" id="writeContractBtn">Write</button><pre id="writeContractResult" style="background: var(--light); padding: 1rem; border-radius: 8px; margin-top: 0.5rem; min-height: 2rem; font-size: 0.8rem; display: none;"></pre></div>' ;
}
2026-03-28 00:21:18 -07:00
html += '<p style="margin-top: 0.5rem;"><a href="' + EXPLORER _ORIGIN + '/address/' + encodeURIComponent ( addr ) + '/contract" target="_blank" rel="noopener noreferrer" style="color: var(--primary);">Read / Write contract on Blockscout</a></p>' ;
2026-02-16 03:09:53 -08:00
el . innerHTML = html ;
if ( viewFns . length > 0 ) {
( function setupReadContract ( contractAddr , abiJson , viewFunctions ) {
var selectEl = document . getElementById ( 'readContractSelect' ) ;
var inputsEl = document . getElementById ( 'readContractInputs' ) ;
var resultEl = document . getElementById ( 'readContractResult' ) ;
var btnEl = document . getElementById ( 'readContractBtn' ) ;
function renderInputs ( ) {
var name = selectEl && selectEl . value ;
var fn = viewFunctions . find ( function ( f ) { return f . name === name ; } ) ;
if ( ! inputsEl || ! fn ) return ;
var inputs = fn . inputs || [ ] ;
if ( inputs . length === 0 ) { inputsEl . innerHTML = '' ; return ; }
var h = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">' ;
inputs . forEach ( function ( inp , i ) {
h += '<label>' + escapeHtml ( inp . name || 'arg' + i ) + ' (' + escapeHtml ( inp . type ) + '):</label><input type="text" id="readArg' + i + '" placeholder="' + escapeHtml ( inp . type ) + '" style="padding: 0.35rem; border-radius: 6px; min-width: 120px;">' ;
} ) ;
h += '</div>' ;
inputsEl . innerHTML = h ;
}
if ( selectEl ) selectEl . addEventListener ( 'change' , renderInputs ) ;
renderInputs ( ) ;
if ( btnEl ) btnEl . addEventListener ( 'click' , function ( ) {
if ( typeof ethers === 'undefined' ) { showToast ( 'Ethers.js not loaded. Refresh the page.' , 'error' ) ; return ; }
var name = selectEl && selectEl . value ;
var fn = viewFunctions . find ( function ( f ) { return f . name === name ; } ) ;
if ( ! fn || ! resultEl ) return ;
var inputs = fn . inputs || [ ] ;
var args = [ ] ;
for ( var i = 0 ; i < inputs . length ; i ++ ) {
var inp = inputs [ i ] ;
var val = document . getElementById ( 'readArg' + i ) && document . getElementById ( 'readArg' + i ) . value ;
if ( val === undefined || val === '' ) val = '' ;
var t = ( inp . type || '' ) . toLowerCase ( ) ;
if ( t . indexOf ( 'uint' ) === 0 || t === 'int256' ) args . push ( val ? ( val . trim ( ) ? BigInt ( val ) : 0 ) : 0 ) ;
else if ( t === 'bool' ) args . push ( val === 'true' || val === '1' ) ;
else if ( t . indexOf ( 'address' ) === 0 ) args . push ( val && val . trim ( ) ? val . trim ( ) : '0x0000000000000000000000000000000000000000' ) ;
else args . push ( val ) ;
}
resultEl . style . display = 'block' ;
resultEl . textContent = 'Calling...' ;
var provider = new ethers . providers . JsonRpcProvider ( RPC _URL ) ;
var contract = new ethers . Contract ( contractAddr , abiJson , provider ) ;
contract [ name ] . apply ( contract , args ) . then ( function ( res ) {
if ( Array . isArray ( res ) ) resultEl . textContent = JSON . stringify ( res , null , 2 ) ;
else if ( res != null && typeof res . toString === 'function' ) resultEl . textContent = res . toString ( ) ;
else resultEl . textContent = JSON . stringify ( res , null , 2 ) ;
} ) . catch ( function ( err ) {
resultEl . textContent = 'Error: ' + ( err . message || String ( err ) ) ;
} ) ;
} ) ;
} ) ( addr , abi , viewFns ) ;
}
if ( writeFns . length > 0 ) {
( function setupWriteContract ( contractAddr , abiJson , writeFunctions ) {
var selectEl = document . getElementById ( 'writeContractSelect' ) ;
var inputsEl = document . getElementById ( 'writeContractInputs' ) ;
var valueRow = document . getElementById ( 'writeContractValueRow' ) ;
var valueEl = document . getElementById ( 'writeContractValue' ) ;
var resultEl = document . getElementById ( 'writeContractResult' ) ;
var btnEl = document . getElementById ( 'writeContractBtn' ) ;
function renderWriteInputs ( ) {
var name = selectEl && selectEl . value ;
var opt = selectEl && selectEl . options [ selectEl . selectedIndex ] ;
var payable = opt && opt . getAttribute ( 'data-payable' ) === 'true' ;
if ( valueRow ) valueRow . style . display = payable ? 'block' : 'none' ;
var fn = writeFunctions . find ( function ( f ) { return f . name === name ; } ) ;
if ( ! inputsEl || ! fn ) return ;
var inputs = fn . inputs || [ ] ;
var h = '<div style="display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;">' ;
inputs . forEach ( function ( inp , i ) {
h += '<label>' + escapeHtml ( inp . name || 'arg' + i ) + ' (' + escapeHtml ( inp . type ) + '):</label><input type="text" id="writeArg' + i + '" placeholder="' + escapeHtml ( inp . type ) + '" style="padding: 0.35rem; border-radius: 6px; min-width: 120px;">' ;
} ) ;
h += '</div>' ;
inputsEl . innerHTML = h ;
}
if ( selectEl ) selectEl . addEventListener ( 'change' , renderWriteInputs ) ;
renderWriteInputs ( ) ;
if ( btnEl ) btnEl . addEventListener ( 'click' , function ( ) {
if ( typeof ethers === 'undefined' ) { showToast ( 'Ethers.js not loaded. Refresh the page.' , 'error' ) ; return ; }
if ( ! window . ethereum ) { showToast ( 'Connect MetaMask to write.' , 'error' ) ; return ; }
var name = selectEl && selectEl . value ;
var fn = writeFunctions . find ( function ( f ) { return f . name === name ; } ) ;
if ( ! fn || ! resultEl ) return ;
var inputs = fn . inputs || [ ] ;
var args = [ ] ;
for ( var i = 0 ; i < inputs . length ; i ++ ) {
var inp = inputs [ i ] ;
var val = document . getElementById ( 'writeArg' + i ) && document . getElementById ( 'writeArg' + i ) . value ;
if ( val === undefined || val === '' ) val = '' ;
var t = ( inp . type || '' ) . toLowerCase ( ) ;
if ( t . indexOf ( 'uint' ) === 0 || t === 'int256' ) args . push ( val ? ( val . trim ( ) ? BigInt ( val ) : 0 ) : 0 ) ;
else if ( t === 'bool' ) args . push ( val === 'true' || val === '1' ) ;
else if ( t . indexOf ( 'address' ) === 0 ) args . push ( val && val . trim ( ) ? val . trim ( ) : '0x0000000000000000000000000000000000000000' ) ;
else args . push ( val ) ;
}
var valueWei = '0' ;
if ( fn . stateMutability === 'payable' && valueEl && valueEl . value ) {
try { valueWei = ethers . utils . parseEther ( valueEl . value . trim ( ) || '0' ) . toString ( ) ; } catch ( e ) { showToast ( 'Invalid ETH value' , 'error' ) ; return ; }
}
resultEl . style . display = 'block' ;
resultEl . textContent = 'Confirm in wallet...' ;
var provider = new ethers . providers . Web3Provider ( window . ethereum ) ;
provider . send ( 'eth_requestAccounts' , [ ] ) . then ( function ( ) {
var signer = provider . getSigner ( ) ;
var contract = new ethers . Contract ( contractAddr , abiJson , signer ) ;
var overrides = valueWei !== '0' ? { value : valueWei } : { } ;
return contract [ name ] . apply ( contract , args . concat ( [ overrides ] ) ) ;
} ) . then ( function ( tx ) {
resultEl . textContent = 'Tx hash: ' + tx . hash + '\nWaiting for confirmation...' ;
return tx . wait ( ) ;
} ) . then ( function ( receipt ) {
resultEl . textContent = 'Success. Block: ' + receipt . blockNumber + ', Tx: ' + receipt . transactionHash ;
showToast ( 'Transaction confirmed' , 'success' ) ;
} ) . catch ( function ( err ) {
resultEl . textContent = 'Error: ' + ( err . message || String ( err ) ) ;
showToast ( err . message || 'Transaction failed' , 'error' ) ;
} ) ;
} ) ;
} ) ( addr , abi , writeFns ) ;
}
} catch ( e ) {
el . innerHTML = '<p class="error">Failed to load contract info</p>' ;
}
}
try {
let txs ;
if ( CHAIN _ID === 138 ) {
const response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions?address= ${ address } &page=1&page_size=10 ` ) ;
const rawTxs = response . items || [ ] ;
txs = { data : rawTxs . map ( normalizeTransaction ) . filter ( tx => tx !== null ) } ;
} else {
txs = await fetchAPIWithRetry ( ` ${ API _BASE } /v1/transactions?from_address= ${ address } &page_size=10 ` ) ;
}
const txContainer = document . getElementById ( 'addressTransactions' ) ;
if ( txContainer ) {
2026-03-24 18:11:08 -07:00
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 ) ;
2026-02-16 03:09:53 -08:00
if ( txs . data && txs . data . length > 0 ) {
2026-03-24 18:11:08 -07:00
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 ) {
2026-03-28 14:09:23 -07:00
txHtml += '<tr onclick="showTransactionDetail(\'' + escapeHtml ( tx . hash ) + '\')" style="cursor: pointer;"><td>' + explorerTransactionLink ( tx . hash , escapeHtml ( shortenHash ( tx . hash ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + explorerBlockLink ( String ( tx . block _number ) , escapeHtml ( String ( tx . block _number ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + explorerAddressLink ( tx . from , formatAddressWithLabel ( tx . from ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + ( tx . to ? explorerAddressLink ( tx . to , formatAddressWithLabel ( tx . to ) , 'color: inherit; text-decoration: none;' ) : 'N/A' ) + '</td><td>' + escapeHtml ( formatEther ( tx . value || '0' ) ) + ' ETH</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredTxs . length === 0 ) {
txHtml += '<tr><td colspan="5" style="text-align:center; padding: 1rem;">No transactions match the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
txHtml += '</tbody></table>' ;
txContainer . innerHTML = txHtml ;
} else {
2026-03-24 18:11:08 -07:00
txContainer . innerHTML = filterBar + '<p>No transactions found</p>' ;
2026-02-16 03:09:53 -08:00
}
}
} catch ( e ) {
const txContainer = document . getElementById ( 'addressTransactions' ) ;
if ( txContainer ) txContainer . innerHTML = '<p>Failed to load transactions</p>' ;
}
} else {
container . innerHTML = '<div class="error">Address not found</div>' ;
}
} catch ( error ) {
container . innerHTML = '<div class="error">Failed to load address: ' + escapeHtml ( error . message ) + '</div>' ;
}
}
2026-03-28 13:59:00 -07:00
window . _showAddressDetail = renderAddressDetail ;
2026-03-02 12:14:13 -08:00
// Keep wrapper (do not overwrite) so all calls go through setTimeout and avoid stack overflow
2026-02-16 03:09:53 -08:00
async function showTokenDetail ( tokenAddress ) {
if ( ! /^0x[a-fA-F0-9]{40}$/ . test ( tokenAddress ) ) return ;
currentDetailKey = 'token:' + tokenAddress . toLowerCase ( ) ;
showView ( 'tokenDetail' ) ;
updatePath ( '/token/' + tokenAddress ) ;
var container = document . getElementById ( 'tokenDetail' ) ;
updateBreadcrumb ( 'token' , tokenAddress ) ;
container . innerHTML = createSkeletonLoader ( 'detail' ) ;
try {
var urls = [ BLOCKSCOUT _API + '/v2/tokens/' + tokenAddress , BLOCKSCOUT _API + '/v2/token/' + tokenAddress ] ;
var data = null ;
for ( var i = 0 ; i < urls . length ; i ++ ) {
try {
var r = await fetchAPIWithRetry ( urls [ i ] ) ;
if ( r && ( r . symbol || r . name || r . total _supply != null ) ) { data = r ; break ; }
} catch ( e ) { }
}
if ( ! data ) {
container . innerHTML = '<p class="error">Token not found or not indexed.</p><p><a href="/address/' + encodeURIComponent ( tokenAddress ) + '">View as address</a></p>' ;
return ;
}
2026-02-22 15:35:45 -08:00
var knownTokenDetail = {
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' : { name : 'Wrapped Ether' , symbol : 'WETH' , decimals : 18 } ,
'0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f' : { name : 'Wrapped Ether v10' , symbol : 'WETH' , decimals : 18 }
} ;
var known = knownTokenDetail [ tokenAddress . toLowerCase ( ) ] ;
var name = ( known && known . name ) || data . name || '-' ;
var symbol = ( known && known . symbol ) || data . symbol || '-' ;
var decimals = ( known && known . decimals != null ) ? known . decimals : ( data . decimals != null ? data . decimals : 18 ) ;
decimals = parseInt ( decimals , 10 ) ;
if ( isNaN ( decimals ) || decimals < 0 || decimals > 255 ) decimals = 18 ;
2026-02-16 03:09:53 -08:00
var supply = data . total _supply != null ? data . total _supply : ( data . total _supply _raw || '0' ) ;
2026-02-22 15:35:45 -08:00
var supplyNum = Number ( supply ) / Math . pow ( 10 , decimals ) ;
2026-02-16 03:09:53 -08:00
var holders = data . holders _count != null ? data . holders _count : ( data . holder _count || '-' ) ;
var transfersResp = null ;
try {
transfersResp = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/tokens/' + tokenAddress + '/transfers?page=1&page_size=10' ) . catch ( function ( ) { return { items : [ ] } ; } ) ;
} catch ( e ) { }
var transfers = ( transfersResp && transfersResp . items ) ? transfersResp . items : [ ] ;
2026-02-22 15:35:45 -08:00
var addrEsc = tokenAddress . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) ;
var symbolEsc = String ( symbol ) . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) ;
2026-03-02 12:14:13 -08:00
var html = '<div class="token-detail-actions" style="margin-bottom: 1.25rem;"><button type="button" class="btn btn-primary btn-add-token-wallet-prominent" onclick="window.addTokenToWallet && window.addTokenToWallet(\'' + addrEsc + '\', \'' + symbolEsc + '\', ' + decimals + ');" aria-label="Add token to wallet"><i class="fas fa-wallet" aria-hidden="true"></i> Add to wallet (MetaMask)</button></div>' ;
2026-03-28 14:09:23 -07:00
html += '<div class="info-row"><div class="info-label">Contract</div><div class="info-value">' + explorerAddressLink ( tokenAddress , escapeHtml ( tokenAddress ) , 'color: inherit; text-decoration: none;' ) + ' <button type="button" class="btn-add-token-wallet" onclick="window.addTokenToWallet && window.addTokenToWallet(\'' + addrEsc + '\', \'' + symbolEsc + '\', ' + decimals + ');" aria-label="Add to wallet" title="Add to wallet"><i class="fas fa-wallet" aria-hidden="true"></i></button></div></div>' ;
2026-02-16 03:09:53 -08:00
html += '<div class="info-row"><div class="info-label">Name</div><div class="info-value">' + escapeHtml ( name ) + '</div></div>' ;
html += '<div class="info-row"><div class="info-label">Symbol</div><div class="info-value">' + escapeHtml ( symbol ) + '</div></div>' ;
html += '<div class="info-row"><div class="info-label">Decimals</div><div class="info-value">' + decimals + '</div></div>' ;
html += '<div class="info-row"><div class="info-label">Total Supply</div><div class="info-value">' + supplyNum . toLocaleString ( undefined , { maximumFractionDigits : 6 } ) + '</div></div>' ;
html += '<div class="info-row"><div class="info-label">Holders</div><div class="info-value">' + ( holders !== '-' ? formatNumber ( holders ) : '-' ) + '</div></div>' ;
2026-03-24 18:11:08 -07:00
const transfersFilter = getExplorerPageFilter ( 'tokenTransfers' ) ;
const transfersFilterBar = renderPageFilterBar ( 'tokenTransfers' , 'Filter by from, to, value, or tx hash...' , 'Filters the recent transfers below.' , 'showTokenDetail(\'' + addrEsc + '\')' ) ;
2026-02-16 03:09:53 -08:00
html += '<div class="card" style="margin-top: 1rem;"><h3>Recent Transfers</h3>' ;
if ( transfers . length === 0 ) {
2026-03-24 18:11:08 -07:00
html += transfersFilterBar + '<p style="color: var(--text-light);">No transfers</p>' ;
2026-02-16 03:09:53 -08:00
} else {
2026-03-24 18:11:08 -07:00
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 ) {
2026-02-16 03:09:53 -08:00
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 || '' ;
2026-03-28 14:09:23 -07:00
html += '<tr><td>' + explorerAddressLink ( from , escapeHtml ( shortenHash ( from ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + explorerAddressLink ( to , escapeHtml ( shortenHash ( to ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + escapeHtml ( v . toLocaleString ( undefined , { maximumFractionDigits : 6 } ) ) + '</td><td>' + ( txHash ? explorerTransactionLink ( txHash , escapeHtml ( shortenHash ( txHash ) ) , 'color: inherit; text-decoration: none;' ) : '-' ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-03-24 18:11:08 -07:00
if ( filteredTransfers . length === 0 ) {
html += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No transfers match the current filter.</td></tr>' ;
}
2026-02-16 03:09:53 -08:00
html += '</tbody></table>' ;
}
html += '</div>' ;
container . innerHTML = html ;
} catch ( err ) {
container . innerHTML = '<div class="error">Failed to load token: ' + escapeHtml ( err . message || 'Unknown' ) + '</div>' ;
}
}
window . showTokenDetail = showTokenDetail ;
async function showNftDetail ( contractAddress , tokenId ) {
if ( ! /^0x[a-fA-F0-9]{40}$/ . test ( contractAddress ) ) return ;
currentDetailKey = 'nft:' + contractAddress . toLowerCase ( ) + ':' + tokenId ;
showView ( 'nftDetail' ) ;
updatePath ( '/nft/' + contractAddress + '/' + tokenId ) ;
var container = document . getElementById ( 'nftDetail' ) ;
updateBreadcrumb ( 'nft' , contractAddress , tokenId ) ;
container . innerHTML = createSkeletonLoader ( 'detail' ) ;
try {
var urls = [ BLOCKSCOUT _API + '/v2/tokens/' + contractAddress + '/nft/' + tokenId , BLOCKSCOUT _API + '/v2/nft/' + contractAddress + '/' + tokenId ] ;
var data = null ;
for ( var i = 0 ; i < urls . length ; i ++ ) {
try {
var r = await fetchAPIWithRetry ( urls [ i ] ) ;
if ( r ) { data = r ; break ; }
} catch ( e ) { }
}
2026-03-28 14:09:23 -07:00
var html = '<div class="info-row"><div class="info-label">Contract</div><div class="info-value">' + explorerAddressLink ( contractAddress , escapeHtml ( contractAddress ) , 'color: inherit; text-decoration: none;' ) + '</div></div>' ;
2026-02-16 03:09:53 -08:00
html += '<div class="info-row"><div class="info-label">Token ID</div><div class="info-value">' + escapeHtml ( String ( tokenId ) ) + '</div></div>' ;
if ( data ) {
if ( data . metadata && data . metadata . image ) {
html += '<div class="info-row"><div class="info-label">Image</div><div class="info-value"><img src="' + escapeHtml ( data . metadata . image ) + '" alt="NFT" style="max-width: 200px; border-radius: 8px;" onerror="this.style.display=\'none\'"></div></div>' ;
}
if ( data . name ) html += '<div class="info-row"><div class="info-label">Name</div><div class="info-value">' + escapeHtml ( data . name ) + '</div></div>' ;
if ( data . description ) html += '<div class="info-row"><div class="info-label">Description</div><div class="info-value">' + escapeHtml ( data . description ) + '</div></div>' ;
2026-03-28 14:09:23 -07:00
if ( data . owner ) { var ownerAddr = ( data . owner . hash || data . owner ) ; html += '<div class="info-row"><div class="info-label">Owner</div><div class="info-value">' + explorerAddressLink ( ownerAddr , escapeHtml ( ownerAddr ) , 'color: inherit; text-decoration: none;' ) + '</div></div>' ; }
2026-02-16 03:09:53 -08:00
if ( data . metadata && data . metadata . attributes && Array . isArray ( data . metadata . attributes ) ) {
html += '<div class="info-row"><div class="info-label">Traits</div><div class="info-value"><div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">' ;
data . metadata . attributes . forEach ( function ( attr ) {
var traitType = ( attr . trait _type || attr . traitType || '' ) ; var val = ( attr . value != null ? attr . value : '' ) ;
if ( traitType || val ) html += '<span style="background: var(--light); padding: 0.25rem 0.5rem; border-radius: 6px; font-size: 0.85rem;">' + escapeHtml ( traitType ) + ': ' + escapeHtml ( String ( val ) ) + '</span>' ;
} ) ;
html += '</div></div></div>' ;
}
}
2026-03-28 00:21:18 -07:00
html += '<p style="margin-top: 1rem;"><a href="' + EXPLORER _ORIGIN + '/token/' + encodeURIComponent ( contractAddress ) + '/instance/' + encodeURIComponent ( tokenId ) + '" target="_blank" rel="noopener noreferrer" style="color: var(--primary);">View on Blockscout</a></p>' ;
2026-02-16 03:09:53 -08:00
container . innerHTML = html ;
} catch ( err ) {
container . innerHTML = '<div class="error">Failed to load NFT: ' + escapeHtml ( err . message || 'Unknown' ) + '</div>' ;
}
}
window . showNftDetail = showNftDetail ;
function showSearchResultsList ( items , query ) {
showView ( 'searchResults' ) ;
var container = document . getElementById ( 'searchResultsContent' ) ;
if ( ! container ) return ;
var html = '<p style="color: var(--text-light); margin-bottom: 1rem;">Found ' + items . length + ' result(s) for "' + escapeHtml ( query ) + '". Click a row to open.</p>' ;
html += '<table class="table"><thead><tr><th>Type</th><th>Value</th></tr></thead><tbody>' ;
items . forEach ( function ( item ) {
var type = ( item . type || item . address _type || '' ) . toLowerCase ( ) ;
var label = item . name || item . symbol || item . address _hash || item . hash || item . tx _hash || ( item . block _number != null ? 'Block #' + item . block _number : '' ) || '-' ;
var addr , txHash , blockNum , tokenAddr ;
if ( item . token _address || item . token _contract _address _hash ) {
tokenAddr = item . token _address || item . token _contract _address _hash ;
if ( /^0x[a-f0-9]{40}$/i . test ( tokenAddr ) ) {
html += '<tr style="cursor: pointer;" onclick="showTokenDetail(\'' + escapeHtml ( tokenAddr ) + '\')"><td>Token</td><td class="hash">' + escapeHtml ( shortenHash ( tokenAddr ) ) + ' ' + ( item . name || item . symbol ? ' (' + escapeHtml ( item . name || item . symbol ) + ')' : '' ) + '</td></tr>' ;
return ;
}
}
if ( item . address _hash || item . hash ) {
addr = item . address _hash || item . hash ;
if ( /^0x[a-f0-9]{40}$/i . test ( addr ) ) {
2026-03-28 14:09:23 -07:00
html += '<tr><td>Address</td><td>' + explorerAddressLink ( addr , escapeHtml ( shortenHash ( addr ) ) , 'color: inherit; text-decoration: none;' ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
return ;
}
}
if ( item . tx _hash || ( item . hash && item . hash . length === 66 ) ) {
txHash = item . tx _hash || item . hash ;
if ( /^0x[a-f0-9]{64}$/i . test ( txHash ) ) {
2026-03-28 14:09:23 -07:00
html += '<tr><td>Transaction</td><td>' + explorerTransactionLink ( txHash , escapeHtml ( shortenHash ( txHash ) ) , 'color: inherit; text-decoration: none;' ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
return ;
}
}
if ( item . block _number != null ) {
blockNum = String ( item . block _number ) ;
2026-03-28 14:09:23 -07:00
html += '<tr><td>Block</td><td>' + explorerBlockLink ( blockNum , '#' + escapeHtml ( blockNum ) , 'color: inherit; text-decoration: none;' ) + '</td></tr>' ;
2026-02-16 03:09:53 -08:00
return ;
}
html += '<tr><td>' + escapeHtml ( type || 'Unknown' ) + '</td><td>' + escapeHtml ( String ( label ) . substring ( 0 , 80 ) ) + '</td></tr>' ;
} ) ;
html += '</tbody></table>' ;
container . innerHTML = html ;
}
window . showSearchResultsList = showSearchResultsList ;
async function handleSearch ( query ) {
query = query . trim ( ) ;
if ( ! query ) {
showToast ( 'Please enter a search query' , 'info' ) ;
return ;
}
2026-03-27 12:02:36 -07:00
saveSmartSearchHistory ( query ) ;
closeSmartSearchModal ( ) ;
2026-02-16 03:09:53 -08:00
const normalizedQuery = query . toLowerCase ( ) . replace ( /\s/g , '' ) ;
try {
if ( /^0x[a-f0-9]{40}$/i . test ( normalizedQuery ) ) {
await showAddressDetail ( normalizedQuery ) ;
return ;
}
if ( /^0x[a-f0-9]{64}$/i . test ( normalizedQuery ) ) {
await showTransactionDetail ( normalizedQuery ) ;
return ;
}
if ( /^\d+$/ . test ( query ) ) {
await showBlockDetail ( query ) ;
return ;
}
if ( /^0x[a-f0-9]+$/i . test ( normalizedQuery ) ) {
const blockNum = parseInt ( normalizedQuery , 16 ) ;
if ( ! isNaN ( blockNum ) ) {
await showBlockDetail ( blockNum . toString ( ) ) ;
return ;
}
}
if ( CHAIN _ID === 138 ) {
var searchResults = null ;
try {
searchResults = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/search?q=' + encodeURIComponent ( query ) ) ;
} catch ( e ) { }
if ( searchResults && searchResults . items && searchResults . items . length > 0 ) {
showSearchResultsList ( searchResults . items , query ) ;
return ;
}
if ( /^0x[a-f0-9]{8,64}$/i . test ( normalizedQuery ) ) {
try {
var txResp = await fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/transactions/' + normalizedQuery ) ;
if ( txResp && ( txResp . hash || txResp . tx _hash ) ) {
var fullHash = txResp . hash || txResp . tx _hash ;
await showTransactionDetail ( fullHash ) ;
return ;
}
} catch ( e ) { }
showToast ( 'No unique result for partial hash. Use at least 0x + 8 hex characters, or full tx hash (0x + 64 hex).' , 'info' ) ;
return ;
}
}
showToast ( 'Invalid search. Try address (0x...40 hex), tx hash (0x...64 hex or 0x+8 hex), block number, or token/contract name.' , 'error' ) ;
} catch ( error ) {
console . error ( 'Search error:' , error ) ;
showToast ( 'Failed to load search results: ' + ( error . message || 'Unknown error' ) , 'error' ) ;
}
}
window . handleSearch = handleSearch ;
function getTimeAgo ( date ) {
if ( ! date || ! ( date instanceof Date ) || isNaN ( date . getTime ( ) ) ) {
return 'N/A' ;
}
const now = new Date ( ) ;
const diffMs = now - date ;
const diffSecs = Math . floor ( diffMs / 1000 ) ;
const diffMins = Math . floor ( diffSecs / 60 ) ;
const diffHours = Math . floor ( diffMins / 60 ) ;
const diffDays = Math . floor ( diffHours / 24 ) ;
if ( diffSecs < 60 ) {
return ` ${ diffSecs } s ago ` ;
} else if ( diffMins < 60 ) {
return ` ${ diffMins } m ago ` ;
} else if ( diffHours < 24 ) {
return ` ${ diffHours } h ago ` ;
} else if ( diffDays < 7 ) {
return ` ${ diffDays } d ago ` ;
} else {
return date . toLocaleDateString ( ) ;
}
}
function formatNumber ( num ) {
return parseInt ( num || 0 ) . toLocaleString ( ) ;
}
function shortenHash ( hash , length = 10 ) {
if ( ! hash ) return 'N/A' ;
// Convert to string if it's not already
const hashStr = String ( hash ) ;
if ( hashStr . length <= length * 2 + 2 ) return hashStr ;
return hashStr . substring ( 0 , length + 2 ) + '...' + hashStr . substring ( hashStr . length - length ) ;
}
function formatEther ( wei , unit = 'ether' ) {
if ( typeof wei === 'string' && wei . startsWith ( '0x' ) ) {
wei = BigInt ( wei ) ;
}
const weiNum = typeof wei === 'bigint' ? Number ( wei ) : parseFloat ( wei ) ;
const ether = weiNum / Math . pow ( 10 , unit === 'gwei' ? 9 : 18 ) ;
return ether . toFixed ( 6 ) . replace ( /\.?0+$/ , '' ) ;
}
2026-03-27 13:37:53 -07:00
function getExplorerAIPageContext ( ) {
return {
2026-03-28 00:21:18 -07:00
path : ( window . location && window . location . pathname ) ? window . location . pathname : '/' ,
2026-03-27 13:37:53 -07:00
view : currentView || 'home'
} ;
}
function renderExplorerAIMessages ( ) {
var list = document . getElementById ( 'explorerAIMessageList' ) ;
var status = document . getElementById ( 'explorerAIStatus' ) ;
if ( ! list ) return ;
list . innerHTML = _explorerAIState . messages . map ( function ( message ) {
var isAssistant = message . role === 'assistant' ;
var bubbleStyle = isAssistant
? 'background: rgba(37,99,235,0.10); border:1px solid rgba(37,99,235,0.18);'
: 'background: rgba(15,23,42,0.06); border:1px solid rgba(148,163,184,0.25);' ;
return '<div style="display:flex; justify-content:' + ( isAssistant ? 'flex-start' : 'flex-end' ) + ';">' +
'<div style="max-width: 88%; padding: 0.85rem 0.95rem; border-radius: 16px; ' + bubbleStyle + '">' +
'<div style="font-size:0.72rem; letter-spacing:0.06em; text-transform:uppercase; color:var(--text-light); margin-bottom:0.35rem;">' + ( isAssistant ? 'Explorer AI' : 'You' ) + '</div>' +
'<div style="white-space:pre-wrap; line-height:1.55;">' + escapeHtml ( message . content || '' ) + '</div>' +
'</div>' +
'</div>' ;
} ) . join ( '' ) ;
if ( _explorerAIState . loading ) {
list . innerHTML += '<div style="display:flex; justify-content:flex-start;"><div style="padding:0.8rem 0.95rem; border-radius:16px; background:rgba(37,99,235,0.08); border:1px solid rgba(37,99,235,0.16); color:var(--text-light);">Thinking through indexed data, live routes, and docs...</div></div>' ;
}
list . scrollTop = list . scrollHeight ;
if ( status ) {
status . textContent = _explorerAIState . loading
? 'Querying explorer data and the model...'
: 'Read-only assistant using indexed explorer data, route APIs, and curated docs.' ;
}
}
function setExplorerAIOpen ( open ) {
_explorerAIState . open = ! ! open ;
var panel = document . getElementById ( 'explorerAIPanel' ) ;
var button = document . getElementById ( 'explorerAIFab' ) ;
if ( panel ) panel . style . display = open ? 'flex' : 'none' ;
if ( button ) button . setAttribute ( 'aria-expanded' , open ? 'true' : 'false' ) ;
if ( open ) {
renderExplorerAIMessages ( ) ;
var input = document . getElementById ( 'explorerAIInput' ) ;
if ( input ) setTimeout ( function ( ) { input . focus ( ) ; } , 30 ) ;
}
}
function toggleExplorerAIPanel ( forceOpen ) {
if ( typeof forceOpen === 'boolean' ) {
setExplorerAIOpen ( forceOpen ) ;
return ;
}
setExplorerAIOpen ( ! _explorerAIState . open ) ;
}
window . toggleExplorerAIPanel = toggleExplorerAIPanel ;
function buildExplorerAISourceSummary ( context ) {
if ( ! context || ! Array . isArray ( context . sources ) || ! context . sources . length ) return '' ;
return context . sources . map ( function ( source ) {
return source . label || source . type || 'source' ;
} ) . filter ( Boolean ) . join ( ' | ' ) ;
}
async function submitExplorerAIMessage ( prefill ) {
var input = document . getElementById ( 'explorerAIInput' ) ;
var raw = typeof prefill === 'string' ? prefill : ( input ? input . value : '' ) ;
var question = String ( raw || '' ) . trim ( ) ;
if ( ! question || _explorerAIState . loading ) return ;
_explorerAIState . messages . push ( { role : 'user' , content : question } ) ;
if ( input ) input . value = '' ;
_explorerAIState . loading = true ;
renderExplorerAIMessages ( ) ;
try {
var payload = {
messages : _explorerAIState . messages . slice ( - 8 ) ,
pageContext : getExplorerAIPageContext ( )
} ;
var response = await postJSON ( EXPLORER _AI _API _BASE + '/chat' , payload ) ;
var reply = ( response && response . reply ) ? String ( response . reply ) : 'No reply returned.' ;
var sourceSummary = buildExplorerAISourceSummary ( response && response . context ) ;
if ( sourceSummary ) {
reply += '\n\nSources: ' + sourceSummary ;
}
if ( response && Array . isArray ( response . warnings ) && response . warnings . length ) {
reply += '\n\nWarnings: ' + response . warnings . join ( ' | ' ) ;
}
_explorerAIState . messages . push ( { role : 'assistant' , content : reply } ) ;
} catch ( error ) {
_explorerAIState . messages . push ( {
role : 'assistant' ,
content : 'Explorer AI could not complete that request.\n\n' + ( error && error . message ? error . message : 'Unknown error' ) + '\n\nIf this is production, confirm the backend has OPENAI_API_KEY and TOKEN_AGGREGATION_API_BASE configured.'
} ) ;
} finally {
_explorerAIState . loading = false ;
renderExplorerAIMessages ( ) ;
}
}
window . submitExplorerAIMessage = submitExplorerAIMessage ;
function initExplorerAIPanel ( ) {
if ( document . getElementById ( 'explorerAIPanel' ) || ! document . body ) return ;
var style = document . createElement ( 'style' ) ;
style . textContent = `
# explorerAIFab {
position : fixed ;
right : 20 px ;
bottom : 20 px ;
z - index : 20010 ;
border : 0 ;
border - radius : 999 px ;
padding : 0.9 rem 1 rem ;
background : linear - gradient ( 135 deg , # 0 f172a , # 2563 eb ) ;
color : # fff ;
box - shadow : 0 16 px 36 px rgba ( 15 , 23 , 42 , 0.28 ) ;
cursor : pointer ;
font - weight : 700 ;
letter - spacing : 0.02 em ;
}
# explorerAIPanel {
position : fixed ;
right : 20 px ;
bottom : 84 px ;
width : min ( 420 px , calc ( 100 vw - 24 px ) ) ;
height : min ( 72 vh , 680 px ) ;
display : none ;
flex - direction : column ;
z - index : 20010 ;
background : var ( -- card - bg ) ;
border : 1 px solid var ( -- border ) ;
border - radius : 22 px ;
box - shadow : 0 24 px 60 px rgba ( 15 , 23 , 42 , 0.25 ) ;
overflow : hidden ;
}
# explorerAIPanel textarea {
width : 100 % ;
min - height : 88 px ;
resize : vertical ;
border - radius : 14 px ;
border : 1 px solid var ( -- border ) ;
background : var ( -- light ) ;
color : var ( -- text ) ;
padding : 0.85 rem 0.9 rem ;
font : inherit ;
}
@ media ( max - width : 680 px ) {
# explorerAIPanel {
right : 12 px ;
left : 12 px ;
bottom : 76 px ;
width : auto ;
height : min ( 74 vh , 720 px ) ;
}
# explorerAIFab {
right : 12 px ;
bottom : 12 px ;
}
}
` ;
document . head . appendChild ( style ) ;
var button = document . createElement ( 'button' ) ;
button . id = 'explorerAIFab' ;
button . type = 'button' ;
button . setAttribute ( 'aria-expanded' , 'false' ) ;
button . setAttribute ( 'aria-controls' , 'explorerAIPanel' ) ;
button . innerHTML = '<i class="fas fa-robot" aria-hidden="true" style="margin-right:0.45rem;"></i>Explorer AI' ;
button . addEventListener ( 'click' , function ( ) { toggleExplorerAIPanel ( ) ; } ) ;
var panel = document . createElement ( 'section' ) ;
panel . id = 'explorerAIPanel' ;
panel . setAttribute ( 'aria-label' , 'Explorer AI' ) ;
panel . innerHTML = '' +
'<div style="padding:1rem 1rem 0.85rem; border-bottom:1px solid var(--border); background:linear-gradient(180deg, rgba(37,99,235,0.10), rgba(37,99,235,0));">' +
'<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:1rem;">' +
'<div>' +
'<div style="font-size:1rem; font-weight:800;">Explorer AI</div>' +
'<div id="explorerAIStatus" style="font-size:0.84rem; color:var(--text-light); margin-top:0.2rem;">Read-only assistant using indexed explorer data, route APIs, and curated docs.</div>' +
'</div>' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.6rem;" onclick="toggleExplorerAIPanel(false)">Close</button>' +
'</div>' +
'<div style="display:flex; flex-wrap:wrap; gap:0.45rem; margin-top:0.85rem;">' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.65rem;" onclick="submitExplorerAIMessage(\'Which Chain 138 routes are live right now?\')">Live routes</button>' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.65rem;" onclick="submitExplorerAIMessage(\'Why would a route show partial instead of live?\')">Route status</button>' +
'<button type="button" class="btn btn-secondary" style="padding:0.35rem 0.65rem;" onclick="submitExplorerAIMessage(\'Summarize the current page context and what I can do next.\')">Current page</button>' +
'</div>' +
'</div>' +
'<div id="explorerAIMessageList" style="flex:1; overflow:auto; padding:1rem; display:grid; gap:0.7rem; background:linear-gradient(180deg, rgba(15,23,42,0.02), rgba(15,23,42,0));"></div>' +
'<div style="padding:1rem; border-top:1px solid var(--border); display:grid; gap:0.7rem;">' +
'<div style="font-size:0.78rem; color:var(--text-light);">Public explorer and route data only. No private key handling, no transaction execution.</div>' +
'<textarea id="explorerAIInput" placeholder="Ask about a tx hash, address, bridge path, liquidity pool, or route status..."></textarea>' +
'<div style="display:flex; justify-content:space-between; align-items:center; gap:0.75rem;">' +
'<div style="font-size:0.78rem; color:var(--text-light);">Shift+Enter for a new line. Enter to send.</div>' +
'<button type="button" class="btn btn-primary" id="explorerAISendBtn">Ask Explorer AI</button>' +
'</div>' +
'</div>' ;
document . body . appendChild ( button ) ;
document . body . appendChild ( panel ) ;
var input = document . getElementById ( 'explorerAIInput' ) ;
var sendButton = document . getElementById ( 'explorerAISendBtn' ) ;
if ( sendButton ) {
sendButton . addEventListener ( 'click' , function ( ) {
submitExplorerAIMessage ( ) ;
} ) ;
}
if ( input ) {
input . addEventListener ( 'keydown' , function ( e ) {
if ( e . key === 'Enter' && ! e . shiftKey ) {
e . preventDefault ( ) ;
submitExplorerAIMessage ( ) ;
}
} ) ;
}
renderExplorerAIMessages ( ) ;
}
2026-02-16 03:09:53 -08:00
// Export functions
function exportBlockData ( blockNumber ) {
// Fetch block data and export as JSON
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/blocks/ ${ blockNumber } ` )
. then ( response => {
const block = normalizeBlock ( response ) ;
const dataStr = JSON . stringify ( block , null , 2 ) ;
const dataBlob = new Blob ( [ dataStr ] , { type : 'application/json' } ) ;
const url = URL . createObjectURL ( dataBlob ) ;
const link = document . createElement ( 'a' ) ;
link . href = url ;
link . download = ` block- ${ blockNumber } .json ` ;
link . click ( ) ;
URL . revokeObjectURL ( url ) ;
} )
. catch ( error => {
alert ( 'Failed to export block data: ' + error . message ) ;
} ) ;
}
function exportTransactionData ( txHash ) {
// Fetch transaction data and export as JSON
fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions/ ${ txHash } ` )
. then ( response => {
const tx = normalizeTransaction ( response ) ;
const dataStr = JSON . stringify ( tx , null , 2 ) ;
const dataBlob = new Blob ( [ dataStr ] , { type : 'application/json' } ) ;
const url = URL . createObjectURL ( dataBlob ) ;
const link = document . createElement ( 'a' ) ;
link . href = url ;
link . download = ` transaction- ${ txHash . substring ( 0 , 10 ) } .json ` ;
link . click ( ) ;
URL . revokeObjectURL ( url ) ;
} )
. catch ( error => {
alert ( 'Failed to export transaction data: ' + error . message ) ;
} ) ;
}
// Global error handler
window . addEventListener ( 'error' , ( event ) => {
console . error ( 'Global error:' , event . error ) ;
if ( typeof showToast === 'function' ) {
showToast ( 'An error occurred. Please refresh the page.' , 'error' ) ;
}
} ) ;
window . addEventListener ( 'unhandledrejection' , ( event ) => {
console . error ( 'Unhandled promise rejection:' , event . reason ) ;
if ( typeof showToast === 'function' ) {
showToast ( 'A network error occurred. Please try again.' , 'error' ) ;
}
} ) ;
function setLiveRegion ( text ) {
var el = document . getElementById ( 'explorerLiveRegion' ) ;
if ( el ) el . textContent = text || '' ;
}
// Toast notification function
function showToast ( message , type = 'info' , duration = 3000 ) {
if ( type === 'error' ) setLiveRegion ( message ) ;
const toast = document . createElement ( 'div' ) ;
toast . className = ` toast toast- ${ type } ` ;
toast . style . cssText = `
position : fixed ;
top : 20 px ;
right : 20 px ;
padding : 1 rem 1.5 rem ;
background : $ { type === 'error' ? '#fee2e2' : type === 'success' ? '#d1fae5' : '#dbeafe' } ;
color : $ { type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6' } ;
border - radius : 8 px ;
box - shadow : 0 4 px 12 px rgba ( 0 , 0 , 0 , 0.15 ) ;
z - index : 10000 ;
animation : slideIn 0.3 s ease - out ;
` ;
toast . textContent = message ;
document . body . appendChild ( toast ) ;
setTimeout ( ( ) => {
toast . style . animation = 'slideOut 0.3s ease-out' ;
setTimeout ( ( ) => toast . remove ( ) , 300 ) ;
} , duration ) ;
}
// Add CSS for toast animations
const style = document . createElement ( 'style' ) ;
style . textContent = `
@ keyframes slideIn {
from { transform : translateX ( 100 % ) ; opacity : 0 ; }
to { transform : translateX ( 0 ) ; opacity : 1 ; }
}
@ keyframes slideOut {
from { transform : translateX ( 0 ) ; opacity : 1 ; }
to { transform : translateX ( 100 % ) ; opacity : 0 ; }
}
` ;
document . head . appendChild ( style ) ;
2026-03-27 12:02:36 -07:00
// Search launcher, modal handlers, and mobile nav close on link click
2026-02-16 03:09:53 -08:00
document . addEventListener ( 'DOMContentLoaded' , ( ) => {
2026-03-27 13:37:53 -07:00
initExplorerAIPanel ( ) ;
2026-03-27 12:02:36 -07:00
const launchBtn = document . getElementById ( 'searchLauncherBtn' ) ;
const modal = document . getElementById ( 'smartSearchModal' ) ;
const backdrop = document . getElementById ( 'smartSearchBackdrop' ) ;
const closeBtn = document . getElementById ( 'smartSearchCloseBtn' ) ;
const input = document . getElementById ( 'smartSearchInput' ) ;
const submitBtn = document . getElementById ( 'smartSearchSubmitBtn' ) ;
if ( launchBtn ) {
launchBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ( ) ;
openSmartSearchModal ( '' ) ;
} ) ;
}
if ( closeBtn ) {
closeBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ( ) ;
closeSmartSearchModal ( ) ;
} ) ;
}
if ( backdrop ) {
backdrop . addEventListener ( 'click' , closeSmartSearchModal ) ;
}
if ( input ) {
input . addEventListener ( 'input' , function ( e ) {
updateSmartSearchPreview ( e . target . value ) ;
} ) ;
input . addEventListener ( 'keydown' , function ( e ) {
if ( e . key === 'Escape' ) {
e . preventDefault ( ) ;
closeSmartSearchModal ( ) ;
} else if ( e . key === 'Enter' ) {
2026-02-16 03:09:53 -08:00
e . preventDefault ( ) ;
handleSearch ( e . target . value ) ;
}
} ) ;
}
2026-03-27 12:02:36 -07:00
if ( submitBtn && input ) {
submitBtn . addEventListener ( 'click' , function ( e ) {
2026-02-16 03:09:53 -08:00
e . preventDefault ( ) ;
2026-03-27 12:02:36 -07:00
handleSearch ( input . value ) ;
2026-02-16 03:09:53 -08:00
} ) ;
}
2026-03-27 12:02:36 -07:00
window . addEventListener ( 'keydown' , function ( e ) {
var target = e . target ;
var tag = target && target . tagName ? target . tagName . toLowerCase ( ) : '' ;
var isEditable = ! ! ( target && ( target . isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select' ) ) ;
if ( e . key === 'Escape' && modal && modal . style . display === 'block' ) {
e . preventDefault ( ) ;
closeSmartSearchModal ( ) ;
return ;
}
if ( ( e . key === '/' || ( ( e . metaKey || e . ctrlKey ) && e . key . toLowerCase ( ) === 'k' ) ) && ! isEditable ) {
e . preventDefault ( ) ;
openSmartSearchModal ( '' ) ;
}
} ) ;
2026-02-16 03:09:53 -08:00
var navLinks = document . getElementById ( 'navLinks' ) ;
if ( navLinks ) {
navLinks . addEventListener ( 'click' , function ( e ) {
if ( e . target . closest ( 'a' ) ) closeNavMenu ( ) ;
} ) ;
initNavDropdowns ( ) ;
}
} ) ;