2026-02-16 03:09:53 -08:00
const API _BASE = '/api' ;
2026-05-22 21:55:42 -07:00
if ( typeof console !== 'undefined' && console . warn ) {
console . warn ( '[DBIS Explorer] Legacy SPA bundle loaded from /legacy/. Use / for the current Next.js explorer UI.' ) ;
}
2026-03-27 14:12:14 -07:00
const EXPLORER _API _BASE = '/explorer-api' ;
const EXPLORER _API _V1 _BASE = EXPLORER _API _BASE + '/v1' ;
2026-04-07 23:22:12 -07:00
const EXPLORER _TRACK1 _BASE = EXPLORER _API _V1 _BASE + '/track1' ;
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 ;
2026-04-07 23:22:12 -07:00
const ADDRESS _DETAIL _BLOCKSCOUT _TIMEOUT _MS = 4000 ;
2026-02-16 03:09:53 -08:00
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' ;
2026-04-07 23:22:12 -07:00
const RPC _URLS _HTTPS = [ RPC _FQDN , 'https://rpc.d-bis.org' , 'https://rpc2.d-bis.org' , 'https://rpc.defi-oracle.io' ] ;
2026-02-16 03:09:53 -08:00
const RPC _URLS = ( typeof window !== 'undefined' && window . location && window . location . protocol === 'https:' )
2026-04-07 23:22:12 -07:00
? RPC _URLS _HTTPS : [ RPC _IP ] ;
2026-02-16 03:09:53 -08:00
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 ;
}
2026-04-10 12:52:17 -07:00
const BLOCKSCOUT _API _ORIGIN = 'https://blockscout.defi-oracle.io/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)
2026-04-10 12:52:17 -07:00
const EXPLORER _HOSTS = [ 'explorer.d-bis.org' , 'blockscout.defi-oracle.io' , '192.168.11.140' ] ;
2026-02-22 15:35:45 -08:00
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-04-10 12:52:17 -07:00
const EXPLORER _ORIGINS = [ 'https://explorer.d-bis.org' , 'http://explorer.d-bis.org' , 'https://blockscout.defi-oracle.io' , 'http://blockscout.defi-oracle.io' , '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://blockscout.defi-oracle.io' ;
2026-02-16 03:09:53 -08:00
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 ( ) ;
2026-05-10 12:56:30 -07:00
if ( typeof focusTrapStart === 'function' ) focusTrapStart ( modal ) ;
2026-03-27 12:02:36 -07:00
} , 0 ) ;
}
function closeSmartSearchModal ( ) {
2026-05-10 12:56:30 -07:00
if ( typeof focusTrapEnd === 'function' ) focusTrapEnd ( ) ;
2026-03-27 12:02:36 -07:00
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-04-07 23:22:12 -07:00
var KNOWN _ADDRESS _LABELS = {
'0xcacfd227a040002e49e2e01626363071324f820a' : 'CCIP WETH9 Bridge (Chain 138)' ,
'0xe0e93247376aa097db308b92e6ba36ba015535d0' : 'CCIP WETH10 Bridge (Chain 138)' ,
'0xc9901ce2ddb6490faa183645147a87496d8b20b6' : 'CCIP WETH9 Bridge (Mainnet current)' ,
'0x04e1e22b0d41e99f4275bd40a50480219bc9a223' : 'CCIP WETH10 Bridge (Mainnet current)' ,
'0x2a0840e5117683b11682ac46f5cf5621e67269e3' : 'CCIP WETH9 Bridge (Mainnet legacy)' ,
'0x89dd12025bfcd38a168455a44b400e913ed33be2' : 'WETH9 (Arbitrum custom)' ,
'0x73376eb92c16977b126db9112936a20fa0de3442' : 'CCIP WETH10 Bridge (Arbitrum)' ,
'0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2' : 'WETH9' ,
'0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f' : 'WETH10' ,
'0x42dab7b888dd382bd5adcf9e038dbf1fd03b4817' : 'CCIP Router' ,
'0x8078a09637e47fa5ed34f626046ea2094a5cde5e' : 'CCIP Router (legacy direct)' ,
'0x105f8a15b819948a89153505762444ee9f324684' : 'CCIP Sender (legacy direct)'
} ;
const BRIDGE _TRACE _EXTRA = [
'0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d' ,
'0xF1c93F54A5C2fc0d7766Ccb0Ad8f157DFb4C99Ce' ,
'0xCd42e8eD79Dc50599535d1de48d3dAFa0BE156F8' ,
'0x7D0022B7e8360172fd9C0bB6778113b7Ea3674E7' ,
'0x88495B3dccEA93b0633390fDE71992683121Fa62' ,
'0x9Cb97adD29c52e3B81989BcA2E33D46074B530eF'
] ;
function getBridgeRouterAddressesLower ( ) {
var o = { } ;
Object . keys ( KNOWN _ADDRESS _LABELS || { } ) . forEach ( function ( k ) { o [ k . toLowerCase ( ) ] = true ; } ) ;
( BRIDGE _TRACE _EXTRA || [ ] ) . forEach ( function ( a ) { o [ String ( a ) . toLowerCase ( ) ] = true ; } ) ;
return o ;
}
function describeLogTopicsForBridge ( logs ) {
var hints = [ ] ;
var xfer = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' ;
var routers = getBridgeRouterAddressesLower ( ) ;
( logs || [ ] ) . forEach ( function ( log ) {
var topics = log . topics && Array . isArray ( log . topics ) ? log . topics : ( log . topic0 ? [ log . topic0 ] : [ ] ) ;
var t0 = String ( topics [ 0 ] || '' ) . toLowerCase ( ) ;
var addr = log . address && ( log . address . hash || log . address ) || log . address || '' ;
addr = String ( addr ) . toLowerCase ( ) ;
if ( t0 === xfer && routers [ addr ] ) {
hints . push ( 'ERC-20 Transfer from a known bridge/router contract (' + shortenHash ( addr ) + ').' ) ;
}
} ) ;
return hints ;
}
function buildBridgeTraceInnerHtml ( toAddr , logs ) {
var lines = [ ] ;
var to = ( toAddr || '' ) . toLowerCase ( ) ;
if ( to && ( KNOWN _ADDRESS _LABELS [ to ] || getBridgeRouterAddressesLower ( ) [ to ] ) ) {
lines . push ( 'The transaction targets a known bridge, router, or PMM-related contract.' ) ;
if ( KNOWN _ADDRESS _LABELS [ to ] ) lines . push ( 'Label: ' + KNOWN _ADDRESS _LABELS [ to ] ) ;
} else if ( to ) {
lines . push ( 'To address is not in the explorer allowlist; inspect logs and internal transactions for cross-chain or swap activity.' ) ;
} else {
lines . push ( 'Contract creation or empty "to"; use internal transactions and logs for interpretation.' ) ;
}
var lh = describeLogTopicsForBridge ( logs ) ;
lh . forEach ( function ( x ) { lines . push ( x ) ; } ) ;
if ( ! lines . length ) {
return '<p style="color:var(--text-light);">No bridge heuristics matched.</p>' ;
}
return '<ul style="margin:0; padding-left:1.2rem; line-height:1.5;">' + lines . map ( function ( l ) { return '<li>' + escapeHtml ( l ) + '</li>' ; } ) . join ( '' ) + '</ul>' ;
}
function buildBridgeTraceCardHtml ( toAddr , logs ) {
return '<div class="card" style="margin-top: 1rem; border-left: 3px solid var(--primary);" id="txBridgeTraceCard">' +
'<h3 style="margin-top:0;"><i class="fas fa-diagram-project"></i> Bridge / route interpretation</h3>' +
'<p style="color:var(--text-light); font-size:0.88rem;">Heuristic summary only — not proof of settlement or liquidity.</p>' +
'<div id="txBridgeTraceBody">' + buildBridgeTraceInnerHtml ( toAddr , logs ) + '</div>' +
'<p style="margin-top:0.75rem; font-size:0.85rem;"><a href="/chain138-command-center.html" target="_blank" rel="noopener noreferrer">Visual command center (new tab)</a></p>' +
'</div>' ;
}
const CHAIN _138 _PMM _INTEGRATION _ADDRESS = '0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895' ;
2026-03-27 12:02:36 -07:00
const CHAIN _138 _PRIVATE _POOL _REGISTRY = '0xb27057B27db09e8Df353AF722c299f200519882A' ;
const CHAIN _138 _CUSDT _ADDRESS = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22' ;
const CHAIN _138 _CUSDC _ADDRESS = '0xf22258f57794CC8E06237084b353Ab30fFfa640b' ;
2026-04-07 23:22:12 -07:00
const CHAIN _138 _CUSDT _V2 _ADDRESS = '0x9FBfab33882Efe0038DAa608185718b772EE5660' ;
const CHAIN _138 _CUSDC _V2 _ADDRESS = '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d' ;
2026-03-27 12:02:36 -07:00
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 '' ;
2026-04-07 23:22:12 -07:00
var len = hexToNumber ( '0x' + hex . slice ( 64 , 128 ) ) ;
2026-03-27 12:02:36 -07:00
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 ) {
2026-04-07 23:22:12 -07:00
var safeDecimals = parseInt ( decimals , 10 ) ;
if ( ! Number . isFinite ( safeDecimals ) || safeDecimals < 0 ) safeDecimals = 0 ;
if ( safeDecimals > 255 ) safeDecimals = 255 ;
return formatUnits ( value , safeDecimals , precision == null ? 3 : precision ) ;
}
function parseTokenUnitsToNumber ( value , decimals ) {
var parsed = Number ( formatTokenUnits ( value , decimals , Number ( decimals || 0 ) ) ) ;
return Number . isFinite ( parsed ) && parsed > 0 ? parsed : 0 ;
}
function isChain138UsdFallbackToken ( address , ctx ) {
var lower = String ( address || '' ) . toLowerCase ( ) ;
if ( ! lower ) return false ;
var candidates = [
( ctx && ctx . compliantUSDT ) || CHAIN _138 _CUSDT _ADDRESS ,
( ctx && ctx . compliantUSDC ) || CHAIN _138 _CUSDC _ADDRESS ,
( ctx && ctx . officialUSDT ) || '' ,
( ctx && ctx . officialUSDC ) || '' ,
CHAIN _138 _CUSDT _V2 _ADDRESS ,
CHAIN _138 _CUSDC _V2 _ADDRESS
] . map ( function ( value ) { return String ( value || '' ) . toLowerCase ( ) ; } ) . filter ( Boolean ) ;
return candidates . indexOf ( lower ) !== - 1 ;
}
function estimateChain138FallbackDepthUsd ( tokenIn , reserveIn , tokenOut , reserveOut , ctx ) {
if ( ! ( reserveIn > 0 n && reserveOut > 0 n ) ) {
return { tvlUsd : 0 , estimatedTradeCapacityUsd : 0 } ;
}
var reserveInUsd = isChain138UsdFallbackToken ( tokenIn , ctx ) ? parseTokenUnitsToNumber ( reserveIn , CHAIN _138 _POOL _BALANCE _DECIMALS ) : 0 ;
var reserveOutUsd = isChain138UsdFallbackToken ( tokenOut , ctx ) ? parseTokenUnitsToNumber ( reserveOut , CHAIN _138 _POOL _BALANCE _DECIMALS ) : 0 ;
var tvlUsd = 0 ;
if ( reserveInUsd > 0 && reserveOutUsd > 0 ) tvlUsd = reserveInUsd + reserveOutUsd ;
else if ( reserveInUsd > 0 ) tvlUsd = reserveInUsd * 2 ;
else if ( reserveOutUsd > 0 ) tvlUsd = reserveOutUsd * 2 ;
return {
tvlUsd : tvlUsd ,
estimatedTradeCapacityUsd : tvlUsd > 0 ? Math . min ( tvlUsd , tvlUsd * 0.2 ) : 0
} ;
2026-03-27 12:02:36 -07:00
}
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 ) ;
2026-04-07 23:22:12 -07:00
var depthUsd = estimateChain138FallbackDepthUsd ( query . tokenIn , reserveIn , query . tokenOut , reserveOut , ctx ) ;
2026-03-27 12:02:36 -07:00
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 : {
2026-04-07 23:22:12 -07:00
tvlUsd : depthUsd . tvlUsd ,
2026-03-27 12:02:36 -07:00
reserve0 : reserveInFormatted ,
reserve1 : reserveOutFormatted ,
2026-04-07 23:22:12 -07:00
estimatedTradeCapacityUsd : depthUsd . estimatedTradeCapacityUsd ,
2026-03-27 12:02:36 -07:00
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 : {
2026-04-07 23:22:12 -07:00
tvlUsd : depthUsd . tvlUsd ,
2026-03-27 12:02:36 -07:00
reserve0 : reserveInFormatted ,
reserve1 : reserveOutFormatted ,
2026-04-07 23:22:12 -07:00
estimatedTradeCapacityUsd : depthUsd . estimatedTradeCapacityUsd ,
2026-03-27 12:02:36 -07:00
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 ( [
2026-04-07 23:22:12 -07:00
'0xcacfd227A040002e49e2e01626363071324f820a' ,
2026-03-27 12:02:36 -07:00
'0xe0E93247376aa097dB308B92e6Ba36bA015535D0' ,
2026-04-07 23:22:12 -07:00
'0xc9901ce2Ddb6490FAA183645147a87496d8b20B6' ,
'0x04E1e22B0D41e99f4275bd40A50480219bc9A223' ,
'0x24293CA562aE1100E60a4640FF49bd656cFf93B4' ,
'0x937824f2516fa58f25aeAb92E7BFf7D74F463B4c' ,
'0xF7736443f02913e7e0773052103296CfE1637448' ,
'0x0CA60e6f8589c540200daC9D9Cb27BC2e48eE66A' ,
'0x73376eB92c16977B126dB9112936A20Fa0De3442' ,
'0x6e94e53F73893b2a6784Df663920D31043A6dE07'
2026-03-27 12:02:36 -07:00
] . 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 ; }
2026-05-10 12:56:30 -07:00
var INSTITUTION _PREFS _KEY = 'explorer_inst_prefs_v1' ;
function getInstitutionPrefs ( ) {
try {
var j = localStorage . getItem ( INSTITUTION _PREFS _KEY ) ;
var o = j ? JSON . parse ( j ) : { } ;
return {
dateFormat : o . dateFormat === 'local' ? 'local' : 'iso' ,
gasUnit : o . gasUnit === 'eth' ? 'eth' : 'gwei' ,
externalLinks : o . externalLinks === 'blockscout' ? 'blockscout' : 'spa' ,
addressRows : o . addressRows === 'compact' ? 'compact' : 'rich'
} ;
} catch ( e ) {
return { dateFormat : 'iso' , gasUnit : 'gwei' , externalLinks : 'spa' , addressRows : 'rich' } ;
}
}
function setInstitutionPrefs ( patch ) {
var cur = getInstitutionPrefs ( ) ;
Object . assign ( cur , patch || { } ) ;
try { localStorage . setItem ( INSTITUTION _PREFS _KEY , JSON . stringify ( cur ) ) ; } catch ( e ) { }
applyInstitutionPrefsToDom ( ) ;
if ( typeof syncContractVerifyBlockscoutLink === 'function' ) syncContractVerifyBlockscoutLink ( ) ;
return cur ;
}
function applyInstitutionPrefsToDom ( ) {
var p = getInstitutionPrefs ( ) ;
try {
document . body . classList . toggle ( 'explorer-compact-table-rows' , p . addressRows === 'compact' ) ;
} catch ( e ) { }
}
function formatInstitutionTimestamp ( isoOrMs ) {
if ( isoOrMs == null || isoOrMs === '' ) return '—' ;
var d = typeof isoOrMs === 'number' ? new Date ( isoOrMs ) : new Date ( isoOrMs ) ;
if ( ! Number . isFinite ( d . getTime ( ) ) ) return '—' ;
var p = getInstitutionPrefs ( ) ;
if ( p . dateFormat === 'local' ) return d . toLocaleString ( ) ;
return d . toISOString ( ) ;
}
var SAVED _VIEWS _KEY = 'explorer_saved_views_v1' ;
function getSavedViews ( ) {
try { var j = localStorage . getItem ( SAVED _VIEWS _KEY ) ; var a = j ? JSON . parse ( j ) : [ ] ; return Array . isArray ( a ) ? a : [ ] ; } catch ( e ) { return [ ] ; }
}
function saveSavedViews ( arr ) { try { localStorage . setItem ( SAVED _VIEWS _KEY , JSON . stringify ( ( arr || [ ] ) . slice ( 0 , 40 ) ) ) ; } catch ( e ) { } }
var FILTER _PRESETS _KEY = 'explorer_filter_presets_v1' ;
function getFilterPresets ( ) { try { var j = localStorage . getItem ( FILTER _PRESETS _KEY ) ; return j ? JSON . parse ( j ) : { } ; } catch ( e ) { return { } ; } }
function setFilterPresets ( m ) { try { localStorage . setItem ( FILTER _PRESETS _KEY , JSON . stringify ( m || { } ) ) ; } catch ( e ) { } }
var NOTIF _SETTINGS _KEY = 'explorer_notif_settings_v1' ;
function getNotifSettings ( ) {
try {
var j = localStorage . getItem ( NOTIF _SETTINGS _KEY ) ;
var o = j ? JSON . parse ( j ) : { } ;
return {
enabled : ! ! o . enabled ,
intervalMin : Math . max ( 5 , Math . min ( 1440 , parseInt ( o . intervalMin , 10 ) || 60 ) )
} ;
} catch ( e ) { return { enabled : false , intervalMin : 60 } ; }
}
function setNotifSettings ( patch ) {
var cur = getNotifSettings ( ) ;
Object . assign ( cur , patch || { } ) ;
try { localStorage . setItem ( NOTIF _SETTINGS _KEY , JSON . stringify ( cur ) ) ; } catch ( e ) { }
return cur ;
}
var NOTIF _SNAP _KEY = 'explorer_notif_snap_v1' ;
function getNotifSnap ( ) { try { var j = localStorage . getItem ( NOTIF _SNAP _KEY ) ; return j ? JSON . parse ( j ) : { } ; } catch ( e ) { return { } ; } }
function setNotifSnap ( m ) { try { localStorage . setItem ( NOTIF _SNAP _KEY , JSON . stringify ( m || { } ) ) ; } catch ( e ) { } }
var REPORT _SCHED _KEY = 'explorer_report_schedules_v1' ;
function getReportSchedules ( ) { try { var j = localStorage . getItem ( REPORT _SCHED _KEY ) ; var a = j ? JSON . parse ( j ) : [ ] ; return Array . isArray ( a ) ? a : [ ] ; } catch ( e ) { return [ ] ; } }
function setReportSchedules ( a ) { try { localStorage . setItem ( REPORT _SCHED _KEY , JSON . stringify ( ( a || [ ] ) . slice ( 0 , 20 ) ) ) ; } catch ( e ) { } }
function decodeExplorerJwtPayload ( token ) {
if ( ! token || typeof token !== 'string' ) return null ;
var parts = token . split ( '.' ) ;
if ( parts . length < 2 ) return null ;
try {
var b64 = parts [ 1 ] . replace ( /-/g , '+' ) . replace ( /_/g , '/' ) ;
var pad = b64 . length % 4 ? '=' . repeat ( 4 - ( b64 . length % 4 ) ) : '' ;
var json = atob ( b64 + pad ) ;
return JSON . parse ( json ) ;
} catch ( e ) { return null ; }
}
var _focusTrapState = null ;
function focusTrapStart ( rootEl ) {
focusTrapEnd ( ) ;
if ( ! rootEl ) return ;
function selectable ( el ) {
if ( ! el || el . disabled || el . getAttribute ( 'aria-hidden' ) === 'true' ) return false ;
var ti = el . getAttribute ( 'tabindex' ) ;
if ( ti === '-1' ) return false ;
return el . tabIndex >= 0 || el . matches ( 'a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])' ) ;
}
function listFocusable ( ) {
return Array . prototype . slice . call ( rootEl . querySelectorAll ( 'a[href],button,textarea,input,select,[tabindex]' ) ) . filter ( selectable ) ;
}
var onKeydown = function ( e ) {
if ( e . key !== 'Tab' ) return ;
var nodes = listFocusable ( ) ;
if ( ! nodes . length ) return ;
var first = nodes [ 0 ] ;
var last = nodes [ nodes . length - 1 ] ;
if ( e . shiftKey ) {
if ( document . activeElement === first ) { e . preventDefault ( ) ; last . focus ( ) ; }
} else {
if ( document . activeElement === last ) { e . preventDefault ( ) ; first . focus ( ) ; }
}
} ;
rootEl . addEventListener ( 'keydown' , onKeydown ) ;
_focusTrapState = { root : rootEl , onKeydown : onKeydown } ;
var n = listFocusable ( ) ;
if ( n . length ) setTimeout ( function ( ) { n [ 0 ] . focus ( ) ; } , 0 ) ;
}
function focusTrapEnd ( ) {
if ( ! _focusTrapState || ! _focusTrapState . root ) { _focusTrapState = null ; return ; }
try { _focusTrapState . root . removeEventListener ( 'keydown' , _focusTrapState . onKeydown ) ; } catch ( e ) { }
_focusTrapState = null ;
}
function updateNavAriaCurrentFromPath ( path ) {
var raw = path != null ? String ( path ) : ( window . location . pathname || '/' ) ;
var p = raw . split ( '?' ) [ 0 ] . replace ( /\/$/ , '' ) || '/' ;
var links = document . querySelectorAll ( '#navLinks a[href^="/"]' ) ;
links . forEach ( function ( a ) {
var h = ( a . getAttribute ( 'href' ) || '' ) . split ( '?' ) [ 0 ] . replace ( /\/$/ , '' ) || '/' ;
if ( h === p || ( h !== '/' && ( p === h || p . indexOf ( h + '/' ) === 0 ) ) ) {
a . setAttribute ( 'aria-current' , 'page' ) ;
} else {
a . removeAttribute ( 'aria-current' ) ;
}
} ) ;
}
2026-02-16 03:09:53 -08:00
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-05-10 12:56:30 -07:00
var detailViews = [ 'blockDetail' , 'transactionDetail' , 'addressDetail' , 'tokenDetail' , 'nftDetail' , 'watchlist' , 'searchResults' , 'tokens' , 'addresses' , 'pools' , 'routes' , 'liquidity' , 'more' , 'bridge' , 'weth' , 'system' , 'institution' , 'compare' , 'analytics' , 'operator' ] ;
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-04-07 23:22:12 -07:00
window . showSystemTopology = function ( ) { if ( _inNavHandler ) return ; _inNavHandler = true ; try { switchToView ( 'system' ) ; updateBreadcrumb ( 'system' ) ; updatePath ( '/system' ) ; if ( window . _showSystemTopology ) window . _showSystemTopology ( ) ; } 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' ;
2026-05-10 12:56:30 -07:00
const userAccountNav = document . getElementById ( 'userAccountNav' ) ;
const verifyContractsRow = document . getElementById ( 'userMenuVerifyContractsRow' ) ;
const identityLabel = document . getElementById ( 'userMenuIdentityLabel' ) ;
if ( userAccountNav ) {
userAccountNav . style . display = authToken ? '' : 'none' ;
}
if ( verifyContractsRow ) {
verifyContractsRow . style . display = authToken && hasAccess ( 4 ) ? '' : 'none' ;
}
var instRow = document . getElementById ( 'userMenuInstitutionRow' ) ;
var cmpRow = document . getElementById ( 'userMenuCompareRow' ) ;
var kbRow = document . getElementById ( 'userMenuKeyboardRow' ) ;
var diagRow = document . getElementById ( 'userMenuDiagnosticsRow' ) ;
var incRow = document . getElementById ( 'userMenuIncidentsRow' ) ;
if ( instRow ) instRow . style . display = authToken && hasAccess ( 2 ) ? '' : 'none' ;
if ( cmpRow ) cmpRow . style . display = authToken && hasAccess ( 2 ) ? '' : 'none' ;
if ( kbRow ) kbRow . style . display = authToken ? '' : 'none' ;
if ( diagRow ) diagRow . style . display = authToken && hasAccess ( 2 ) ? '' : 'none' ;
if ( incRow ) incRow . style . display = authToken && hasAccess ( 4 ) ? '' : 'none' ;
if ( identityLabel && userAddress ) {
identityLabel . textContent = 'Signed in: ' + shortenHash ( userAddress ) ;
} else if ( identityLabel ) {
identityLabel . textContent = 'Signed in' ;
}
}
window . disconnectExplorerWallet = async function disconnectExplorerWallet ( ) {
var tok = authToken ;
if ( tok ) {
try {
await fetch ( EXPLORER _API _V1 _BASE + '/auth/logout' , {
method : 'POST' ,
headers : { 'Authorization' : 'Bearer ' + tok , 'Content-Type' : 'application/json' }
} ) ;
} catch ( e ) { }
}
try {
localStorage . removeItem ( 'authToken' ) ;
localStorage . removeItem ( 'userAddress' ) ;
} catch ( e ) { }
authToken = null ;
userAddress = null ;
userTrack = 1 ;
const walletBtn = document . getElementById ( 'walletConnectBtn' ) ;
const walletStatus = document . getElementById ( 'walletStatus' ) ;
const walletAddress = document . getElementById ( 'walletAddress' ) ;
if ( walletBtn ) {
walletBtn . style . display = 'block' ;
walletBtn . disabled = false ;
}
if ( walletStatus ) walletStatus . style . display = 'none' ;
if ( walletAddress ) walletAddress . textContent = '' ;
focusTrapEnd ( ) ;
updateUIForTrack ( ) ;
loadFeatureFlags ( ) ;
if ( typeof showToast === 'function' ) {
showToast ( 'Signed out' , 'success' ) ;
}
} ;
2026-02-16 03:09:53 -08:00
2026-04-07 23:22:12 -07:00
let connectingWallet = false ;
async function readApiErrorMessage ( response , fallbackMessage ) {
try {
const errorData = await response . json ( ) ;
return ( errorData && errorData . error && errorData . error . message ) ? errorData . error . message : fallbackMessage ;
} catch ( e ) {
return fallbackMessage ;
}
}
2026-02-16 03:09:53 -08:00
// Wallet authentication
async function connectWallet ( ) {
2026-04-07 23:22:12 -07:00
if ( connectingWallet ) {
console . log ( 'connectWallet already in progress, skipping...' ) ;
return ;
}
2026-02-16 03:09:53 -08:00
if ( typeof ethers === 'undefined' ) {
alert ( 'Ethers.js not loaded. Please refresh the page.' ) ;
return ;
}
2026-04-07 23:22:12 -07:00
const walletBtn = document . getElementById ( 'walletConnectBtn' ) ;
const originalWalletBtnText = walletBtn ? walletBtn . textContent : '' ;
if ( walletBtn ) {
walletBtn . disabled = true ;
walletBtn . textContent = 'Connecting...' ;
}
connectingWallet = true ;
2026-02-16 03:09:53 -08:00
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 } )
} ) ;
2026-04-07 23:22:12 -07:00
const nonceData = await nonceResp . json ( ) . catch ( ( ) => null ) ;
if ( ! nonceResp . ok || ! nonceData || ! nonceData . nonce ) {
const nonceError =
( nonceData && nonceData . error && nonceData . error . message )
? nonceData . error . message
: 'Failed to request wallet sign-in nonce' ;
alert ( 'Wallet sign-in is temporarily unavailable: ' + nonceError ) ;
return ;
}
2026-02-16 03:09:53 -08:00
// Sign message
2026-04-30 03:06:49 -07:00
const message = ` Sign this message to authenticate with DBIS Explorer. \n \n Nonce: ${ nonceData . nonce } ` ;
2026-02-16 03:09:53 -08:00
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 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 {
2026-04-07 23:22:12 -07:00
const errorMessage = await readApiErrorMessage ( authResp , 'Unknown error' ) ;
alert ( 'Authentication failed: ' + errorMessage ) ;
2026-02-16 03:09:53 -08:00
}
} 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-04-07 23:22:12 -07:00
} finally {
connectingWallet = false ;
if ( walletBtn && walletBtn . style . display !== 'none' ) {
walletBtn . disabled = false ;
walletBtn . textContent = originalWalletBtnText || t ( 'connectWallet' ) ;
}
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-04-07 23:22:12 -07:00
function normalizeRpcAddress ( address , balanceHex , txCountHex , codeHex ) {
var normalizedAddress = safeAddress ( address ) ;
if ( ! normalizedAddress ) return null ;
var txCount = hexToNumber ( txCountHex || '0x0' ) ;
var code = String ( codeHex || '0x' ) ;
return {
address : normalizedAddress ,
hash : normalizedAddress ,
balance : hexToDecimalString ( balanceHex || '0x0' ) ,
transaction _count : txCount ,
token _count : 0 ,
is _contract : ! ! ( code && code !== '0x' && code !== '0x0' ) ,
is _verified : false ,
tx _sent : txCount ,
tx _received : 0 ,
label : getAddressLabel ( normalizedAddress ) || null ,
name : null ,
ens _domain _name : null ,
creation _tx _hash : null ,
first _seen _at : null ,
last _seen _at : null
} ;
}
2026-02-16 03:09:53 -08:00
2026-04-10 12:52:17 -07:00
function mergeAddressTabsCounters ( addressDetail , counters ) {
if ( ! addressDetail || ! counters || typeof counters !== 'object' ) return addressDetail ;
var merged = Object . assign ( { } , addressDetail ) ;
if ( counters . transactions _count != null ) {
merged . transaction _count = Number ( counters . transactions _count ) || 0 ;
}
if ( counters . token _balances _count != null ) {
merged . token _count = Number ( counters . token _balances _count ) || 0 ;
}
return merged ;
}
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 {
2026-04-07 23:22:12 -07:00
var bigintValue = BigInt ( String ( value ) ) ;
if ( bigintValue > BigInt ( Number . MAX _SAFE _INTEGER ) ) return Number . MAX _SAFE _INTEGER ;
if ( bigintValue < BigInt ( Number . MIN _SAFE _INTEGER ) ) return Number . MIN _SAFE _INTEGER ;
return Number ( bigintValue ) ;
2026-03-28 13:59:00 -07:00
} 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
} ) ;
}
2026-04-07 23:22:12 -07:00
function normalizeRpcLog ( log ) {
if ( ! log || typeof log !== 'object' ) return null ;
return {
address : log . address || null ,
block _hash : log . blockHash || null ,
block _number : log . blockNumber != null ? hexToNumber ( log . blockNumber ) : null ,
data : log . data || '0x' ,
decoded : null ,
index : log . logIndex != null ? hexToNumber ( log . logIndex ) : null ,
topics : Array . isArray ( log . topics ) ? log . topics : [ ] ,
transaction _hash : log . transactionHash || null
} ;
}
2026-03-28 13:59:00 -07:00
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 ) ,
2026-04-07 23:22:12 -07:00
rawTransaction : Object . assign ( { source : 'blockscout' } , response )
2026-03-28 13:59:00 -07:00
} ;
} catch ( error ) {
console . warn ( 'Falling back to RPC transaction detail:' , error . message || error ) ;
}
var rpcTx = await rpcCall ( 'eth_getTransactionByHash' , [ txHash ] ) ;
if ( ! rpcTx ) {
2026-04-10 12:52:17 -07:00
var latestBlock = null ;
try {
latestBlock = await rpcCall ( 'eth_blockNumber' , [ ] ) ;
} catch ( error ) {
latestBlock = null ;
}
return {
transaction : null ,
rawTransaction : {
source : 'unavailable' ,
diagnostics : {
blockscout _indexed : false ,
rpc _transaction _found : false ,
rpc _receipt _found : false ,
latest _block _number : latestBlock ? parseInt ( latestBlock , 16 ) : null
}
}
} ;
2026-03-28 13:59:00 -07:00
}
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 ) ,
2026-04-07 23:22:12 -07:00
rawTransaction : {
source : 'rpc_fallback' ,
rpc _transaction : rpcTx ,
receipt : receipt ,
block : block
}
} ;
}
async function fetchChain138AddressDetail ( address ) {
var normalizedAddress = safeAddress ( address ) ;
if ( ! normalizedAddress ) {
return { address : null , rawAddress : null , source : 'invalid' } ;
}
try {
var response = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/addresses/ ${ normalizedAddress } ` , 1 , RETRY _DELAY _MS , ADDRESS _DETAIL _BLOCKSCOUT _TIMEOUT _MS ) ;
var raw = response && ( response . data !== undefined ? response . data : response . address !== undefined ? response . address : response . items && response . items [ 0 ] !== undefined ? response . items [ 0 ] : response ) ;
var normalized = normalizeAddress ( raw ) ;
2026-04-10 12:52:17 -07:00
var needsCounters = raw && raw . hash && raw . transactions _count == null && raw . transaction _count == null && raw . tx _count == null ;
needsCounters = needsCounters || ( raw && raw . hash && raw . token _count == null ) ;
if ( normalized && normalized . hash && needsCounters ) {
try {
var counters = await fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/addresses/ ${ normalizedAddress } /tabs-counters ` , 1 , RETRY _DELAY _MS , ADDRESS _DETAIL _BLOCKSCOUT _TIMEOUT _MS ) ;
normalized = mergeAddressTabsCounters ( normalized , counters ) ;
} catch ( counterError ) {
console . warn ( 'Address counters unavailable:' , counterError . message || counterError ) ;
}
}
2026-04-07 23:22:12 -07:00
if ( normalized && normalized . hash ) {
return {
address : normalized ,
rawAddress : response ,
source : 'blockscout'
} ;
}
} catch ( error ) {
console . warn ( 'Falling back to RPC address detail:' , error . message || error ) ;
}
var results = await Promise . all ( [
rpcCall ( 'eth_getBalance' , [ normalizedAddress , 'latest' ] ) . catch ( function ( ) { return '0x0' ; } ) ,
rpcCall ( 'eth_getTransactionCount' , [ normalizedAddress , 'latest' ] ) . catch ( function ( ) { return '0x0' ; } ) ,
rpcCall ( 'eth_getCode' , [ normalizedAddress , 'latest' ] ) . catch ( function ( ) { return '0x' ; } )
] ) ;
return {
address : normalizeRpcAddress ( normalizedAddress , results [ 0 ] , results [ 1 ] , results [ 2 ] ) ,
rawAddress : {
source : 'rpc_fallback' ,
balance : results [ 0 ] ,
transaction _count : results [ 1 ] ,
code : results [ 2 ]
} ,
source : 'rpc_fallback'
2026-03-28 13:59:00 -07:00
} ;
}
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-05-22 17:58:27 -07:00
async function fetchChain138AddEthereumChainPayload ( ) {
if ( window . _chain138AddEthereumChainCache ) return window . _chain138AddEthereumChainCache ;
try {
var res = await fetch ( '/api/v1/config/metamask?chainId=138' , { cache : 'no-store' } ) ;
if ( res . ok ) {
var data = await res . json ( ) ;
if ( data && data . addEthereumChain ) {
window . _chain138AddEthereumChainCache = data . addEthereumChain ;
return window . _chain138AddEthereumChainCache ;
}
}
} catch ( e ) { }
return null ;
}
async function fetchChain138ReportTokenList ( ) {
if ( window . _chain138TokenListCache ) return window . _chain138TokenListCache ;
try {
var res = await fetch ( '/api/v1/report/token-list?chainId=138' , { cache : 'no-store' } ) ;
if ( res . ok ) {
var data = await res . json ( ) ;
window . _chain138TokenListCache = data && Array . isArray ( data . tokens ) ? data . tokens : [ ] ;
return window . _chain138TokenListCache ;
}
} catch ( e ) { }
return [ ] ;
}
async function resolveTokenLogoUri ( address ) {
if ( ! address ) return undefined ;
var tokens = await fetchChain138ReportTokenList ( ) ;
var normalized = String ( address ) . toLowerCase ( ) ;
for ( var i = 0 ; i < tokens . length ; i ++ ) {
var token = tokens [ i ] ;
if ( token && token . address && String ( token . address ) . toLowerCase ( ) === normalized ) {
return token . logoURI || undefined ;
}
}
return undefined ;
}
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-05-22 17:58:27 -07:00
var logoURI = await resolveTokenLogoUri ( address ) ;
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-05-22 17:58:27 -07:00
image : logoURI
2026-02-22 15:35:45 -08:00
}
}
} ) ;
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 {
2026-05-22 17:58:27 -07:00
var chainPayload = await fetchChain138AddEthereumChainPayload ( ) ;
2026-02-16 03:09:53 -08:00
await window . ethereum . request ( {
method : 'wallet_addEthereumChain' ,
2026-05-22 17:58:27 -07:00
params : [ chainPayload || {
2026-02-16 03:09:53 -08:00
chainId ,
2026-05-22 17:58:27 -07:00
chainName : 'DeFi Oracle Meta Mainnet' ,
2026-02-16 03:09:53 -08:00
nativeCurrency : {
2026-05-22 17:58:27 -07:00
name : 'Ether' ,
2026-02-16 03:09:53 -08:00
symbol : 'ETH' ,
decimals : 18
} ,
rpcUrls : RPC _URLS . length > 0 ? RPC _URLS : [ RPC _URL ] ,
2026-05-22 17:58:27 -07:00
blockExplorerUrls : [ 'https://explorer.d-bis.org' , 'https://blockscout.defi-oracle.io' ]
2026-02-16 03:09:53 -08:00
} ] ,
} ) ;
} 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-04-07 23:22:12 -07:00
async function runMissionControlBridgeTrace ( ) {
var input = document . getElementById ( 'missionControlBridgeTraceInput' ) ;
var result = document . getElementById ( 'missionControlBridgeTraceResult' ) ;
if ( ! input || ! result ) return ;
var tx = String ( input . value || '' ) . trim ( ) ;
if ( ! /^0x[a-fA-F0-9]{64}$/ . test ( tx ) ) {
result . innerHTML = '<div class="error">Enter a valid 32-byte transaction hash.</div>' ;
return ;
}
result . innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Resolving trace...</div>' ;
try {
var res = await fetch ( EXPLORER _API _V1 _BASE + '/mission-control/bridge/trace?tx=' + encodeURIComponent ( tx ) ) ;
var body = await res . json ( ) ;
if ( ! res . ok ) {
throw new Error ( ( body && body . error && body . error . message ) || ( 'HTTP ' + res . status ) ) ;
}
var data = body && body . data ? body . data : { } ;
var html = '' ;
html += '<div style="display:grid; gap:0.65rem;">' ;
html += '<div style="font-size:0.88rem; color:var(--text-light);">Mission-control trace resolved the tx through Blockscout and labeled any matching Chain 138 contracts from <code>smart-contracts-master.json</code>.</div>' ;
html += '<div style="display:grid; grid-template-columns:repeat(auto-fit, minmax(220px, 1fr)); gap:0.65rem;">' ;
html += '<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--light);"><div style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.3rem;">From</div><div style="font-weight:700; margin-bottom:0.2rem;">' + escapeHtml ( data . from _registry || 'Unlabeled' ) + '</div><div style="font-size:0.83rem; word-break:break-all;">' + escapeHtml ( data . from || '—' ) + '</div></div>' ;
html += '<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--light);"><div style="font-size:0.8rem; color:var(--text-light); margin-bottom:0.3rem;">To</div><div style="font-weight:700; margin-bottom:0.2rem;">' + escapeHtml ( data . to _registry || 'Unlabeled' ) + '</div><div style="font-size:0.83rem; word-break:break-all;">' + escapeHtml ( data . to || '—' ) + '</div></div>' ;
html += '</div>' ;
if ( data . blockscout _url ) {
html += '<div><a href="' + escapeAttr ( data . blockscout _url ) + '" target="_blank" rel="noopener noreferrer" class="btn btn-secondary" style="text-decoration:none;"><i class="fas fa-arrow-up-right-from-square"></i> Open transaction on explorer</a></div>' ;
}
html += '</div>' ;
result . innerHTML = html ;
} catch ( err ) {
result . innerHTML = '<div class="error">Bridge trace failed: ' + escapeHtml ( err . message || 'Unknown error' ) + '</div>' ;
}
}
window . runMissionControlBridgeTrace = runMissionControlBridgeTrace ;
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-05-10 12:56:30 -07:00
window . loadInstitutionAnalyticsCohortSnapshot = async function ( ) {
var el = document . getElementById ( 'institutionCohortSnapshot' ) ;
if ( ! el ) return ;
var out = { at : new Date ( ) . toISOString ( ) } ;
var strip = document . getElementById ( 'missionControlHealthStrip' ) ;
if ( strip ) out . missionControlStrip = strip . textContent ;
try {
var r = await fetch ( EXPLORER _TRACK1 _BASE + '/bridge/status' , { credentials : 'omit' } ) ;
if ( r . ok ) out . bridgeStatus = await r . json ( ) ;
else out . bridgeHttp = r . status ;
} catch ( e ) {
out . bridgeError = String ( e . message || e ) ;
}
el . textContent = JSON . stringify ( out , null , 2 ) ;
} ;
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>' ;
2026-05-10 12:56:30 -07:00
html += '<div style="grid-column: 1 / -1; border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);">' ;
html += '<div style="font-weight:700; margin-bottom:0.35rem;"><i class="fas fa-people-group"></i> Flow & cohort snapshot (institution)</div>' ;
html += '<p style="color:var(--text-light); line-height:1.45; font-size:0.9rem;">Cohort-style snapshot: combine live bridge status with the mission-control strip. Use for internal reporting until dedicated analytics endpoints are enabled.</p>' ;
html += '<button type="button" class="btn btn-secondary" onclick="loadInstitutionAnalyticsCohortSnapshot()">Refresh snapshot</button>' ;
html += '<pre id="institutionCohortSnapshot" style="margin-top:0.6rem; padding:0.65rem; border-radius:8px; background:var(--card-bg); border:1px solid var(--border); font-size:0.78rem; overflow:auto; max-height:220px;">Click refresh after this page loads.</pre></div>' ;
2026-03-28 15:15:23 -07:00
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;">' ;
2026-04-07 23:22:12 -07:00
html += '<div style="padding:0.75rem 1rem; border-radius:10px; border:1px solid var(--border); background:rgba(200,160,40,0.12); font-size:0.88rem; line-height:1.5;">Track 4 APIs require authenticated wallet + IP allowlisting on the Go backend. This browser UI never holds deployer keys. Do not paste secrets into Explorer AI; PMM/MCP execution is off unless the server enables it explicitly.</div>' ;
2026-03-28 15:15:23 -07:00
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>' ;
2026-04-07 23:22:12 -07:00
html += '<div style="border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Mission-control APIs</div><div style="color:var(--text-light); line-height:1.6; font-size:0.92rem;"><code>GET /explorer-api/v1/mission-control/stream</code><br><code>GET /explorer-api/v1/mission-control/bridge/trace?tx=0x...</code><br><code>GET /explorer-api/v1/mission-control/liquidity/token/{address}/pools</code></div></div>' ;
html += '<div style="border:1px solid var(--border); border-radius:14px; padding:1rem; background:var(--muted-surface);"><div style="font-weight:700; margin-bottom:0.3rem;">Track 4 script API</div><div style="color:var(--text-light); line-height:1.6; font-size:0.92rem;"><code>POST /explorer-api/v1/track4/operator/run-script</code><br>Requires authenticated wallet, IP allowlisting, and backend allowlist configuration.</div></div>' ;
2026-03-28 15:15:23 -07:00
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-04-07 23:22:12 -07:00
function renderTopologyCytoscape ( graph ) {
var c = document . getElementById ( 'systemTopologyContent' ) ;
if ( ! c ) return ;
var elements = ( graph && graph . elements ) ? graph . elements : [ ] ;
var liqNote = '' ;
var ls = graph && graph . liquiditySample ;
if ( ls && typeof ls . poolCount === 'number' ) {
liqNote = '<p style="margin-top:0.35rem; color:var(--text-light); font-size:0.82rem;">Token-aggregation sample at generation: <strong>' + ls . poolCount + '</strong> pools for sample token (set <code>TOKEN_AGGREGATION_BASE_URL</code> when running the generator).</p>' ;
}
c . innerHTML = '<div id="cyTopology" style="width:100%; height:560px; border:1px solid var(--border); border-radius:10px; background:var(--light);"></div>' +
liqNote +
'<p style="margin-top:0.5rem; color:var(--text-light); font-size:0.85rem;">Regenerate: <code>bash explorer-monorepo/scripts/generate-topology-graph.sh</code> from repo root, then deploy <code>/var/www/html/config/topology-graph.json</code>. Click a contract node to open its address page when <code>href</code> is present.</p>' ;
function runLayout ( ) {
if ( typeof cytoscape === 'undefined' ) return ;
var cyInst = cytoscape ( {
container : document . getElementById ( 'cyTopology' ) ,
elements : elements ,
style : [
{ selector : 'node' , style : { 'label' : 'data(label)' , 'font-size' : '10px' , 'text-wrap' : 'wrap' , 'text-max-width' : '120px' , 'background-color' : '#2563eb' , 'color' : '#fff' } } ,
{ selector : 'edge' , style : { 'label' : 'data(label)' , 'font-size' : '9px' , 'curve-style' : 'bezier' , 'target-arrow-shape' : 'triangle' , 'line-color' : '#94a3b8' , 'target-arrow-color' : '#94a3b8' } }
] ,
layout : { name : 'cose' , animate : true , padding : 12 }
} ) ;
cyInst . on ( 'tap' , 'node' , function ( evt ) {
var href = evt . target . data ( 'href' ) ;
if ( href ) window . open ( href , '_blank' , 'noopener' ) ;
} ) ;
}
if ( window . cytoscape ) {
runLayout ( ) ;
return ;
}
var s = document . createElement ( 'script' ) ;
s . src = 'https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js' ;
s . onload = runLayout ;
s . onerror = function ( ) { c . innerHTML = '<p class="error">Could not load graph library (CDN).</p>' ; } ;
document . head . appendChild ( s ) ;
}
function renderSystemTopologyView ( ) {
var c = document . getElementById ( 'systemTopologyContent' ) ;
if ( ! c ) return ;
c . innerHTML = '<div class="loading">Loading topology…</div>' ;
fetch ( '/config/topology-graph.json?_=' + Date . now ( ) ) . then ( function ( r ) {
if ( ! r . ok ) throw new Error ( 'HTTP ' + r . status ) ;
return r . json ( ) ;
} ) . then ( function ( g ) {
renderTopologyCytoscape ( g ) ;
} ) . catch ( function ( ) {
c . innerHTML = '<p class="error">Missing <code>/config/topology-graph.json</code>. Run <code>explorer-monorepo/scripts/generate-topology-graph.sh</code> and deploy to the explorer host.</p>' ;
} ) ;
}
window . _showSystemTopology = renderSystemTopologyView ;
function ensureMissionControlHealthStrip ( ) {
var main = document . getElementById ( 'mainContent' ) ;
if ( ! main || document . getElementById ( 'missionControlHealthStrip' ) ) return ;
var el = document . createElement ( 'div' ) ;
el . id = 'missionControlHealthStrip' ;
el . setAttribute ( 'role' , 'status' ) ;
el . style . cssText = 'margin: 0 0 0.75rem 0; padding: 0.45rem 0.75rem; border-radius: 8px; font-size: 0.82rem; color: var(--text); border: 1px solid var(--border);' ;
el . textContent = 'Mission control: loading RPC health…' ;
main . insertBefore ( el , main . firstChild ) ;
}
function missionControlRelativeAge ( isoString ) {
if ( ! isoString ) return '' ;
var ts = Date . parse ( isoString ) ;
if ( ! Number . isFinite ( ts ) ) return '' ;
var diffSec = Math . max ( 0 , Math . round ( ( Date . now ( ) - ts ) / 1000 ) ) ;
if ( diffSec < 60 ) return diffSec + 's ago' ;
var diffMin = Math . round ( diffSec / 60 ) ;
if ( diffMin < 60 ) return diffMin + 'm ago' ;
var diffHr = Math . round ( diffMin / 60 ) ;
return diffHr + 'h ago' ;
}
function summarizeMissionControlRelay ( relayData ) {
if ( ! relayData || typeof relayData !== 'object' ) return null ;
if ( relayData . url _probe && typeof relayData . url _probe === 'object' && relayData . url _probe . ok === false ) {
return { text : 'CCIP relay: down' , degraded : true , paused : false } ;
}
if ( relayData . file _snapshot _error ) {
return { text : 'CCIP relay snapshot error' , degraded : true , paused : false } ;
}
var snapshot = null ;
if ( relayData . url _probe && relayData . url _probe . body && typeof relayData . url _probe . body === 'object' ) {
snapshot = relayData . url _probe . body ;
} else if ( relayData . file _snapshot && typeof relayData . file _snapshot === 'object' ) {
snapshot = relayData . file _snapshot ;
}
if ( ! snapshot ) {
return { text : 'CCIP relay: probe configured' , degraded : false , paused : false } ;
}
var status = String ( snapshot . status || 'unknown' ) . toLowerCase ( ) ;
var dest = snapshot . destination && ( snapshot . destination . chain _name || snapshot . destination . chainName ) ;
var queueSize = snapshot . queue && snapshot . queue . size != null ? snapshot . queue . size : null ;
var pollAge = missionControlRelativeAge ( snapshot . last _source _poll && snapshot . last _source _poll . at ) ;
var text = 'CCIP relay: ' + status ;
if ( dest ) text += ' -> ' + dest ;
if ( queueSize != null ) text += ' · q ' + queueSize ;
if ( pollAge ) text += ' · ' + pollAge + ' poll' ;
return {
text : text ,
degraded : [ 'degraded' , 'stale' , 'stopped' , 'down' ] . indexOf ( status ) >= 0 ,
paused : status === 'paused'
} ;
}
function applyMissionControlHealthData ( d ) {
var el = document . getElementById ( 'missionControlHealthStrip' ) ;
if ( ! el || ! d ) return ;
var probes = d . rpc _probe || [ ] ;
var parts = [ ] ;
var degraded = ( d . status || '' ) === 'degraded' ;
var paused = false ;
probes . forEach ( function ( p ) {
var st = p . ok ? 'ok' : 'down' ;
var lat = p . latencyMs != null ? p . latencyMs + 'ms' : '—' ;
var head = p . headAgeSeconds != null ? Math . round ( p . headAgeSeconds ) + 's head' : '' ;
parts . push ( p . name + ': ' + st + ' · ' + lat + ( head ? ' · ' + head : '' ) ) ;
} ) ;
if ( d . operator _verify && typeof d . operator _verify === 'object' && ! d . operator _verify . error ) {
parts . push ( 'operator verify JSON' ) ;
}
var relaySummary = summarizeMissionControlRelay ( d . ccip _relay ) ;
if ( relaySummary ) {
parts . push ( relaySummary . text ) ;
degraded = degraded || relaySummary . degraded ;
paused = paused || relaySummary . paused ;
}
el . textContent = 'Mission control · ' + ( parts . join ( ' | ' ) || 'no RPC probes' ) ;
el . style . background = degraded
? 'rgba(200,100,80,0.18)'
: ( paused ? 'rgba(180,120,0,0.15)' : 'rgba(60,140,200,0.12)' ) ;
}
var _missionControlEventSource = null ;
function startMissionControlEventSource ( ) {
if ( typeof EventSource === 'undefined' ) return ;
if ( _missionControlEventSource ) return ;
try {
var url = EXPLORER _API _V1 _BASE + '/mission-control/stream' ;
var es = new EventSource ( url ) ;
es . addEventListener ( 'mission-control' , function ( ev ) {
try {
var o = JSON . parse ( ev . data ) ;
applyMissionControlHealthData ( o && o . data ? o . data : { } ) ;
} catch ( err ) { }
} ) ;
es . onerror = function ( ) {
es . close ( ) ;
_missionControlEventSource = null ;
} ;
_missionControlEventSource = es ;
} catch ( e ) { }
}
async function refreshMissionControlHealthStrip ( ) {
var el = document . getElementById ( 'missionControlHealthStrip' ) ;
if ( ! el ) return ;
try {
var r = await fetch ( EXPLORER _TRACK1 _BASE + '/bridge/status' , { credentials : 'omit' } ) ;
if ( ! r . ok ) {
el . textContent = 'Mission control: operator API unreachable (HTTP ' + r . status + ').' ;
el . style . background = 'rgba(180,120,0,0.15)' ;
return ;
}
var j = await r . json ( ) ;
applyMissionControlHealthData ( j && j . data ? j . data : { } ) ;
} catch ( e ) {
el . textContent = 'Mission control: could not load /explorer-api/v1/track1/bridge/status.' ;
el . style . background = 'rgba(120,120,120,0.15)' ;
}
}
2026-02-16 03:09:53 -08:00
function showView ( viewName ) {
currentView = viewName ;
2026-05-10 12:56:30 -07:00
var detailViews = [ 'blockDetail' , 'transactionDetail' , 'addressDetail' , 'tokenDetail' , 'nftDetail' , 'watchlist' , 'searchResults' , 'tokens' , 'addresses' , 'pools' , 'routes' , 'liquidity' , 'more' , 'analytics' , 'operator' , 'system' , 'bridge' , 'weth' , 'institution' , 'compare' ] ;
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 ) ;
}
2026-05-10 12:56:30 -07:00
if ( typeof updateNavAriaCurrentFromPath === 'function' ) {
updateNavAriaCurrentFromPath ( path ) ;
}
2026-02-16 03:09:53 -08:00
}
window . updatePath = updatePath ;
2026-05-10 12:56:30 -07:00
window . refreshWalletJwt = async function refreshWalletJwt ( ) {
if ( ! authToken ) return ;
try {
var r = await fetch ( EXPLORER _API _V1 _BASE + '/auth/refresh' , {
method : 'POST' ,
headers : { 'Authorization' : 'Bearer ' + authToken , 'Content-Type' : 'application/json' }
} ) ;
var data = await r . json ( ) . catch ( function ( ) { return null ; } ) ;
if ( ! r . ok || ! data || ! data . token ) {
if ( typeof showToast === 'function' ) showToast ( 'Session refresh failed (HTTP ' + r . status + ').' , 'error' ) ;
return ;
}
authToken = data . token ;
if ( data . track != null ) userTrack = parseInt ( data . track , 10 ) || userTrack ;
try { localStorage . setItem ( 'authToken' , authToken ) ; } catch ( e ) { }
updateUIForTrack ( ) ;
await loadFeatureFlags ( ) ;
if ( typeof showToast === 'function' ) showToast ( 'Session refreshed' , 'success' ) ;
if ( typeof renderInstitutionView === 'function' && currentView === 'institution' ) renderInstitutionView ( ) ;
} catch ( e ) {
if ( typeof showToast === 'function' ) showToast ( 'Session refresh failed.' , 'error' ) ;
}
} ;
window . showInstitutionConsole = function showInstitutionConsole ( ) {
if ( ! authToken || ! hasAccess ( 2 ) ) {
if ( typeof showToast === 'function' ) showToast ( 'Institution console requires track 2+ after wallet sign-in.' , 'warning' ) ;
return ;
}
switchToView ( 'institution' ) ;
updatePath ( '/institution' ) ;
renderInstitutionView ( ) ;
} ;
window . showCompareAddresses = function showCompareAddresses ( a , b ) {
if ( ! authToken || ! hasAccess ( 2 ) ) {
if ( typeof showToast === 'function' ) showToast ( 'Compare addresses requires track 2+.' , 'warning' ) ;
return ;
}
var aa = a ? safeAddress ( String ( a ) ) : null ;
var bb = b ? safeAddress ( String ( b ) ) : null ;
switchToView ( 'compare' ) ;
if ( aa && bb ) {
updatePath ( '/compare/' + encodeURIComponent ( aa ) + '/' + encodeURIComponent ( bb ) ) ;
} else {
updatePath ( '/compare' ) ;
}
renderCompareView ( aa || '' , bb || '' ) ;
} ;
window . openInstitutionSavedViewById = function ( id ) {
var views = getSavedViews ( ) ;
for ( var i = 0 ; i < views . length ; i ++ ) {
if ( views [ i ] . id === id ) {
if ( typeof history !== 'undefined' && history . pushState ) {
history . pushState ( null , '' , views [ i ] . path ) ;
}
applyHashRoute ( ) ;
return ;
}
}
} ;
window . removeInstitutionSavedViewById = function ( id ) {
saveSavedViews ( getSavedViews ( ) . filter ( function ( v ) { return v . id !== id ; } ) ) ;
if ( typeof renderInstitutionView === 'function' ) renderInstitutionView ( ) ;
} ;
window . saveCurrentPageAsInstitutionView = function ( ) {
var name = window . prompt ( 'Name for this saved view (e.g. Bridge dashboard)' , 'Saved view' ) ;
if ( name == null ) return ;
name = String ( name ) . trim ( ) || 'Saved view' ;
var path = window . location . pathname || '/' ;
var views = getSavedViews ( ) ;
views . unshift ( { id : 'sv_' + Date . now ( ) , name : name , path : path } ) ;
saveSavedViews ( views ) ;
if ( typeof showToast === 'function' ) showToast ( 'Saved view stored in this browser.' , 'success' ) ;
renderInstitutionView ( ) ;
} ;
window . promptSaveFilterPresetInstitution = function ( key ) {
var name = window . prompt ( 'Preset name for ' + key , 'My filter' ) ;
if ( name == null ) return ;
name = String ( name ) . trim ( ) || 'Preset' ;
var val = getExplorerPageFilter ( key ) ;
var all = getFilterPresets ( ) ;
if ( ! all [ key ] ) all [ key ] = [ ] ;
all [ key ] . push ( { id : 'fp_' + Date . now ( ) , name : name , value : val } ) ;
setFilterPresets ( all ) ;
if ( typeof showToast === 'function' ) showToast ( 'Filter preset saved locally.' , 'success' ) ;
renderInstitutionView ( ) ;
} ;
window . applyFilterPresetInstitution = function ( key , value ) {
setExplorerPageFilter ( key , value || '' ) ;
if ( key === 'blocksList' && typeof loadAllBlocks === 'function' ) loadAllBlocks ( blocksListPage ) ;
if ( key === 'transactionsList' && typeof loadAllTransactions === 'function' ) loadAllTransactions ( transactionsListPage ) ;
if ( typeof showToast === 'function' ) showToast ( 'Filter applied. Open Blocks or Transactions to see results.' , 'info' ) ;
} ;
window . applyFilterPresetByIdInstitution = function ( key , pid ) {
var arr = ( getFilterPresets ( ) [ key ] || [ ] ) . filter ( function ( p ) { return p . id === pid ; } ) ;
if ( ! arr . length ) return ;
applyFilterPresetInstitution ( key , arr [ 0 ] . value ) ;
} ;
window . removeFilterPresetInstitution = function ( key , pid ) {
var all = getFilterPresets ( ) ;
if ( ! all [ key ] ) return ;
all [ key ] = all [ key ] . filter ( function ( p ) { return p . id !== pid ; } ) ;
setFilterPresets ( all ) ;
renderInstitutionView ( ) ;
} ;
window . saveInstitutionNotifSettingsFromForm = function ( ) {
var en = document . getElementById ( 'instNotifEnabled' ) ;
var iv = document . getElementById ( 'instNotifInterval' ) ;
setNotifSettings ( {
enabled : ! ! ( en && en . checked ) ,
intervalMin : iv ? parseInt ( iv . value , 10 ) || 60 : 60
} ) ;
if ( typeof showToast === 'function' ) showToast ( 'Notification settings saved.' , 'success' ) ;
renderInstitutionView ( ) ;
if ( typeof restartWatchlistInstitutionNotifications === 'function' ) restartWatchlistInstitutionNotifications ( ) ;
} ;
window . saveInstitutionReportSchedule = function ( ) {
if ( ! hasAccess ( 3 ) ) return ;
var name = window . prompt ( 'Report schedule name' , 'Weekly digest' ) ;
if ( name == null ) return ;
var freq = window . prompt ( 'Frequency: manual, daily, or weekly' , 'weekly' ) ;
if ( freq == null ) return ;
freq = String ( freq ) . toLowerCase ( ) ;
if ( freq !== 'daily' && freq !== 'weekly' && freq !== 'manual' ) freq = 'manual' ;
var list = getReportSchedules ( ) ;
list . push ( { id : 'rs_' + Date . now ( ) , name : String ( name ) . trim ( ) || 'Report' , frequency : freq , createdAt : new Date ( ) . toISOString ( ) } ) ;
setReportSchedules ( list ) ;
renderInstitutionView ( ) ;
} ;
window . removeInstitutionReportSchedule = function ( rid ) {
setReportSchedules ( getReportSchedules ( ) . filter ( function ( r ) { return r . id !== rid ; } ) ) ;
renderInstitutionView ( ) ;
} ;
function renderInstitutionView ( ) {
var c = document . getElementById ( 'institutionContent' ) ;
if ( ! c ) return ;
if ( ! authToken || ! hasAccess ( 2 ) ) {
c . innerHTML = '<p class="error">Institution console requires track 2+ wallet authentication.</p>' ;
return ;
}
var prefs = getInstitutionPrefs ( ) ;
var jwt = decodeExplorerJwtPayload ( authToken ) ;
var expText = '—' ;
if ( jwt && jwt . exp ) {
expText = formatInstitutionTimestamp ( jwt . exp * 1000 ) ;
if ( jwt . jti ) expText += ' · jti: ' + escapeHtml ( String ( jwt . jti ) . slice ( 0 , 14 ) ) + '…' ;
}
var views = getSavedViews ( ) ;
var viewRows = views . length
? views . map ( function ( v ) {
return '<tr><td>' + escapeHtml ( v . name ) + '</td><td><code style="font-size:0.82rem;">' + escapeHtml ( v . path ) + '</code></td><td><button type="button" class="btn btn-secondary" onclick="openInstitutionSavedViewById(\'' + escapeAttr ( v . id ) + '\')">Open</button> <button type="button" class="btn btn-secondary" onclick="removeInstitutionSavedViewById(\'' + escapeAttr ( v . id ) + '\')">Remove</button></td></tr>' ;
} ) . join ( '' )
: '<tr><td colspan="3" style="color:var(--text-light);">No saved views. Browse the explorer, then click “Save current page”.</td></tr>' ;
var fp = getFilterPresets ( ) ;
function fpRows ( key ) {
var arr = fp [ key ] || [ ] ;
if ( ! arr . length ) return '<tr><td colspan="3" style="color:var(--text-light);">None</td></tr>' ;
return arr . map ( function ( p ) {
return '<tr><td>' + escapeHtml ( p . name ) + '</td><td><code>' + escapeHtml ( p . value || '(empty)' ) + '</code></td><td><button type="button" class="btn btn-secondary" onclick="applyFilterPresetByIdInstitution(\'' + key + '\', \'' + escapeAttr ( p . id ) + '\')">Apply</button> <button type="button" class="btn btn-secondary" onclick="removeFilterPresetInstitution(\'' + key + '\', \'' + escapeAttr ( p . id ) + '\')">Remove</button></td></tr>' ;
} ) . join ( '' ) ;
}
var ns = getNotifSettings ( ) ;
var rs = getReportSchedules ( ) ;
var rsRows = ! hasAccess ( 3 )
? '<tr><td colspan="3" style="color:var(--text-light);">Track 3+ unlocks scheduled report metadata.</td></tr>'
: ( rs . length ? rs . map ( function ( r ) {
return '<tr><td>' + escapeHtml ( r . name ) + '</td><td>' + escapeHtml ( r . frequency ) + '</td><td><button type="button" class="btn btn-secondary" onclick="removeInstitutionReportSchedule(\'' + escapeAttr ( r . id ) + '\')">Remove</button></td></tr>' ;
} ) . join ( '' ) : '<tr><td colspan="3" style="color:var(--text-light);">No schedules. Add a label for compliance tracking; snapshot download is manual from this browser.</td></tr>' ) ;
var apiBlock = ! hasAccess ( 3 )
? '<p style="color:var(--text-light);">Track 3+ users: RPC API keys and usage audit live under the access console (email session), not the wallet JWT. See <a href="https://gitea.d-bis.org/d-bis/explorer-monorepo/src/branch/main/docs/EXPLORER_API_ACCESS.md" target="_blank" rel="noopener noreferrer">EXPLORER_API_ACCESS.md</a>.</p>'
: '<p>RPC products and keys: <code>GET ' + escapeHtml ( EXPLORER _API _V1 _BASE ) + '/access/products</code> (public) and <code>GET …/access/api-keys</code> with an <strong>email</strong> access token from <code>POST …/auth/register</code> / <code>login</code>. Wallet JWTs gate explorer tracks, not the RPC key store.</p>' ;
c . innerHTML = '' +
'<div style="display:grid; gap:1.25rem;">' +
'<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-shield-halved"></i> Session & security</h3></div><div style="padding:0 1rem 1rem; line-height:1.55;">' +
'<p><strong>Wallet</strong>: ' + escapeHtml ( userAddress || '' ) + ' · <strong>Track</strong>: ' + escapeHtml ( String ( userTrack ) ) + '</p>' +
'<p><strong>JWT expiry (decoded)</strong>: ' + expText + '</p>' +
'<p style="color:var(--text-light); font-size:0.88rem;">Only the <strong>current browser session</strong> is shown. Revoking this token uses Sign out (server <code>POST /auth/logout</code>). Organization-wide “sign out everywhere” needs operator revocation of all JTIs for the address (not exposed here).</p>' +
'<button type="button" class="btn btn-primary" onclick="refreshWalletJwt()"><i class="fas fa-rotate"></i> Refresh JWT</button></div></div>' +
'<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-sliders"></i> Preferences (this browser)</h3></div><div style="padding:0 1rem 1rem;">' +
'<div style="display:grid; gap:0.65rem; max-width:32rem;">' +
'<label>Timestamps <select id="instPrefDate" class="btn btn-secondary" style="width:100%;">' +
'<option value="iso"' + ( prefs . dateFormat === 'iso' ? ' selected' : '' ) + '>ISO UTC</option>' +
'<option value="local"' + ( prefs . dateFormat === 'local' ? ' selected' : '' ) + '>Locale</option></select></label>' +
'<label>Gas display <select id="instPrefGas" class="btn btn-secondary" style="width:100%;">' +
'<option value="gwei"' + ( prefs . gasUnit === 'gwei' ? ' selected' : '' ) + '>Gwei-first labels</option>' +
'<option value="eth"' + ( prefs . gasUnit === 'eth' ? ' selected' : '' ) + '>ETH-first labels</option></select></label>' +
'<label>External contract links <select id="instPrefExt" class="btn btn-secondary" style="width:100%;">' +
'<option value="spa"' + ( prefs . externalLinks === 'spa' ? ' selected' : '' ) + '>Stay in explorer SPA</option>' +
'<option value="blockscout"' + ( prefs . externalLinks === 'blockscout' ? ' selected' : '' ) + '>Prefer Blockscout origin</option></select></label>' +
'<label>Address tables <select id="instPrefRows" class="btn btn-secondary" style="width:100%;">' +
'<option value="rich"' + ( prefs . addressRows === 'rich' ? ' selected' : '' ) + '>Rich</option>' +
'<option value="compact"' + ( prefs . addressRows === 'compact' ? ' selected' : '' ) + '>Compact</option></select></label>' +
'<button type="button" class="btn btn-primary" onclick="setInstitutionPrefs({ dateFormat: document.getElementById(\'instPrefDate\').value, gasUnit: document.getElementById(\'instPrefGas\').value, externalLinks: document.getElementById(\'instPrefExt\').value, addressRows: document.getElementById(\'instPrefRows\').value }); showToast(\'Preferences saved\', \'success\');">Save preferences</button>' +
'</div></div></div>' +
'<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-bookmark"></i> Saved views</h3></div><div style="padding:0 1rem 1rem;">' +
'<button type="button" class="btn btn-primary" onclick="saveCurrentPageAsInstitutionView()"><i class="fas fa-floppy-disk"></i> Save current page</button>' +
'<table class="table" style="margin-top:0.75rem;"><thead><tr><th>Name</th><th>Path</th><th></th></tr></thead><tbody>' + viewRows + '</tbody></table></div></div>' +
'<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-filter"></i> Filter presets (blocks / transactions lists)</h3></div><div style="padding:0 1rem 1rem;">' +
'<p style="color:var(--text-light); font-size:0.88rem;">Apply filters on Blocks or Transactions pages first, then capture a preset here.</p>' +
'<button type="button" class="btn btn-secondary" onclick="promptSaveFilterPresetInstitution(\'blocksList\')">Save blocks filter</button> ' +
'<button type="button" class="btn btn-secondary" onclick="promptSaveFilterPresetInstitution(\'transactionsList\')">Save transactions filter</button>' +
'<h4 style="margin:1rem 0 0.35rem;">Blocks</h4><table class="table"><thead><tr><th>Name</th><th>Value</th><th></th></tr></thead><tbody>' + fpRows ( 'blocksList' ) + '</tbody></table>' +
'<h4 style="margin:1rem 0 0.35rem;">Transactions</h4><table class="table"><thead><tr><th>Name</th><th>Value</th><th></th></tr></thead><tbody>' + fpRows ( 'transactionsList' ) + '</tbody></table></div></div>' +
'<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-bell"></i> Watchlist notifications</h3></div><div style="padding:0 1rem 1rem;">' +
'<p style="color:var(--text-light); font-size:0.88rem;">While this tab is open, the explorer compares cached transaction counts for watchlist addresses on your chosen interval.</p>' +
'<label style="display:flex; gap:0.5rem; align-items:center;"><input type="checkbox" id="instNotifEnabled"' + ( ns . enabled ? ' checked' : '' ) + '> Enable</label>' +
'<label>Interval (minutes) <input type="number" id="instNotifInterval" min="5" max="1440" value="' + escapeAttr ( String ( ns . intervalMin ) ) + '" style="width:6rem; padding:0.35rem; border-radius:8px; border:1px solid var(--border);"></label> ' +
'<button type="button" class="btn btn-primary" onclick="saveInstitutionNotifSettingsFromForm()">Save</button></div></div>' +
( hasAccess ( 3 ) ? '<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-file-export"></i> Reports & snapshots</h3></div><div style="padding:0 1rem 1rem;">' +
'<p style="color:var(--text-light); font-size:0.88rem;">Download a JSON bundle for audit / ticketing. Email delivery is not wired in-browser; export and attach to your SOC workflow.</p>' +
'<button type="button" class="btn btn-primary" onclick="downloadInstitutionReportSnapshot()"><i class="fas fa-download"></i> Download snapshot JSON</button> ' +
'<button type="button" class="btn btn-secondary" onclick="saveInstitutionReportSchedule()">Add schedule label</button>' +
'<table class="table" style="margin-top:0.75rem;"><thead><tr><th>Name</th><th>Frequency</th><th></th></tr></thead><tbody>' + rsRows + '</tbody></table></div></div>' : '' ) +
'<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-key"></i> API & RPC access</h3></div><div style="padding:0 1rem 1rem;">' + apiBlock + '</div></div>' +
'<div class="card" style="margin:0;"><div class="card-header"><h3 class="card-title"><i class="fas fa-columns"></i> Address comparison</h3></div><div style="padding:0 1rem 1rem;">' +
'<button type="button" class="btn btn-primary" onclick="showCompareAddresses()">Open compare tool</button></div></div>' +
'</div>' ;
}
function renderCompareView ( a , b ) {
var c = document . getElementById ( 'compareContent' ) ;
if ( ! c ) return ;
if ( ! authToken || ! hasAccess ( 2 ) ) {
c . innerHTML = '<p class="error">Track 2+ required.</p>' ;
return ;
}
var aa = a ? safeAddress ( String ( a ) ) : null ;
var bb = b ? safeAddress ( String ( b ) ) : null ;
var form = '<div style="display:flex; flex-wrap:wrap; gap:0.75rem; align-items:flex-end; margin-bottom:1rem;">' +
'<div><label for="cmpA">Address A</label><br><input id="cmpA" value="' + escapeAttr ( aa || '' ) + '" placeholder="0x…" style="min-width:16rem; padding:0.45rem; border-radius:8px; border:1px solid var(--border);"></div>' +
'<div><label for="cmpB">Address B</label><br><input id="cmpB" value="' + escapeAttr ( bb || '' ) + '" placeholder="0x…" style="min-width:16rem; padding:0.45rem; border-radius:8px; border:1px solid var(--border);"></div>' +
'<button type="button" class="btn btn-primary" onclick="var x=document.getElementById(\'cmpA\').value,y=document.getElementById(\'cmpB\').value; showCompareAddresses(x,y);">Compare</button></div>' ;
if ( ! aa || ! bb ) {
c . innerHTML = form + '<p style="color:var(--text-light);">Enter two contract or EOA addresses to compare balances and activity counters side by side.</p>' ;
return ;
}
c . innerHTML = form + '<div class="loading"><i class="fas fa-spinner"></i> Loading both addresses…</div>' ;
Promise . all ( [ fetchChain138AddressDetail ( aa ) , fetchChain138AddressDetail ( bb ) ] ) . then ( function ( results ) {
var ra = results [ 0 ] && results [ 0 ] . address ;
var rb = results [ 1 ] && results [ 1 ] . address ;
var rowBal = '<tr><th>Balance (ETH)</th><td>' + ( ra ? escapeHtml ( formatEther ( ra . balance || '0' ) ) : '—' ) + '</td><td>' + ( rb ? escapeHtml ( formatEther ( rb . balance || '0' ) ) : '—' ) + '</td></tr>' ;
var rowTx = '<tr><th>Tx count</th><td>' + ( ra ? escapeHtml ( String ( ra . transaction _count != null ? ra . transaction _count : 0 ) ) : '—' ) + '</td><td>' + ( rb ? escapeHtml ( String ( rb . transaction _count != null ? rb . transaction _count : 0 ) ) : '—' ) + '</td></tr>' ;
var rowTy = '<tr><th>Type</th><td>' + ( ra ? ( ra . is _contract ? 'Contract' : 'EOA' ) : '—' ) + '</td><td>' + ( rb ? ( rb . is _contract ? 'Contract' : 'EOA' ) : '—' ) + '</td></tr>' ;
var rowTk = '<tr><th>Tokens</th><td>' + ( ra ? escapeHtml ( String ( ra . token _count != null ? ra . token _count : 0 ) ) : '—' ) + '</td><td>' + ( rb ? escapeHtml ( String ( rb . token _count != null ? rb . token _count : 0 ) ) : '—' ) + '</td></tr>' ;
c . innerHTML = form + '<table class="table"><thead><tr><th>Metric</th><th>A</th><th>B</th></tr></thead><tbody>' + rowBal + rowTx + rowTy + rowTk + '</tbody></table>' ;
} ) . catch ( function ( err ) {
c . innerHTML = form + '<p class="error">' + escapeHtml ( err . message || String ( err ) ) + '</p>' ;
} ) ;
}
window . closeKeyboardShortcutsModal = function ( ) {
focusTrapEnd ( ) ;
var m = document . getElementById ( 'keyboardShortcutsModal' ) ;
if ( m ) {
m . style . display = 'none' ;
m . setAttribute ( 'aria-hidden' , 'true' ) ;
}
document . body . style . overflow = '' ;
} ;
window . showKeyboardShortcutsModal = function ( ) {
var m = document . getElementById ( 'keyboardShortcutsModal' ) ;
if ( ! m ) return ;
if ( typeof closeAllNavDropdowns === 'function' ) closeAllNavDropdowns ( ) ;
m . style . display = 'block' ;
m . setAttribute ( 'aria-hidden' , 'false' ) ;
document . body . style . overflow = 'hidden' ;
focusTrapStart ( m ) ;
} ;
window . closeIncidentLinksModal = function ( ) {
focusTrapEnd ( ) ;
var m = document . getElementById ( 'incidentLinksModal' ) ;
if ( m ) {
m . style . display = 'none' ;
m . setAttribute ( 'aria-hidden' , 'true' ) ;
}
document . body . style . overflow = '' ;
} ;
window . showIncidentLinksModal = function ( ) {
if ( ! authToken || ! hasAccess ( 4 ) ) {
if ( typeof showToast === 'function' ) showToast ( 'Incident hub is limited to operator-track wallets.' , 'warning' ) ;
return ;
}
var m = document . getElementById ( 'incidentLinksModal' ) ;
if ( ! m ) return ;
if ( typeof closeAllNavDropdowns === 'function' ) closeAllNavDropdowns ( ) ;
m . style . display = 'block' ;
m . setAttribute ( 'aria-hidden' , 'false' ) ;
document . body . style . overflow = 'hidden' ;
focusTrapStart ( m ) ;
} ;
window . copyExplorerDiagnosticsBundle = async function ( ) {
if ( ! authToken ) {
if ( typeof showToast === 'function' ) showToast ( 'Sign in to copy diagnostics.' , 'warning' ) ;
return ;
}
var pl = decodeExplorerJwtPayload ( authToken ) ;
var bundle = {
exportedAt : new Date ( ) . toISOString ( ) ,
pathname : window . location . pathname ,
userTrack : userTrack ,
wallet : userAddress ,
jwtExp : pl && pl . exp ,
jwtJtiPrefix : pl && pl . jti ? String ( pl . jti ) . slice ( 0 , 12 ) : null ,
chainId : typeof CHAIN _ID !== 'undefined' ? CHAIN _ID : null ,
userAgent : navigator . userAgent ,
explorerApi : EXPLORER _API _V1 _BASE
} ;
var strip = document . getElementById ( 'missionControlHealthStrip' ) ;
if ( strip ) bundle . missionControlStrip = strip . textContent ;
var txt = JSON . stringify ( bundle , null , 2 ) ;
try {
await navigator . clipboard . writeText ( txt ) ;
if ( typeof showToast === 'function' ) showToast ( 'Diagnostics bundle copied' , 'success' ) ;
} catch ( e ) {
window . prompt ( 'Copy diagnostics:' , txt ) ;
}
} ;
window . downloadInstitutionReportSnapshot = async function ( ) {
if ( ! hasAccess ( 3 ) ) {
if ( typeof showToast === 'function' ) showToast ( 'Track 3+ required for report snapshot.' , 'warning' ) ;
return ;
}
var snap = {
generatedAt : new Date ( ) . toISOString ( ) ,
userTrack : userTrack ,
pathname : window . location . pathname ,
schedules : getReportSchedules ( )
} ;
var strip = document . getElementById ( 'missionControlHealthStrip' ) ;
if ( strip ) snap . missionControlStrip = strip . textContent ;
var g = document . getElementById ( 'gasCurrentValue' ) ;
if ( g ) snap . gasHomeLabel = g . textContent ;
try {
var r = await fetch ( EXPLORER _TRACK1 _BASE + '/bridge/status' , { credentials : 'omit' } ) ;
if ( r . ok ) snap . bridgeStatus = await r . json ( ) ;
} catch ( e ) { }
var blob = new Blob ( [ JSON . stringify ( snap , null , 2 ) ] , { type : 'application/json' } ) ;
var url = URL . createObjectURL ( blob ) ;
var a = document . createElement ( 'a' ) ;
a . href = url ;
a . download = 'explorer-institution-snapshot-' + snap . generatedAt . slice ( 0 , 19 ) . replace ( /[:]/g , '-' ) + '.json' ;
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
var list = getReportSchedules ( ) ;
for ( var i = 0 ; i < list . length ; i ++ ) {
list [ i ] . lastDownloadAt = snap . generatedAt ;
}
setReportSchedules ( list ) ;
if ( typeof showToast === 'function' ) showToast ( 'Snapshot downloaded' , 'success' ) ;
renderInstitutionView ( ) ;
} ;
var _watchlistNotifTimer = null ;
function restartWatchlistInstitutionNotifications ( ) {
if ( _watchlistNotifTimer ) {
clearInterval ( _watchlistNotifTimer ) ;
_watchlistNotifTimer = null ;
}
_watchlistNotifTimer = setInterval ( tickWatchlistInstitutionNotifications , 60000 ) ;
}
window . restartWatchlistInstitutionNotifications = restartWatchlistInstitutionNotifications ;
async function tickWatchlistInstitutionNotifications ( ) {
var s = getNotifSettings ( ) ;
if ( ! authToken || ! s . enabled ) return ;
var wl = getWatchlist ( ) ;
if ( ! wl . length ) return ;
var last = 0 ;
try { last = parseInt ( sessionStorage . getItem ( 'explorer_notif_last_run' ) || '0' , 10 ) ; } catch ( e ) { }
var now = Date . now ( ) ;
if ( now - last < s . intervalMin * 60000 ) return ;
try { sessionStorage . setItem ( 'explorer_notif_last_run' , String ( now ) ) ; } catch ( e ) { }
var snap = getNotifSnap ( ) ;
var slice = wl . slice ( 0 , 8 ) ;
for ( var i = 0 ; i < slice . length ; i ++ ) {
var addr = slice [ i ] ;
try {
var res = await fetchChain138AddressDetail ( addr ) ;
var ad = res && res . address ;
var tc = ad && ad . transaction _count != null ? ad . transaction _count : 0 ;
var prev = snap [ addr . toLowerCase ( ) ] ;
if ( prev != null && tc > prev && typeof showToast === 'function' ) {
showToast ( 'Watchlist activity: ' + shortenHash ( addr ) + ' tx count ' + prev + ' → ' + tc , 'info' , 8000 ) ;
}
snap [ addr . toLowerCase ( ) ] = tc ;
} catch ( e ) { }
}
setNotifSnap ( snap ) ;
}
2026-02-16 03:09:53 -08:00
function applyHashRoute ( ) {
2026-05-10 12:56:30 -07:00
try {
2026-02-16 03:09:53 -08:00
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 ; }
2026-04-10 12:52:17 -07:00
if ( ( parts [ 0 ] === 'address' || parts [ 0 ] === 'addresses' ) && 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 ; }
2026-03-02 12:14:13 -08:00
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 ; }
2026-05-10 12:56:30 -07:00
if ( parts [ 0 ] === 'institution' ) {
if ( ! authToken || ! hasAccess ( 2 ) ) { if ( currentView !== 'home' ) showHome ( ) ; return ; }
if ( currentView !== 'institution' ) switchToView ( 'institution' ) ;
renderInstitutionView ( ) ;
return ;
}
if ( parts [ 0 ] === 'compare' ) {
if ( ! authToken || ! hasAccess ( 2 ) ) { if ( currentView !== 'home' ) showHome ( ) ; return ; }
var c1 = parts [ 1 ] ? decode ( parts [ 1 ] ) : '' ;
var c2 = parts [ 2 ] ? decode ( parts [ 2 ] ) : '' ;
if ( currentView !== 'compare' ) switchToView ( 'compare' ) ;
renderCompareView ( c1 , c2 ) ;
return ;
}
2026-02-16 03:09:53 -08:00
if ( parts [ 0 ] === 'analytics' ) { if ( currentView !== 'analytics' ) showAnalytics ( ) ; return ; }
if ( parts [ 0 ] === 'operator' ) { if ( currentView !== 'operator' ) showOperator ( ) ; return ; }
2026-04-07 23:22:12 -07:00
if ( parts [ 0 ] === 'system' ) { if ( currentView !== 'system' ) showSystemTopology ( ) ; return ; }
2026-05-10 12:56:30 -07:00
} finally {
if ( typeof updateNavAriaCurrentFromPath === 'function' ) {
updateNavAriaCurrentFromPath ( window . location . pathname ) ;
}
}
2026-02-16 03:09:53 -08:00
}
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-04-07 23:22:12 -07:00
case 'system' :
breadcrumbContainer = document . getElementById ( 'systemBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
breadcrumbHTML += '<span class="breadcrumb-current">System</span>' ;
break ;
2026-02-16 03:09:53 -08:00
case 'nft' :
breadcrumbContainer = document . getElementById ( 'nftDetailBreadcrumb' ) ;
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>' ;
2026-04-10 12:52:17 -07:00
breadcrumbHTML += '<a href="/addresses/' + encodeURIComponent ( identifier ) + '">' + escapeHtml ( shortenHash ( identifier ) ) + '</a>' ;
2026-02-16 03:09:53 -08:00
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
2026-04-07 23:22:12 -07:00
async function fetchAPIWithRetry ( url , maxRetries = FETCH _MAX _RETRIES , retryDelay = RETRY _DELAY _MS , timeoutMs = FETCH _TIMEOUT _MS ) {
2026-02-16 03:09:53 -08:00
for ( let attempt = 0 ; attempt < maxRetries ; attempt ++ ) {
try {
2026-04-07 23:22:12 -07:00
return await fetchAPI ( url , timeoutMs ) ;
2026-02-16 03:09:53 -08:00
} 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 ) ) ;
}
}
}
2026-04-07 23:22:12 -07:00
async function fetchAPI ( url , timeoutMs = FETCH _TIMEOUT _MS ) {
2026-02-16 03:09:53 -08:00
const controller = new AbortController ( ) ;
2026-04-07 23:22:12 -07:00
const timeoutId = setTimeout ( ( ) => controller . abort ( ) , timeoutMs ) ;
2026-02-16 03:09:53 -08:00
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 ) ) ;
2026-04-10 12:52:17 -07:00
var blockHref = '/blocks/' + encodeURIComponent ( String ( d . blockNum ) ) ;
2026-03-28 13:59:00 -07:00
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 ) ;
2026-04-10 12:52:17 -07:00
var txHref = '/transactions/' + encodeURIComponent ( hash ) ;
2026-03-28 13:59:00 -07:00
var hashLink = '<a class="hash" href="' + txHref + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + safeHash + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml ( shortenHash ( hash ) ) + '</a>' ;
2026-04-10 12:52:17 -07:00
var fromLink = safeAddress ( from ) ? '<a class="hash" href="/addresses/' + 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="/addresses/' + 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="/blocks/' + 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 ) ) ;
2026-03-28 13:59:00 -07:00
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-04-10 12:52:17 -07:00
html += '<td><a class="hash" href="/addresses/' + 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 = [
2026-04-07 23:22:12 -07:00
{
title : 'Mission-control cached token pools' ,
method : 'GET' ,
href : EXPLORER _API _V1 _BASE + '/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools' ,
notes : '30-second cached proxy to token-aggregation pools for the configured chain. Useful for fast operator checks and UI panels.'
} ,
2026-03-27 12:02:36 -07:00
{
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 = [
2026-04-07 23:22:12 -07:00
'GET ' + EXPLORER _API _V1 _BASE + '/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools' ,
2026-03-27 12:02:36 -07:00
'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>' ;
2026-04-10 12:52:17 -07:00
html += '<a class="btn btn-secondary" href="/docs" style="text-decoration:none;"><i class="fas fa-book"></i> Explorer docs</a>' ;
2026-03-27 12:02:36 -07:00
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' ) ;
2026-04-10 12:52:17 -07:00
if ( ( window . location . pathname || '' ) . replace ( /^\// , '' ) . split ( '/' ) [ 0 ] !== 'operations' ) updatePath ( '/operations' ) ;
2026-03-24 18:11:08 -07:00
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' } ,
2026-04-30 03:06:49 -07:00
{ title : 'Unit Converter' , icon : 'fa-scale-balanced' , status : 'Live' , badgeClass : 'badge-success' , desc : 'Convert wei, gwei, ether, and common Chain 138 6-decimal GRU units with a quick in-page helper.' , action : 'showUnitConverterModal();' , href : '/operations' } ,
2026-03-27 12:11:18 -07:00
{ 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-04-07 23:22:12 -07:00
{ title : 'Visual Command Center' , icon : 'fa-satellite-dish' , status : 'Live' , badgeClass : 'badge-info' , desc : 'Interactive Mermaid topology: Chain 138 hub, CCIP, Alltra, stack, flows, cross-chain, cW Mainnet, and off-chain integrations (from SMOM_DBIS_138_FULL_DEPLOYMENT_FLOW_MAP).' , action : 'window.location.href=\'/chain138-command-center.html\';' , href : '/chain138-command-center.html' } ,
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' } ,
2026-04-10 12:52:17 -07:00
{ 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 : '/operations' }
2026-03-27 12:11:18 -07:00
]
} ,
{
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 : '#' } ,
2026-04-10 12:52:17 -07:00
{ 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 : '/operations' } ,
2026-03-27 12:11:18 -07:00
{ 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%;">' ;
2026-04-10 12:52:17 -07:00
html += '<div style="font-size:1.25rem; font-weight:800; margin-bottom:0.75rem;">Operations Hub</div>' ;
2026-04-30 03:06:49 -07:00
html += '<div style="color:var(--text-light); line-height:1.7; margin-bottom:1rem;">Discover DBIS Explorer operational tools in one place, grouped the way users expect from a polished specialist explorer.</div>' ;
2026-03-27 12:11:18 -07:00
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();' ) ) ;
2026-04-10 12:52:17 -07:00
var href = disabled ? '/operations' : item . href ;
2026-03-27 12:11:18 -07:00
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);">' +
2026-04-30 03:06:49 -07:00
'<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 GRU units for Chain 138.</div></div>' +
2026-03-27 12:11:18 -07:00
'<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>' +
2026-04-30 03:06:49 -07:00
'<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">GRU token (6 decimals)</option></select></label>' +
2026-03-27 12:11:18 -07:00
'<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 ;
2026-04-07 23:22:12 -07:00
var amount = String ( amountEl . value || '' ) . trim ( ) ;
2026-03-27 12:11:18 -07:00
var unit = unitEl . value ;
2026-04-07 23:22:12 -07:00
var decimals = unit === 'ether' ? 18 : ( unit === 'gwei' ? 9 : ( unit === 'stable' ? 6 : 0 ) ) ;
var baseUnits = parseDecimalToUnits ( amount , decimals ) ;
if ( baseUnits == null ) {
2026-03-27 12:11:18 -07:00
resultsEl . innerHTML = '<div style="color:var(--text-light);">Enter a non-negative amount to convert.</div>' ;
return ;
}
2026-04-07 23:22:12 -07:00
var etherValue = unit === 'stable' ? 'N/A' : formatUnits ( baseUnits , 18 , 18 ) ;
var gweiValue = unit === 'stable' ? 'N/A' : formatUnits ( baseUnits , 9 , 9 ) ;
var stableValue = formatUnits ( baseUnits , 6 , 6 ) ;
var baseUnitsLabel = unit === 'stable' ? 'Stable base units' : 'Wei' ;
2026-03-27 12:11:18 -07:00
resultsEl . innerHTML =
2026-04-07 23:22:12 -07:00
'<div style="padding:0.8rem; border:1px solid var(--border); border-radius:12px; background:var(--muted-surface);"><strong>' + escapeHtml ( baseUnitsLabel ) + ':</strong> ' + escapeHtml ( baseUnits . toString ( ) ) + '</div>' +
2026-03-27 12:11:18 -07:00
'<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
2026-04-07 23:22:12 -07:00
const WETH9 _BRIDGE _138 = '0xcacfd227A040002e49e2e01626363071324f820a' ;
2026-02-16 03:09:53 -08:00
const WETH10 _BRIDGE _138 = '0xe0E93247376aa097dB308B92e6Ba36bA015535D0' ;
2026-05-22 17:58:27 -07:00
// Ethereum Mainnet bridge references (relay release + CCIP hubs)
const WETH9 _BRIDGE _MAINNET _RELAY = '0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939' ;
const WETH9 _BRIDGE _MAINNET _HUB = '0xc9901ce2Ddb6490FAA183645147a87496d8b20B6' ;
2026-04-07 23:22:12 -07:00
const WETH10 _BRIDGE _MAINNET = '0x04E1e22B0D41e99f4275bd40A50480219bc9A223' ;
2026-05-22 17:58:27 -07:00
const CW _BRIDGE _MAINNET = '0x2bF74583206A49Be07E0E8A94197C12987AbD7B5' ;
const CW _L1 _BRIDGE _138 = '0x152ed3e9912161b76bdfd368d0c84b7c31c10de7' ;
2026-03-27 12:02:36 -07:00
const explorerLinks = {
'BSC (56)' : { label : 'BscScan' , baseUrl : 'https://bscscan.com/address/' } ,
2026-05-22 17:58:27 -07:00
'BNB Chain (56)' : { label : 'BscScan' , baseUrl : 'https://bscscan.com/address/' } ,
2026-03-27 12:02:36 -07:00
'Polygon (137)' : { label : 'PolygonScan' , baseUrl : 'https://polygonscan.com/address/' } ,
'Avalanche (43114)' : { label : 'Avalanche Explorer' , baseUrl : 'https://subnets.avax.network/c-chain/address/' } ,
2026-05-22 17:58:27 -07:00
'Avalanche C-Chain (43114)' : { label : 'Avalanche Explorer' , baseUrl : 'https://subnets.avax.network/c-chain/address/' } ,
2026-03-27 12:02:36 -07:00
'Base (8453)' : { label : 'BaseScan' , baseUrl : 'https://basescan.org/address/' } ,
'Arbitrum (42161)' : { label : 'Arbiscan' , baseUrl : 'https://arbiscan.io/address/' } ,
2026-05-22 17:58:27 -07:00
'Arbitrum One (42161)' : { label : 'Arbiscan' , baseUrl : 'https://arbiscan.io/address/' } ,
2026-03-27 12:02:36 -07:00
'Optimism (10)' : { label : 'Optimistic Etherscan' , baseUrl : 'https://optimistic.etherscan.io/address/' } ,
2026-05-22 17:58:27 -07:00
'Gnosis (100)' : { label : 'GnosisScan' , baseUrl : 'https://gnosisscan.io/address/' } ,
'Cronos (25)' : { label : 'Cronoscan' , baseUrl : 'https://cronoscan.com/address/' } ,
'Celo (42220)' : { label : 'CeloScan' , baseUrl : 'https://celoscan.io/address/' } ,
2026-03-27 12:02:36 -07:00
'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
2026-05-22 17:58:27 -07:00
// Bridge routes — prefer live token-aggregation catalog; static fallback matches config/bridge-routes-chain138-default.json
var routes = {
2026-02-16 03:09:53 -08:00
weth9 : {
2026-05-22 17:58:27 -07:00
'Ethereum Mainnet (1)' : WETH9 _BRIDGE _MAINNET _RELAY ,
'BNB Chain (56)' : '0x886C6A4ABC064dbf74E7caEc460b7eeC31F1b78C' ,
2026-04-07 23:22:12 -07:00
'Polygon (137)' : '0xF7736443f02913e7e0773052103296CfE1637448' ,
2026-05-22 17:58:27 -07:00
'Avalanche C-Chain (43114)' : '0x3f8C409C6072a2B6a4Ff17071927bA70F80c725F' ,
2026-04-07 23:22:12 -07:00
'Base (8453)' : '0x24293CA562aE1100E60a4640FF49bd656cFf93B4' ,
2026-05-22 17:58:27 -07:00
'Arbitrum One (42161)' : '0x937824f2516fa58f25aeAb92E7BFf7D74F463B4c' ,
2026-04-07 23:22:12 -07:00
'Optimism (10)' : '0x6e94e53F73893b2a6784Df663920D31043A6dE07' ,
2026-05-22 17:58:27 -07:00
'Gnosis (100)' : '0x4ab39b5BaB7b463435209A9039bd40Cf241F5a82' ,
'Cronos (25)' : '0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab' ,
'Celo (42220)' : '0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04'
2026-02-16 03:09:53 -08:00
} ,
weth10 : {
2026-05-22 17:58:27 -07:00
'Ethereum Mainnet (1)' : WETH10 _BRIDGE _MAINNET ,
'BNB Chain (56)' : '0x937824f2516fa58f25aeAb92E7BFf7D74F463B4c' ,
2026-04-07 23:22:12 -07:00
'Polygon (137)' : '0x0CA60e6f8589c540200daC9D9Cb27BC2e48eE66A' ,
2026-05-22 17:58:27 -07:00
'Avalanche C-Chain (43114)' : '0x937824f2516fa58f25aeAb92E7BFf7D74F463B4c' ,
2026-04-07 23:22:12 -07:00
'Base (8453)' : '0x937824f2516fa58f25aeAb92E7BFf7D74F463B4c' ,
2026-05-22 17:58:27 -07:00
'Arbitrum One (42161)' : '0x73376eB92c16977B126dB9112936A20Fa0De3442' ,
2026-04-07 23:22:12 -07:00
'Optimism (10)' : '0x24293CA562aE1100E60a4640FF49bd656cFf93B4' ,
2026-05-22 17:58:27 -07:00
'Gnosis (100)' : '0xC15ACdBAC59B3C7Cb4Ea4B3D58334A4b143B4b44' ,
'Cronos (25)' : '0x73376eB92c16977B126dB9112936A20Fa0De3442' ,
'Celo (42220)' : '0xa4B9DD039565AeD9641D45b57061f99d9cA6Df08'
2026-02-16 03:09:53 -08:00
}
} ;
2026-05-22 17:58:27 -07:00
var bridgeRoutesSource = 'static-fallback' ;
try {
var routesResp = await fetch ( TOKEN _AGGREGATION _API _BASE + '/v1/bridge/routes' , { credentials : 'omit' } ) ;
if ( routesResp . ok ) {
var routesPayload = await routesResp . json ( ) ;
if ( routesPayload && routesPayload . routes && routesPayload . routes . weth9 ) {
routes = routesPayload . routes ;
bridgeRoutesSource = routesPayload . source || 'token-aggregation' ;
if ( routesPayload . chain138Bridges && routesPayload . chain138Bridges . weth9 ) {
// no-op: source addresses already set above from canonical constants
}
}
}
} catch ( routesErr ) {
console . warn ( 'Bridge routes API unavailable, using static catalog' , routesErr ) ;
}
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;" >
2026-05-22 17:58:27 -07:00
Cross - chain interoperability powered by Chainlink CCIP . Route catalog source : < code > $ { escapeHtml ( bridgeRoutesSource ) } < / c o d e > .
2026-02-16 03:09:53 -08:00
< / d i v >
2026-04-07 23:22:12 -07:00
< div class = "badge badge-warning" style = "display:inline-flex; white-space:normal; text-align:left;" >
Arbitrum remains route - blocked on the current Mainnet hub leg . The latest Mainnet - > Arbitrum WETH9 send reverted before any bridge event was emitted , so treat Arbitrum as unavailable until that hub path is repaired .
< / d i v >
< / d i v >
< div class = "card" style = "margin-bottom: 1.5rem;" >
< div class = "card-header" >
< h3 class = "card-title" > < i class = "fas fa-magnifying-glass-chart" > < / i > M i s s i o n - c o n t r o l b r i d g e t r a c e < / h 3 >
< a class = "btn btn-secondary" href = "/explorer-api/v1/mission-control/stream" target = "_blank" rel = "noopener noreferrer" style = "text-decoration:none;" > < i class = "fas fa-satellite-dish" > < / i > O p e n S S E s t r e a m < / a >
< / d i v >
< div style = "display:grid; gap:0.75rem;" >
< div style = "color: var(--text-light); line-height: 1.6;" > Paste a transaction hash to resolve the < code > from < / c o d e > a n d < c o d e > t o < / c o d e > a d d r e s s e s t h r o u g h t h e m i s s i o n - c o n t r o l b r i d g e - t r a c e A P I . T h e r e s p o n s e i s l a b e l e d w i t h C h a i n 1 3 8 c o n t r a c t n a m e s f r o m t h e s m a r t - c o n t r a c t s r e g i s t r y w h e n a v a i l a b l e . < / d i v >
< div style = "display:flex; gap:0.6rem; flex-wrap:wrap;" >
< input id = "missionControlBridgeTraceInput" type = "text" placeholder = "0x transaction hash" style = "flex:1 1 480px; min-width:280px; padding:0.8rem 0.9rem; border:1px solid var(--border); border-radius:12px; background:var(--light); color:var(--text);" onkeydown = "if (event.key === 'Enter') runMissionControlBridgeTrace();" / >
< button type = "button" class = "btn btn-primary" onclick = "runMissionControlBridgeTrace()" > < i class = "fas fa-search" > < / i > T r a c e t r a n s a c t i o n < / b u t t o n >
< / d i v >
< div id = "missionControlBridgeTraceResult" style = "min-height: 1.5rem;" > < / d i v >
< / d i v >
2026-02-16 03:09:53 -08:00
< / 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-05-22 17:58:27 -07:00
< span class = "badge badge-info" style = "white-space: nowrap;" > Relay + CCIP hubs + cW L2 < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
2026-05-22 17:58:27 -07:00
< div class = "chain-info" style = "grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;" >
2026-02-16 03:09:53 -08:00
< div class = "chain-stat" style = "background: var(--light); padding: 1rem; border-radius: 8px;" >
2026-05-22 17:58:27 -07:00
< div class = "chain-stat-label" style = "font-weight: 600; margin-bottom: 0.5rem;" > CCIPRelayBridge ( 138 → 1 WETH9 ) < / d i v >
2026-02-16 03:09:53 -08:00
< div class = "chain-stat-value" >
2026-05-22 17:58:27 -07:00
< span class = "hash" onclick = "window.open('https://etherscan.io/address/${WETH9_BRIDGE_MAINNET_RELAY}', '_blank', 'noopener,noreferrer')" style = "cursor: pointer; font-size: 0.9rem;" > $ { WETH9 _BRIDGE _MAINNET _RELAY } < / s p a n >
2026-02-16 03:09:53 -08:00
< / d i v >
2026-05-22 17:58:27 -07:00
< / 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;" > CCIPWETH9Bridge ( hub ) < / d i v >
< div class = "chain-stat-value" >
< span class = "hash" onclick = "window.open('https://etherscan.io/address/${WETH9_BRIDGE_MAINNET_HUB}', '_blank', 'noopener,noreferrer')" style = "cursor: pointer; font-size: 0.9rem;" > $ { WETH9 _BRIDGE _MAINNET _HUB } < / s p a n >
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 >
2026-05-22 17:58:27 -07:00
< / 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;" > CWMultiTokenBridge ( cW * ) < / d i v >
< div class = "chain-stat-value" >
< span class = "hash" onclick = "window.open('https://etherscan.io/address/${CW_BRIDGE_MAINNET}', '_blank', 'noopener,noreferrer')" style = "cursor: pointer; font-size: 0.9rem;" > $ { CW _BRIDGE _MAINNET } < / s p a n >
< / 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;" > CW L1 bridge ( Chain 138 ) < / d i v >
< div class = "chain-stat-value" >
$ { explorerAddressLink ( CW _L1 _BRIDGE _138 , escapeHtml ( CW _L1 _BRIDGE _138 ) , 'color: inherit; text-decoration: none; font-size: 0.9rem;' ) }
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 >
2026-04-07 23:22:12 -07:00
< li > < strong > Arbitrum < / s t r o n g > - A r b i t r u m O n e ( c o n f i g u r e d , b u t t h e c u r r e n t M a i n n e t h u b l e g i s b l o c k e d ) < / l i >
2026-02-16 03:09:53 -08:00
< 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-04-07 23:22:12 -07: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 t h e f u l l d e s t i n a t i o n l i s t , i n c l u d i n g l o a d e d C C I P d e s t i n a t i o n s s u c h a s G n o s i s a n d C e l o , p l u s p e n d i n g 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-04-07 23:22:12 -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 4 2 D A b 7 b 8 8 8 D d 3 8 2 b D 5 A d c f 9 E 0 3 8 d B F 1 f D 0 3 b 4 8 1 7 ' , ' 0 x 4 2 D A b 7 b 8 8 8 D d 3 8 2 b D 5 A d c f 9 E 0 3 8 d B F 1 f D 0 3 b 4 8 1 7 ' , ' 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-03-28 14:09:23 -07:00
< 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-05-10 12:56:30 -07:00
function guessContractAddressFromPath ( ) {
var path = ( typeof window !== 'undefined' && window . location && window . location . pathname ) ? window . location . pathname : '' ;
var m = path . match ( /^\/addresses\/(0x[a-fA-F0-9]{40})\/?/i ) ;
if ( m ) return m [ 1 ] ;
m = path . match ( /^\/address\/(0x[a-fA-F0-9]{40})\/?/i ) ;
return m ? m [ 1 ] : '' ;
}
function institutionExternalLinksBlockscout ( ) {
return ( getInstitutionPrefs ( ) . externalLinks || 'spa' ) === 'blockscout' ;
}
function clearExplorerPendingAddressInitialTabFor ( addr ) {
var p = window . _ _explorerPendingAddressInitialTab ;
var low = addr && String ( addr ) . toLowerCase ( ) ;
if ( p && low && p . address === low ) {
window . _ _explorerPendingAddressInitialTab = null ;
}
}
function institutionOpenContractInExplorer ( addr ) {
var a = safeAddress ( addr ) ;
if ( ! a ) return ;
try {
window . _ _explorerPendingAddressInitialTab = { tab : 'contract' , address : a . toLowerCase ( ) } ;
if ( typeof showAddressDetail === 'function' ) showAddressDetail ( a ) ;
if ( typeof updatePath === 'function' ) updatePath ( '/addresses/' + a + '/contract' ) ;
} catch ( e ) { }
}
window . institutionOpenContractInExplorer = institutionOpenContractInExplorer ;
function institutionContractExploreAnchor ( addr , extLabel , spaLabel , inlineStyle ) {
var a = safeAddress ( addr ) ;
if ( ! a ) return '' ;
var sty = inlineStyle ? ' style="' + escapeAttr ( inlineStyle ) + '"' : '' ;
if ( institutionExternalLinksBlockscout ( ) ) {
return '<a href="' + escapeAttr ( EXPLORER _ORIGIN + '/address/' + encodeURIComponent ( a ) + '/contract' ) + '" target="_blank" rel="noopener noreferrer"' + sty + '>' + escapeHtml ( extLabel ) + '</a>' ;
}
var addressForJs = a . replace ( /\\/g , '\\\\' ) . replace ( /'/g , "\\'" ) ;
return '<a href="#"' + sty + ' role="link" onclick="institutionOpenContractInExplorer(\'' + addressForJs + '\'); return false;">' + escapeHtml ( spaLabel ) + '</a>' ;
}
function institutionAddressContractLinkHtml ( addr ) {
return institutionContractExploreAnchor ( addr , 'View contract on Blockscout' , 'Open contract tab' , 'color: var(--primary); font-size: 0.875rem;' ) ;
}
function syncContractVerifyBlockscoutLink ( ) {
var input = document . getElementById ( 'contractVerifyAddressInput' ) ;
var link = document . getElementById ( 'contractVerifyBlockscoutLink' ) ;
if ( ! link ) return ;
var raw = input && input . value ? String ( input . value ) . trim ( ) : '' ;
var addr = safeAddress ( raw ) || safeAddress ( guessContractAddressFromPath ( ) ) ;
link . onclick = null ;
if ( addr ) {
if ( institutionExternalLinksBlockscout ( ) ) {
link . href = EXPLORER _ORIGIN + '/address/' + encodeURIComponent ( addr ) + '/contract' ;
link . setAttribute ( 'target' , '_blank' ) ;
link . setAttribute ( 'rel' , 'noopener noreferrer' ) ;
} else {
link . href = '#' ;
link . removeAttribute ( 'target' ) ;
link . removeAttribute ( 'rel' ) ;
( function ( contractAddr ) {
link . onclick = function ( ev ) {
ev . preventDefault ( ) ;
institutionOpenContractInExplorer ( contractAddr ) ;
if ( typeof closeContractVerifyHelpModal === 'function' ) closeContractVerifyHelpModal ( ) ;
return false ;
} ;
} ) ( addr ) ;
}
link . setAttribute ( 'aria-disabled' , 'false' ) ;
link . style . pointerEvents = '' ;
link . style . opacity = '' ;
} else {
link . href = '#' ;
link . removeAttribute ( 'target' ) ;
link . removeAttribute ( 'rel' ) ;
link . onclick = function ( ev ) { ev . preventDefault ( ) ; return false ; } ;
link . setAttribute ( 'aria-disabled' , 'true' ) ;
link . style . pointerEvents = 'none' ;
link . style . opacity = '0.55' ;
}
}
function closeContractVerifyHelpModal ( ) {
if ( typeof focusTrapEnd === 'function' ) focusTrapEnd ( ) ;
var modal = document . getElementById ( 'contractVerifyHelpModal' ) ;
if ( ! modal ) return ;
modal . style . display = 'none' ;
modal . setAttribute ( 'aria-hidden' , 'true' ) ;
document . body . style . overflow = '' ;
}
window . showContractVerificationHelp = function showContractVerificationHelp ( prefillAddress ) {
if ( ! authToken || ! hasAccess ( 4 ) ) {
if ( typeof showToast === 'function' ) {
showToast ( 'Contract verification tools are available to operator-approved accounts.' , 'warning' ) ;
} else {
alert ( 'Contract verification tools are available to operator-approved accounts.' ) ;
}
return ;
}
var modal = document . getElementById ( 'contractVerifyHelpModal' ) ;
if ( ! modal ) return ;
if ( typeof closeAllNavDropdowns === 'function' ) {
closeAllNavDropdowns ( ) ;
}
var input = document . getElementById ( 'contractVerifyAddressInput' ) ;
var fromArg = prefillAddress && safeAddress ( prefillAddress ) ;
var fromPath = guessContractAddressFromPath ( ) ;
var initial = fromArg || fromPath || ( input && input . value ? safeAddress ( input . value ) : null ) ;
if ( input ) {
input . value = initial || '' ;
}
syncContractVerifyBlockscoutLink ( ) ;
modal . style . display = 'block' ;
modal . setAttribute ( 'aria-hidden' , 'false' ) ;
document . body . style . overflow = 'hidden' ;
if ( typeof focusTrapStart === 'function' ) focusTrapStart ( modal ) ;
} ;
function initContractVerifyHelpModal ( ) {
var modal = document . getElementById ( 'contractVerifyHelpModal' ) ;
var backdrop = document . getElementById ( 'contractVerifyHelpBackdrop' ) ;
var closeBtn = document . getElementById ( 'contractVerifyHelpCloseBtn' ) ;
var input = document . getElementById ( 'contractVerifyAddressInput' ) ;
var copyBtn = document . getElementById ( 'contractVerifyCopyCommandBtn' ) ;
var cmdEl = document . getElementById ( 'contractVerifyScriptCommand' ) ;
if ( backdrop ) {
backdrop . addEventListener ( 'click' , closeContractVerifyHelpModal ) ;
}
if ( closeBtn ) {
closeBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ( ) ;
closeContractVerifyHelpModal ( ) ;
} ) ;
}
if ( input ) {
input . addEventListener ( 'input' , syncContractVerifyBlockscoutLink ) ;
}
if ( copyBtn && cmdEl ) {
copyBtn . addEventListener ( 'click' , function ( e ) {
e . preventDefault ( ) ;
var text = cmdEl . textContent || './scripts/verify/run-contract-verification-with-proxy.sh' ;
if ( navigator . clipboard && navigator . clipboard . writeText ) {
navigator . clipboard . writeText ( text ) . then ( function ( ) {
if ( typeof showToast === 'function' ) showToast ( 'Command copied' , 'success' ) ;
} ) . catch ( function ( ) {
window . prompt ( 'Copy:' , text ) ;
} ) ;
} else {
window . prompt ( 'Copy:' , text ) ;
}
} ) ;
}
}
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' ;
2026-04-10 12:52:17 -07:00
return '<a class="hash" href="/addresses/' + encodeURIComponent ( safe ) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeJsSingleQuoted ( safe ) + '\')" style="' + escapeAttr ( style || 'color: inherit; text-decoration: none;' ) + '">' + ( content || escapeHtml ( shortenHash ( safe ) ) ) + '</a>' ;
2026-03-28 14:09:23 -07:00
}
function explorerTransactionLink ( txHash , content , style ) {
var safe = safeTxHash ( txHash ) ;
if ( ! safe ) return content || 'N/A' ;
2026-04-10 12:52:17 -07:00
return '<a class="hash" href="/transactions/' + encodeURIComponent ( safe ) + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + escapeJsSingleQuoted ( safe ) + '\')" style="' + escapeAttr ( style || 'color: inherit; text-decoration: none;' ) + '">' + ( content || escapeHtml ( shortenHash ( safe ) ) ) + '</a>' ;
2026-03-28 14:09:23 -07:00
}
function explorerBlockLink ( blockNumber , content , style ) {
var safe = safeBlockNumber ( blockNumber ) ;
if ( ! safe ) return content || 'N/A' ;
2026-04-10 12:52:17 -07:00
return '<a href="/blocks/' + 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 14:09:23 -07:00
}
2026-04-07 23:22:12 -07:00
function toBigIntSafe ( value ) {
if ( value == null || value === '' ) return null ;
if ( typeof value === 'bigint' ) return value ;
if ( typeof value === 'number' ) {
if ( ! Number . isFinite ( value ) ) return null ;
return BigInt ( Math . trunc ( value ) ) ;
}
var stringValue = String ( value ) . trim ( ) ;
if ( ! stringValue ) return null ;
if ( /^-?0x[0-9a-f]+$/i . test ( stringValue ) || /^-?\d+$/ . test ( stringValue ) ) {
try {
return BigInt ( stringValue ) ;
} catch ( e ) {
return null ;
}
}
return null ;
}
function formatGroupedDigits ( value , groupSize , separator ) {
var stringValue = String ( value == null ? '' : value ) ;
if ( ! stringValue ) return '' ;
var negative = stringValue [ 0 ] === '-' ;
var digits = negative ? stringValue . slice ( 1 ) : stringValue ;
var groups = [ ] ;
for ( var i = digits . length ; i > 0 ; i -= groupSize ) {
groups . unshift ( digits . slice ( Math . max ( 0 , i - groupSize ) , i ) ) ;
}
return ( negative ? '-' : '' ) + groups . join ( separator || ' ' ) ;
}
function summarizeInspectorValue ( value , head , tail ) {
var stringValue = String ( value == null ? '' : value ) ;
var start = head || 18 ;
var end = tail || 12 ;
if ( stringValue . length <= start + end + 1 ) return stringValue ;
return stringValue . slice ( 0 , start ) + '...' + stringValue . slice ( - end ) ;
}
function buildNumericRepresentations ( value ) {
var parsed = toBigIntSafe ( value ) ;
if ( parsed == null ) return null ;
var negative = parsed < 0 n ;
var absolute = negative ? - parsed : parsed ;
var decimalPlain = parsed . toString ( ) ;
var hexDigits = absolute . toString ( 16 ) ;
var binaryDigits = absolute . toString ( 2 ) ;
var bitLength = absolute === 0 n ? 1 : binaryDigits . length ;
var byteLength = absolute === 0 n ? 1 : Math . ceil ( bitLength / 8 ) ;
return {
decimal : formatGroupedDigits ( decimalPlain , 3 , ',' ) ,
decimal _plain : decimalPlain ,
hex : ( negative ? '-0x' : '0x' ) + formatGroupedDigits ( hexDigits , 4 , ' ' ) ,
hex _plain : ( negative ? '-0x' : '0x' ) + hexDigits ,
binary : ( negative ? '-0b' : '0b' ) + formatGroupedDigits ( binaryDigits , 8 , ' ' ) ,
binary _plain : ( negative ? '-0b' : '0b' ) + binaryDigits ,
bit _length : bitLength ,
byte _length : byteLength
} ;
}
function renderCopyButtonHtml ( value , ariaLabel ) {
if ( value == null || value === '' ) return '' ;
return ' <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard(\'' + escapeJsSingleQuoted ( String ( value ) ) + '\', \'Copied\');" aria-label="' + escapeAttr ( ariaLabel || 'Copy value' ) + '"><i class="fas fa-copy"></i></button>' ;
}
2026-04-10 12:52:17 -07:00
function renderInspectorCopyRow ( valueHtml , copyValue , ariaLabel ) {
return '<div class="tx-inspector-copy-row"><div class="tx-inspector-scroll">' + valueHtml + '</div>' + ( copyValue != null && copyValue !== '' ? '<div class="tx-inspector-copy-action">' + renderCopyButtonHtml ( copyValue , ariaLabel ) + '</div>' : '' ) + '</div>' ;
}
2026-04-07 23:22:12 -07:00
function renderInspectorHtmlLine ( label , valueHtml ) {
return '<div class="tx-inspector-line"><div class="tx-inspector-label">' + escapeHtml ( label ) + '</div><div class="tx-inspector-content">' + ( valueHtml || '<span class="tx-empty">N/A</span>' ) + '</div></div>' ;
}
function renderInspectorTextLine ( label , value , copyValue ) {
if ( value == null || value === '' ) return renderInspectorHtmlLine ( label , '<span class="tx-empty">N/A</span>' ) ;
2026-04-10 12:52:17 -07:00
return renderInspectorHtmlLine ( label , renderInspectorCopyRow ( '<span>' + escapeHtml ( String ( value ) ) + '</span>' , copyValue , 'Copy ' + label ) ) ;
2026-04-07 23:22:12 -07:00
}
function renderInspectorCodeLine ( label , value , copyValue ) {
if ( value == null || value === '' ) return renderInspectorHtmlLine ( label , '<span class="tx-empty">N/A</span>' ) ;
2026-04-10 12:52:17 -07:00
return renderInspectorHtmlLine ( label , renderInspectorCopyRow ( '<code class="tx-inspector-mono">' + escapeHtml ( String ( value ) ) + '</code>' , copyValue != null ? copyValue : value , 'Copy ' + label ) ) ;
2026-04-07 23:22:12 -07:00
}
function renderNumericInspectorEntry ( label , value , note , openByDefault ) {
var repr = buildNumericRepresentations ( value ) ;
if ( ! repr ) return '' ;
var summary = repr . hex _plain . length <= 22 ? repr . hex _plain : summarizeInspectorValue ( repr . hex _plain , 18 , 10 ) ;
var html = '<details class="tx-inspector-entry"' + ( openByDefault ? ' open' : '' ) + '>' ;
html += '<summary><span>' + escapeHtml ( label ) + '</span><span class="tx-inspector-summary-value">' + escapeHtml ( summary ) + '</span></summary>' ;
html += '<div class="tx-inspector-entry-body">' ;
if ( note ) {
html += '<div class="tx-inspector-note">' + escapeHtml ( note ) + '</div>' ;
}
html += renderInspectorCodeLine ( 'Dec' , repr . decimal , repr . decimal _plain ) ;
html += renderInspectorCodeLine ( 'Hex' , repr . hex , repr . hex _plain ) ;
html += renderInspectorCodeLine ( 'Bin' , repr . binary , repr . binary _plain ) ;
html += renderInspectorTextLine ( 'Size' , repr . bit _length + ' bits / ' + repr . byte _length + ' bytes' ) ;
html += '</div></details>' ;
return html ;
}
function safeJsonStringify ( value ) {
try {
return JSON . stringify ( value , function ( key , item ) {
return typeof item === 'bigint' ? item . toString ( ) : item ;
} , 2 ) ;
} catch ( error ) {
return String ( value == null ? '' : value ) ;
}
}
function formatDurationValue ( value ) {
if ( ! Array . isArray ( value ) || value . length === 0 ) return '' ;
return value . map ( function ( item ) {
return typeof item === 'number' ? item . toLocaleString ( ) : String ( item ) ;
} ) . join ( ' -> ' ) + ' ms' ;
}
function formatLogDecodedValue ( value ) {
if ( value == null || value === '' ) return '' ;
return typeof value === 'string' ? value : safeJsonStringify ( value ) ;
}
2026-04-10 12:52:17 -07:00
var KNOWN _LOG _SIGNATURES = {
transfer : '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' ,
approval : '0x8c5be1e5ebec7d5bd14f714f7e582d5c3b27c1d03c7d98cfc9b7c6f7d3a5b5d' ,
approvalForAll : '0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31' ,
transferSingle : '0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62'
} ;
function formatTopicSignaturePreview ( topic ) {
if ( ! topic ) return '' ;
var value = String ( topic ) ;
return value . length > 20 ? value . slice ( 0 , 12 ) + '…' + value . slice ( - 6 ) : value ;
}
function normalizeHexWord ( value ) {
if ( ! value ) return '' ;
var normalized = String ( value ) . toLowerCase ( ) ;
if ( ! normalized . startsWith ( '0x' ) ) normalized = '0x' + normalized ;
return normalized ;
}
function splitHexDataWords ( dataValue ) {
var normalized = normalizeHexWord ( dataValue ) ;
if ( ! /^0x[0-9a-f]*$/i . test ( normalized ) ) return [ ] ;
var payload = normalized . slice ( 2 ) ;
var words = [ ] ;
for ( var i = 0 ; i < payload . length ; i += 64 ) {
var word = payload . slice ( i , i + 64 ) ;
if ( word . length === 64 ) words . push ( '0x' + word ) ;
}
return words ;
}
function extractAddressFromTopic ( topicValue ) {
var normalized = normalizeHexWord ( topicValue ) ;
if ( ! /^0x[0-9a-f]{64}$/i . test ( normalized ) ) return '' ;
return safeAddress ( '0x' + normalized . slice ( - 40 ) ) || '' ;
}
function extractBoolFromWord ( wordValue ) {
var parsed = toBigIntSafe ( wordValue ) ;
if ( parsed == null ) return '' ;
return parsed === 0 n ? 'false' : 'true' ;
}
function formatUintWord ( wordValue ) {
var parsed = toBigIntSafe ( wordValue ) ;
if ( parsed == null ) return '' ;
return formatGroupedDigits ( parsed . toString ( ) , 3 , ',' ) ;
}
function detectKnownLogEvent ( topics , dataValue ) {
var topic0 = topics && topics [ 0 ] ? normalizeHexWord ( topics [ 0 ] ) : '' ;
var words = splitHexDataWords ( dataValue ) ;
if ( ! topic0 ) return null ;
if ( topic0 === KNOWN _LOG _SIGNATURES . transfer ) {
if ( topics . length >= 4 ) {
return {
name : 'Transfer' ,
standard : 'ERC-721' ,
fields : [
{ label : 'From' , type : 'address' , value : extractAddressFromTopic ( topics [ 1 ] ) } ,
{ label : 'To' , type : 'address' , value : extractAddressFromTopic ( topics [ 2 ] ) } ,
{ label : 'Token ID' , type : 'uint' , value : normalizeHexWord ( topics [ 3 ] ) }
]
} ;
}
if ( topics . length >= 3 && words . length >= 1 ) {
return {
name : 'Transfer' ,
standard : 'ERC-20' ,
fields : [
{ label : 'From' , type : 'address' , value : extractAddressFromTopic ( topics [ 1 ] ) } ,
{ label : 'To' , type : 'address' , value : extractAddressFromTopic ( topics [ 2 ] ) } ,
{ label : 'Value' , type : 'uint' , value : words [ 0 ] }
]
} ;
}
}
if ( topic0 === KNOWN _LOG _SIGNATURES . approval ) {
if ( topics . length >= 4 ) {
return {
name : 'Approval' ,
standard : 'ERC-721' ,
fields : [
{ label : 'Owner' , type : 'address' , value : extractAddressFromTopic ( topics [ 1 ] ) } ,
{ label : 'Approved' , type : 'address' , value : extractAddressFromTopic ( topics [ 2 ] ) } ,
{ label : 'Token ID' , type : 'uint' , value : normalizeHexWord ( topics [ 3 ] ) }
]
} ;
}
if ( topics . length >= 3 && words . length >= 1 ) {
return {
name : 'Approval' ,
standard : 'ERC-20' ,
fields : [
{ label : 'Owner' , type : 'address' , value : extractAddressFromTopic ( topics [ 1 ] ) } ,
{ label : 'Spender' , type : 'address' , value : extractAddressFromTopic ( topics [ 2 ] ) } ,
{ label : 'Value' , type : 'uint' , value : words [ 0 ] }
]
} ;
}
}
if ( topic0 === KNOWN _LOG _SIGNATURES . approvalForAll && topics . length >= 3 && words . length >= 1 ) {
return {
name : 'ApprovalForAll' ,
standard : 'ERC-721 / ERC-1155' ,
fields : [
{ label : 'Owner' , type : 'address' , value : extractAddressFromTopic ( topics [ 1 ] ) } ,
{ label : 'Operator' , type : 'address' , value : extractAddressFromTopic ( topics [ 2 ] ) } ,
{ label : 'Approved' , type : 'bool' , value : words [ 0 ] }
]
} ;
}
if ( topic0 === KNOWN _LOG _SIGNATURES . transferSingle && topics . length >= 4 && words . length >= 2 ) {
return {
name : 'TransferSingle' ,
standard : 'ERC-1155' ,
fields : [
{ label : 'Operator' , type : 'address' , value : extractAddressFromTopic ( topics [ 1 ] ) } ,
{ label : 'From' , type : 'address' , value : extractAddressFromTopic ( topics [ 2 ] ) } ,
{ label : 'To' , type : 'address' , value : extractAddressFromTopic ( topics [ 3 ] ) } ,
{ label : 'Token ID' , type : 'uint' , value : words [ 0 ] } ,
{ label : 'Value' , type : 'uint' , value : words [ 1 ] }
]
} ;
}
return null ;
}
function renderStructuredLogFields ( eventInfo ) {
if ( ! eventInfo || ! Array . isArray ( eventInfo . fields ) || eventInfo . fields . length === 0 ) return '' ;
var lines = eventInfo . fields . map ( function ( field ) {
if ( ! field || ! field . value ) return '' ;
if ( field . type === 'address' ) {
return renderInspectorHtmlLine ( field . label , renderInspectorCopyRow ( explorerAddressLink ( field . value , escapeHtml ( field . value ) , 'color: inherit; text-decoration: none;' ) , field . value , 'Copy ' + field . label ) ) ;
}
if ( field . type === 'bool' ) {
var boolValue = extractBoolFromWord ( field . value ) ;
return renderInspectorTextLine ( field . label , boolValue || 'N/A' , boolValue || '' ) ;
}
if ( field . type === 'uint' ) {
var numeric = formatUintWord ( field . value ) ;
return renderInspectorTextLine ( field . label , numeric || 'N/A' , field . value ) ;
}
return renderInspectorTextLine ( field . label , field . value , field . value ) ;
} ) . filter ( function ( line ) { return line ; } ) . join ( '' ) ;
if ( ! lines ) return '' ;
return '<div class="tx-inspector-structured">' +
'<div class="tx-inspector-structured-title">Structured Fields</div>' +
lines +
'</div>' ;
}
2026-04-07 23:22:12 -07:00
function renderTransactionLogEntry ( log , idx ) {
var addressValue = ( log . address && ( log . address . hash || log . address ) ) || log . address || '' ;
var topics = Array . isArray ( log . topics ) ? log . topics . filter ( function ( topic ) { return topic != null ; } ) : [ ] ;
var dataValue = log . data || log . raw _data || '0x' ;
var decodedValue = formatLogDecodedValue ( log . decoded || log . decoded _text || '' ) ;
var dataBytes = /^0x[0-9a-f]*$/i . test ( String ( dataValue || '' ) ) ? Math . max ( 0 , ( String ( dataValue ) . length - 2 ) / 2 ) : 0 ;
var blockNumber = log . block _number != null ? String ( log . block _number ) : '' ;
var txHash = log . transaction _hash || log . transactionHash || '' ;
2026-04-10 12:52:17 -07:00
var knownEvent = detectKnownLogEvent ( topics , dataValue ) ;
2026-04-07 23:22:12 -07:00
var topicRows = topics . length ? '<div class="tx-inspector-topic-list">' + topics . map ( function ( topic , topicIndex ) {
2026-04-10 12:52:17 -07:00
return '<div class="tx-inspector-topic-row"><div class="tx-inspector-topic-index">Topic ' + topicIndex + '</div>' + renderInspectorCopyRow ( '<code class="tx-inspector-mono">' + escapeHtml ( String ( topic ) ) + '</code>' , String ( topic ) , 'Copy topic ' + topicIndex ) + '</div>' ;
2026-04-07 23:22:12 -07:00
} ) . join ( '' ) + '</div>' : '<span class="tx-empty">No topics</span>' ;
var metaChips = '<div class="tx-chip-row">' +
'<span class="tx-chip"><span class="tx-chip-label">Index</span><span>' + escapeHtml ( String ( log . index != null ? log . index : idx ) ) + '</span></span>' +
'<span class="tx-chip"><span class="tx-chip-label">Topics</span><span>' + escapeHtml ( String ( topics . length ) ) + '</span></span>' +
'<span class="tx-chip"><span class="tx-chip-label">Data</span><span>' + escapeHtml ( String ( dataBytes ) ) + ' bytes</span></span>' +
2026-04-10 12:52:17 -07:00
'<span class="tx-chip tx-chip-emphasis" id="txLogEventChip' + idx + '"><span class="tx-chip-label">Event</span><span>' + escapeHtml ( knownEvent && knownEvent . name ? knownEvent . name : ( decodedValue ? 'decoded' : ( topics [ 0 ] ? formatTopicSignaturePreview ( topics [ 0 ] ) : 'raw log' ) ) ) + '</span></span>' +
( knownEvent && knownEvent . standard ? '<span class="tx-chip"><span class="tx-chip-label">Standard</span><span>' + escapeHtml ( knownEvent . standard ) + '</span></span>' : '' ) +
2026-04-07 23:22:12 -07:00
'</div>' ;
2026-04-10 12:52:17 -07:00
var summaryTitle = 'Log #' + escapeHtml ( String ( log . index != null ? log . index : idx ) ) + ' • ' + escapeHtml ( knownEvent && knownEvent . name ? knownEvent . name : ( addressValue ? shortenHash ( addressValue ) : 'Unknown address' ) ) ;
2026-04-07 23:22:12 -07:00
var html = '<details class="tx-inspector-entry"' + ( idx === 0 ? ' open' : '' ) + '>' ;
2026-04-10 12:52:17 -07:00
html += '<summary><span id="txLogSummaryTitle' + idx + '">' + summaryTitle + '</span><span class="tx-inspector-summary-value">' + escapeHtml ( String ( topics . length ) ) + ' topics / ' + escapeHtml ( String ( dataBytes ) ) + ' bytes</span></summary>' ;
2026-04-07 23:22:12 -07:00
html += '<div class="tx-inspector-entry-body">' ;
html += metaChips ;
2026-04-10 12:52:17 -07:00
html += renderInspectorHtmlLine ( 'Address' , addressValue ? renderInspectorCopyRow ( explorerAddressLink ( addressValue , escapeHtml ( addressValue ) , 'color: inherit; text-decoration: none;' ) , addressValue , 'Copy log address' ) : '<span class="tx-empty">N/A</span>' ) ;
2026-04-07 23:22:12 -07:00
if ( blockNumber ) {
2026-04-10 12:52:17 -07:00
html += renderInspectorHtmlLine ( 'Block' , renderInspectorCopyRow ( explorerBlockLink ( blockNumber , escapeHtml ( blockNumber ) , 'color: inherit; text-decoration: none;' ) , blockNumber , 'Copy block number' ) ) ;
2026-04-07 23:22:12 -07:00
}
if ( txHash ) {
2026-04-10 12:52:17 -07:00
html += renderInspectorHtmlLine ( 'Tx Hash' , renderInspectorCopyRow ( explorerTransactionLink ( txHash , escapeHtml ( txHash ) , 'color: inherit; text-decoration: none;' ) , txHash , 'Copy tx hash' ) ) ;
2026-04-07 23:22:12 -07:00
}
2026-04-10 12:52:17 -07:00
html += renderStructuredLogFields ( knownEvent ) ;
2026-04-07 23:22:12 -07:00
html += renderInspectorHtmlLine ( 'Topics' , topicRows ) ;
html += renderInspectorCodeLine ( 'Data' , dataValue , dataValue ) ;
2026-04-10 12:52:17 -07:00
html += renderInspectorHtmlLine ( 'Decoded' , renderInspectorCopyRow ( '<div id="txLogDecoded' + idx + '" class="tx-inspector-mono">' + ( decodedValue ? escapeHtml ( decodedValue ) : '—' ) + '</div>' , decodedValue || '' , 'Copy decoded log' ) ) ;
2026-04-07 23:22:12 -07:00
html += '</div></details>' ;
return html ;
}
function renderInputWordTable ( inputHex ) {
if ( ! inputHex || ! /^0x[0-9a-f]+$/i . test ( String ( inputHex ) ) ) return '' ;
var normalized = String ( inputHex ) . slice ( 2 ) ;
if ( normalized . length <= 8 ) return '' ;
var payload = normalized . slice ( 8 ) ;
if ( ! payload ) return '' ;
var words = [ ] ;
for ( var i = 0 ; i < payload . length ; i += 64 ) {
words . push ( payload . slice ( i , i + 64 ) ) ;
}
if ( ! words . length ) return '' ;
var rows = words . map ( function ( word , index ) {
var offsetBytes = 4 + ( index * 32 ) ;
var hexWord = '0x' + word ;
var decimalPreview = '' ;
var parsed = toBigIntSafe ( hexWord ) ;
if ( parsed != null ) {
decimalPreview = summarizeInspectorValue ( parsed . toString ( ) , 18 , 10 ) ;
}
return '<tr><td>' + escapeHtml ( String ( index ) ) + '</td><td>' + escapeHtml ( String ( offsetBytes ) ) + ' / 0x' + escapeHtml ( offsetBytes . toString ( 16 ) ) + '</td><td class="hash">' + escapeHtml ( hexWord ) + '</td><td>' + ( decimalPreview ? escapeHtml ( decimalPreview ) : '<span class="tx-empty">N/A</span>' ) + '</td></tr>' ;
} ) . join ( '' ) ;
return '<details class="tx-inspector-entry" style="margin-top: 0.75rem;"><summary><span>ABI Word Breakdown</span><span class="tx-inspector-summary-value">' + escapeHtml ( String ( words . length ) ) + ' words</span></summary><div class="tx-inspector-entry-body"><div class="tx-inspector-note">Offsets start after the 4-byte method selector.</div><div style="overflow-x: auto;"><table class="table"><thead><tr><th>Word</th><th>Offset</th><th>Hex</th><th>Dec Preview</th></tr></thead><tbody>' + rows + '</tbody></table></div></div></details>' ;
}
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' ) ;
2026-04-10 12:52:17 -07:00
updatePath ( '/blocks/' + blockNumber ) ;
2026-02-16 03:09:53 -08:00
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' ) ;
2026-04-10 12:52:17 -07:00
updatePath ( '/transactions/' + txHash ) ;
2026-02-16 03:09:53 -08:00
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-04-10 12:52:17 -07:00
if ( ! t ) {
var diagnostics = rawTx && rawTx . diagnostics ? rawTx . diagnostics : null ;
if ( diagnostics && diagnostics . rpc _transaction _found === false ) {
throw new Error ( 'Transaction not found in Blockscout or the Chain 138 public RPC. It may belong to a different network, have been replaced, or never broadcast successfully' + ( diagnostics . latest _block _number ? ' (latest block #' + diagnostics . latest _block _number + ')' : '' ) ) ;
}
throw new Error ( 'Transaction not found' ) ;
}
2026-02-16 03:09:53 -08:00
} 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' ;
2026-04-07 23:22:12 -07:00
const priorityFeeValue = t . max _priority _fee _per _gas || ( rawTx && rawTx . priority _fee ) || null ;
const priorityFeeGwei = priorityFeeValue ? ( parseInt ( priorityFeeValue , 10 ) / 1e9 ) . toFixed ( 2 ) : 'N/A' ;
2026-02-16 03:09:53 -08:00
const burntFeeEth = t . tx _burnt _fee ? formatEther ( t . tx _burnt _fee ) : '0' ;
2026-04-07 23:22:12 -07:00
const totalFeeWei = rawTx && rawTx . fee && rawTx . fee . value
? String ( rawTx . fee . value )
: ( t . gas _used && t . gas _price ? ( BigInt ( t . gas _used ) * BigInt ( t . gas _price ) ) . toString ( ) : '0' ) ;
const totalFee = totalFeeWei ? formatEther ( totalFeeWei ) : '0' ;
2026-02-16 03:09:53 -08:00
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-04-07 23:22:12 -07:00
const methodSelector = t . method _id || ( rawTx && rawTx . method ) || ( inputHex ? inputHex . slice ( 0 , 10 ) : null ) ;
const confirmationDuration = rawTx ? formatDurationValue ( rawTx . confirmation _duration ) : '' ;
const transactionTypeTags = rawTx && Array . isArray ( rawTx . transaction _types ) ? rawTx . transaction _types : [ ] ;
const actionsCount = rawTx && Array . isArray ( rawTx . actions ) ? rawTx . actions . length : 0 ;
const tokenTransferCount = rawTx && Array . isArray ( rawTx . token _transfers ) ? rawTx . token _transfers . length : 0 ;
const authorizationCount = rawTx && Array . isArray ( rawTx . authorization _list ) ? rawTx . authorization _list . length : 0 ;
const inputBytes = inputHex ? Math . max ( 0 , ( inputHex . length - 2 ) / 2 ) : 0 ;
const rawSource = rawTx && rawTx . source ? rawTx . source : ( CHAIN _ID === 138 ? 'blockscout' : 'api' ) ;
const rawSourceLabel = rawSource === 'rpc_fallback' ? 'RPC fallback' : 'Blockscout detail' ;
const rawPayloadId = 'txRawPayload_' + txHash . slice ( 2 , 10 ) ;
const statusText = t . status === 1 ? 'Success' : t . status === 0 ? 'Failed' : 'Pending' ;
const statusClass = t . status === 1 ? 'badge-success' : t . status === 0 ? 'badge-danger' : 'badge-warning' ;
const baseFeeGwei = rawTx && rawTx . base _fee _per _gas ? formatEther ( rawTx . base _fee _per _gas , 'gwei' ) : '' ;
const rawPayload = safeJsonStringify ( {
normalized : t ,
raw : rawTx
} ) ;
const numericInspectorFields = [
{ label : 'Block Number' , value : t . block _number , note : 'Canonical included block height.' } ,
{ label : 'Transaction Index' , value : t . transaction _index != null ? t . transaction _index : ( rawTx && rawTx . position != null ? rawTx . position : null ) , note : 'Zero-based position within the block.' } ,
{ label : 'Nonce' , value : t . nonce , note : 'Account nonce used for this transaction.' } ,
{ label : 'Confirmations' , value : t . confirmations , note : 'Current confirmation count from the explorer index.' } ,
{ label : 'Value (Wei)' , value : t . value , note : 'Native value transferred, expressed in wei.' } ,
{ label : 'Gas Limit' , value : t . gas _limit , note : 'Maximum gas supplied for execution.' } ,
{ label : 'Gas Used' , value : t . gas _used , note : 'Actual gas consumed during execution.' } ,
{ label : 'Gas Price (Wei)' , value : t . gas _price , note : 'Effective gas price used for fee settlement.' } ,
{ label : 'Max Fee Per Gas (Wei)' , value : t . max _fee _per _gas , note : 'EIP-1559 max fee cap.' } ,
{ label : 'Max Priority Fee (Wei)' , value : priorityFeeValue , note : 'Miner / validator tip component.' } ,
{ label : 'Base Fee (Wei)' , value : rawTx && rawTx . base _fee _per _gas ? rawTx . base _fee _per _gas : null , note : 'Base fee returned by the explorer detail payload.' } ,
{ label : 'Total Fee Paid (Wei)' , value : totalFeeWei , note : 'Fee paid for this transaction in wei.' } ,
{ label : 'Burnt Fee (Wei)' , value : t . tx _burnt _fee , note : 'Fee component marked as burnt by the explorer.' } ,
{ label : 'Type' , value : t . type , note : 'Transaction type identifier.' }
] ;
const numericInspectorHtml = numericInspectorFields . filter ( function ( field ) {
return field . value != null && field . value !== '' ;
} ) . map ( function ( field , index ) {
return renderNumericInspectorEntry ( field . label , field . value , field . note , index === 0 ) ;
} ) . join ( '' ) ;
const executionMetaChips = [ ] ;
executionMetaChips . push ( '<span class="tx-chip"><span class="tx-chip-label">Source</span><span>' + escapeHtml ( rawSourceLabel ) + '</span></span>' ) ;
executionMetaChips . push ( '<span class="tx-chip"><span class="tx-chip-label">Status</span><span>' + escapeHtml ( statusText ) + '</span></span>' ) ;
if ( rawTx && rawTx . fee && rawTx . fee . type ) {
executionMetaChips . push ( '<span class="tx-chip"><span class="tx-chip-label">Fee Mode</span><span>' + escapeHtml ( String ( rawTx . fee . type ) ) + '</span></span>' ) ;
}
if ( transactionTypeTags . length ) {
transactionTypeTags . forEach ( function ( tag ) {
executionMetaChips . push ( '<span class="tx-chip"><span class="tx-chip-label">Tx Tag</span><span>' + escapeHtml ( String ( tag ) ) + '</span></span>' ) ;
} ) ;
}
const executionMetaHtml = executionMetaChips . length ? '<div class="tx-chip-row" style="margin-bottom: 0.85rem;">' + executionMetaChips . join ( '' ) + '</div>' : '' ;
const fromCellContent = explorerAddressLink ( t . from || '' , formatAddressWithLabel ( t . from || '' ) , 'color: inherit; text-decoration: none;' ) + renderCopyButtonHtml ( t . from || '' , 'Copy from address' ) ;
const toCellContent = t . to ? explorerAddressLink ( t . to , formatAddressWithLabel ( t . to ) , 'color: inherit; text-decoration: none;' ) + renderCopyButtonHtml ( t . to , 'Copy to address' ) : 'N/A' ;
2026-02-16 03:09:53 -08:00
let mainHtml = `
2026-04-07 23:22:12 -07:00
< div class = "card" >
2026-02-16 03:09:53 -08:00
< 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 >
2026-04-07 23:22:12 -07:00
< div class = "info-row" >
2026-02-16 03:09:53 -08:00
< div class = "info-label" > Transaction Hash < / d i v >
2026-04-07 23:22:12 -07:00
< div class = "info-value hash" > $ { escapeHtml ( t . hash ) } $ { renderCopyButtonHtml ( t . hash , 'Copy transaction hash' ) } < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Type < / d i v >
2026-04-07 23:22:12 -07:00
< div class = "info-value" > < span class = "badge badge-chain" > $ { txType } < / s p a n > < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > Status < / d i v >
< div class = "info-value" >
2026-04-07 23:22:12 -07:00
< span class = "badge ${statusClass}" >
$ { statusText }
2026-02-16 03:09:53 -08:00
< / 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 >
2026-04-07 23:22:12 -07:00
< div class = "info-value hash" > $ { escapeHtml ( t . block _hash || 'N/A' ) } $ { t . block _hash ? renderCopyButtonHtml ( t . block _hash , 'Copy block hash' ) : '' } < / d i v >
2026-02-16 03:09:53 -08:00
< / d i v >
< div class = "info-row" >
< div class = "info-label" > From < / d i v >
2026-04-07 23:22:12 -07:00
< div class = "info-value" > $ { fromCellContent } < / 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> ` : '' }
2026-04-07 23:22:12 -07:00
$ { priorityFeeValue ? ` <div class="info-row"><div class="info-label">Max Priority Fee</div><div class="info-value"> ${ priorityFeeGwei } Gwei</div></div> ` : '' }
2026-02-16 03:09:53 -08:00
$ { ! 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-04-07 23:22:12 -07:00
< / d i v >
` ;
if ( CHAIN _ID === 138 ) {
mainHtml += buildBridgeTraceCardHtml ( t . to , [ ] ) ;
}
mainHtml += `
< div class = "card" style = "margin-top: 1rem;" >
< div class = "card-header" >
< h3 style = "margin: 0; font-size: 1.15rem;" > Execution Details < / h 3 >
< span class = "badge badge-chain" > $ { escapeHtml ( rawSourceLabel ) } < / s p a n >
< / d i v >
$ { executionMetaHtml }
$ { renderInspectorTextLine ( 'Method Selector' , methodSelector , methodSelector ) }
$ { renderInspectorTextLine ( 'Confirmations' , t . confirmations != null ? String ( t . confirmations ) : '' , t . confirmations != null ? String ( t . confirmations ) : null ) }
$ { renderInspectorTextLine ( 'Confirmation Duration' , confirmationDuration ) }
$ { renderInspectorTextLine ( 'Transaction Tag' , rawTx && rawTx . transaction _tag ? String ( rawTx . transaction _tag ) : '' ) }
$ { renderInspectorTextLine ( 'Actions Indexed' , rawTx && Array . isArray ( rawTx . actions ) ? String ( actionsCount ) : '' ) }
$ { renderInspectorTextLine ( 'Token Transfers Indexed' , rawTx && Array . isArray ( rawTx . token _transfers ) ? String ( tokenTransferCount ) : '' ) }
$ { renderInspectorTextLine ( 'Authorization Entries' , rawTx && Array . isArray ( rawTx . authorization _list ) ? String ( authorizationCount ) : '' ) }
$ { renderInspectorTextLine ( 'Exchange Rate' , rawTx && rawTx . exchange _rate ? String ( rawTx . exchange _rate ) + ' USD/ETH' : '' ) }
$ { renderInspectorTextLine ( 'Base Fee' , baseFeeGwei ? baseFeeGwei + ' Gwei' : '' ) }
$ { renderInspectorTextLine ( 'Internal Trace State' , rawTx && rawTx . has _error _in _internal _transactions != null ? ( rawTx . has _error _in _internal _transactions ? 'Errors recorded in internal transactions' : 'Clean / no internal errors flagged' ) : '' ) }
< / d i v >
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 >
` ;
}
2026-04-07 23:22:12 -07:00
if ( inputHex || decodedInput || methodSelector ) {
mainHtml += ` <div class="card" style="margin-top: 1rem;"><div class="card-header"><h3 style="margin: 0; font-size: 1.15rem;">Input Data</h3><span class="badge badge-chain"> ${ inputBytes } bytes</span></div> ` ;
mainHtml += '<div class="tx-inspector-stack">' ;
if ( methodSelector ) {
mainHtml += renderInspectorCodeLine ( 'Selector' , methodSelector , methodSelector ) ;
}
2026-02-16 03:09:53 -08:00
if ( decodedInput && ( decodedInput . method || decodedInput . params ) ) {
const method = decodedInput . method || decodedInput . name || 'Unknown' ;
2026-04-07 23:22:12 -07:00
mainHtml += renderInspectorTextLine ( 'Decoded Method' , method , method ) ;
2026-02-16 03:09:53 -08:00
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 ) {
2026-04-07 23:22:12 -07:00
mainHtml += renderInspectorCodeLine ( 'Hex' , inputHex , inputHex ) ;
mainHtml += renderInputWordTable ( inputHex ) ;
2026-02-16 03:09:53 -08:00
}
2026-04-07 23:22:12 -07:00
mainHtml += '</div></div>' ;
}
if ( numericInspectorHtml ) {
mainHtml += `
< div class = "card" style = "margin-top: 1rem;" >
< div class = "card-header" >
< h3 style = "margin: 0; font-size: 1.15rem;" > Numeric Inspector < / h 3 >
< span class = "badge badge-chain" > DEC / HEX / BIN < / s p a n >
< / d i v >
< div class = "tx-inspector-stack" > $ { numericInspectorHtml } < / d i v >
< / d i v >
` ;
2026-02-16 03:09:53 -08:00
}
2026-04-07 23:22:12 -07:00
mainHtml += `
< div class = "card" style = "margin-top: 1rem;" >
< div class = "card-header" >
< h3 style = "margin: 0; font-size: 1.15rem;" > Raw Payload < / h 3 >
< span class = "badge badge-chain" > $ { escapeHtml ( rawSourceLabel ) } < / s p a n >
< / d i v >
< details class = "tx-inspector-entry" >
< summary >
< span > Normalized + raw transaction payload < / s p a n >
< span class = "tx-inspector-summary-value" > $ { escapeHtml ( rawSourceLabel ) } / $ { escapeHtml ( summarizeInspectorValue ( t . hash , 12 , 10 ) ) } < / s p a n >
< / s u m m a r y >
< div class = "tx-inspector-entry-body" >
< div class = "tx-inspector-note" > This includes the normalized transaction fields used by the explorer plus the underlying Blockscout or RPC fallback payload so nothing important stays hidden behind the UI . < / d i v >
< div class = "tx-inspector-scroll" > < pre id = "${rawPayloadId}" class = "tx-inspector-pre" > $ { escapeHtml ( rawPayload ) } < / p r e > < / d i v >
< div > < button type = "button" class = "btn btn-primary" style = "padding: 0.45rem 0.8rem; font-size: 0.82rem;" onclick = "copyToClipboard(document.getElementById('${rawPayloadId}').textContent, 'Copied raw payload');" > Copy raw JSON < / b u t t o n > < / d i v >
< / d i v >
< / d e t a i l s >
< / d i v >
` ;
2026-02-16 03:09:53 -08:00
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 || [ ] ;
2026-04-07 23:22:12 -07:00
const rpcReceiptLogs = rawTx && rawTx . receipt && Array . isArray ( rawTx . receipt . logs )
? rawTx . receipt . logs . map ( normalizeRpcLog ) . filter ( function ( log ) { return log !== null ; } )
: [ ] ;
let logs = logsResp . items || logsResp . log _entries || [ ] ;
if ( ( ! logs || logs . length === 0 ) && rpcReceiptLogs . length ) {
logs = rpcReceiptLogs ;
}
2026-02-16 03:09:53 -08:00
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' ;
2026-04-07 23:22:12 -07:00
const decoded = formatLogDecodedValue ( log . decoded || log . decoded _text || '' ) ;
2026-03-24 18:11:08 -07:00
return matchesExplorerFilter ( [ addr , topicsStr , data , decoded ] . join ( ' ' ) , logsFilter ) ;
} ) : logs ;
if ( filteredLogs . length === 0 ) {
2026-04-07 23:22:12 -07:00
logsEl . innerHTML = logsFilterBar + '<p style="color: var(--text-light);">No event logs match the current filter.</p>' ;
} else {
logsEl . innerHTML = logsFilterBar + '<div class="tx-inspector-stack">' + filteredLogs . map ( function ( log , idx ) {
return renderTransactionLogEntry ( log , idx ) ;
} ) . join ( '' ) + '</div>' ;
if ( typeof ethers !== 'undefined' && ethers . utils ) {
( function ( logsList ) {
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 . filter ( function ( topic ) { return topic != null ; } ) : ( 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 || '' ;
2026-04-10 12:52:17 -07:00
var summaryTitleEl = document . getElementById ( 'txLogSummaryTitle' + idx ) ;
if ( summaryTitleEl ) {
summaryTitleEl . textContent = 'Log #' + String ( log . index != null ? log . index : idx ) + ' • ' + parsed . name ;
}
var eventChipEl = document . getElementById ( 'txLogEventChip' + idx ) ;
if ( eventChipEl ) {
eventChipEl . innerHTML = '<span class="tx-chip-label">Event</span><span>' + escapeHtml ( parsed . name ) + '</span>' ;
eventChipEl . title = parsed . signature || '' ;
}
2026-04-07 23:22:12 -07:00
}
} catch ( e ) { }
} ) ;
2026-02-16 03:09:53 -08:00
} ) ;
2026-04-07 23:22:12 -07:00
} ) ( filteredLogs ) ;
}
2026-02-16 03:09:53 -08:00
}
}
}
2026-04-07 23:22:12 -07:00
var bridgeTraceBody = document . getElementById ( 'txBridgeTraceBody' ) ;
if ( bridgeTraceBody && CHAIN _ID === 138 ) {
bridgeTraceBody . innerHTML = buildBridgeTraceInnerHtml ( t . to , logs || [ ] ) ;
}
2026-02-16 03:09:53 -08:00
} ) ;
}
} 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 ) {
2026-04-07 23:22:12 -07:00
Promise . resolve ( CHAIN _ID === 138 ? fetchChain138TransactionDetail ( txHash ) : fetchAPIWithRetry ( BLOCKSCOUT _API + '/v2/transactions/' + txHash ) . then ( function ( r ) {
return { transaction : normalizeTransaction ( r ) , rawTransaction : r } ;
} ) ) . then ( function ( result ) {
var t = result && result . transaction ? result . transaction : null ;
var raw = result && result . rawTransaction ? result . rawTransaction : null ;
2026-02-16 03:09:53 -08:00
if ( ! t ) return ;
2026-04-07 23:22:12 -07:00
var totalFeeWei = raw && raw . fee && raw . fee . value
? String ( raw . fee . value )
: ( t . gas _used && t . gas _price ? ( BigInt ( t . gas _used ) * BigInt ( t . gas _price ) ) . toString ( ) : '0' ) ;
var rows = [
[ 'Field' , 'Value' ] ,
[ 'hash' , t . hash ] ,
[ 'status' , t . status ] ,
[ 'type' , t . type != null ? t . type : '' ] ,
[ 'from' , t . from ] ,
[ 'to' , t . to || '' ] ,
[ 'value_wei' , t . value || '0' ] ,
[ 'block_number' , t . block _number || '' ] ,
[ 'transaction_index' , t . transaction _index != null ? t . transaction _index : '' ] ,
[ 'nonce' , t . nonce || '' ] ,
[ 'gas_price_wei' , t . gas _price || '' ] ,
[ 'gas_limit' , t . gas _limit || '' ] ,
[ 'gas_used' , t . gas _used || '' ] ,
[ 'confirmations' , t . confirmations != null ? t . confirmations : '' ] ,
[ 'method_selector' , t . method _id || ( raw && raw . method ) || '' ] ,
[ 'total_fee_wei' , totalFeeWei ]
] ;
2026-02-16 03:09:53 -08:00
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 ;
2026-04-07 23:22:12 -07:00
var displayBalance = formatUnitsLocalized ( balance , decimals , 6 ) ;
2026-02-16 03:09:53 -08:00
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' ) ;
2026-04-10 12:52:17 -07:00
updatePath ( '/addresses/' + address ) ;
2026-02-16 03:09:53 -08:00
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 ) ) {
2026-05-10 12:56:30 -07:00
clearExplorerPendingAddressInitialTabFor ( address ) ;
2026-02-16 03:09:53 -08:00
container . innerHTML = '<div class="error">Invalid address format</div>' ;
return ;
}
let a ;
2026-04-07 23:22:12 -07:00
let addressDetailSource = 'blockscout' ;
2026-02-16 03:09:53 -08:00
// For ChainID 138, use Blockscout API directly
if ( CHAIN _ID === 138 ) {
try {
2026-04-07 23:22:12 -07:00
const detailResult = await fetchChain138AddressDetail ( address ) ;
a = detailResult . address ;
addressDetailSource = detailResult . source || 'blockscout' ;
2026-02-16 03:09:53 -08:00
if ( ! a || ! a . hash ) {
throw new Error ( 'Address not found' ) ;
}
} catch ( error ) {
2026-05-10 12:56:30 -07:00
clearExplorerPendingAddressInitialTabFor ( address ) ;
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 ;
2026-05-10 12:56:30 -07:00
if ( window . _ _explorerPendingAddressInitialTab && window . _ _explorerPendingAddressInitialTab . tab === 'contract' && window . _ _explorerPendingAddressInitialTab . address === address . toLowerCase ( ) && ! isContract ) {
window . _ _explorerPendingAddressInitialTab = null ;
}
2026-02-16 03:09:53 -08:00
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 , "\\'" ) ;
2026-05-10 12:56:30 -07:00
const contractLink = isContract ? institutionAddressContractLinkHtml ( address ) : '' ;
2026-02-16 03:09:53 -08:00
const savedLabel = getAddressLabel ( address ) ;
const inWatchlist = isInWatchlist ( address ) ;
2026-04-07 23:22:12 -07:00
const fallbackNotice = addressDetailSource === 'rpc_fallback'
? ` <div class="card" style="margin-bottom: 1rem; border-left: 4px solid var(--warning);"><strong>RPC fallback mode.</strong> Indexed address metadata is temporarily unavailable or responding too slowly, so this view is using live RPC for balance, nonce-derived transaction count, and contract detection. Transactions, token balances, internal txns, and NFTs remain best-effort while the explorer API recovers.</div> `
: '' ;
2026-05-10 12:56:30 -07:00
const institutionOpStrip = ( isContract && authToken && hasAccess ( 4 ) )
? ` <div class="card" style="margin-bottom: 1rem; border-left: 4px solid rgba(59, 130, 246, 0.85);"><strong>Institution operator tools</strong> · <button type="button" class="btn btn-primary" onclick="showContractVerificationHelp(' ${ addressForJs } ')">Verify & publish wizard</button> ${ contractLink ? ' · ' + contractLink : '' } </div> `
: '' ;
2026-02-16 03:09:53 -08:00
container . innerHTML = `
2026-05-10 12:56:30 -07:00
$ { fallbackNotice } $ { institutionOpStrip }
2026-02-16 03:09:53 -08:00
< 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 ;
2026-05-10 12:56:30 -07:00
var pend = window . _ _explorerPendingAddressInitialTab ;
if ( pend && pend . tab === 'contract' && pend . address === address . toLowerCase ( ) && isContract ) {
window . _ _explorerPendingAddressInitialTab = null ;
switchAddressTab ( 'contract' , address ) ;
}
2026-02-16 03:09:53 -08:00
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 ;
2026-04-07 23:22:12 -07:00
const displayBalance = formatUnitsLocalized ( balance , decimals , 6 ) ;
2026-03-24 18:11:08 -07:00
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 ;
2026-04-07 23:22:12 -07:00
const displayBalance = formatUnitsLocalized ( balance , decimals , 6 ) ;
2026-02-16 03:09:53 -08:00
const type = token . type || b . token _type || 'ERC-20' ;
2026-04-10 12:52:17 -07:00
tbl += '<tr><td><a href="/tokens/' + 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-05-10 12:56:30 -07:00
el . innerHTML = '<p style="color: var(--text-light);">Contract source not indexed. ' + institutionContractExploreAnchor ( addr , 'Verify on Blockscout' , 'Open contract tab' , 'color: var(--primary);' ) + '</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-05-10 12:56:30 -07:00
html += '<p style="margin-top: 0.5rem;">' + institutionContractExploreAnchor ( addr , 'Read / Write contract on Blockscout' , 'Read / Write contract in explorer' , 'color: var(--primary);' ) + '</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 {
2026-05-10 12:56:30 -07:00
clearExplorerPendingAddressInitialTabFor ( address ) ;
2026-02-16 03:09:53 -08:00
container . innerHTML = '<div class="error">Address not found</div>' ;
}
} catch ( error ) {
2026-05-10 12:56:30 -07:00
clearExplorerPendingAddressInitialTabFor ( address ) ;
2026-02-16 03:09:53 -08:00
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' ) ;
2026-04-10 12:52:17 -07:00
updatePath ( '/tokens/' + tokenAddress ) ;
2026-02-16 03:09:53 -08:00
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 ) {
2026-04-10 12:52:17 -07:00
container . innerHTML = '<p class="error">Token not found or not indexed.</p><p><a href="/addresses/' + encodeURIComponent ( tokenAddress ) + '">View as address</a></p>' ;
2026-02-16 03:09:53 -08:00
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-04-07 23:22:12 -07:00
var supplyDisplay = formatUnitsLocalized ( supply , decimals , 6 ) ;
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>' ;
2026-04-07 23:22:12 -07:00
html += '<div class="info-row"><div class="info-label">Total Supply</div><div class="info-value">' + escapeHtml ( supplyDisplay ) + '</div></div>' ;
2026-02-16 03:09:53 -08:00
html += '<div class="info-row"><div class="info-label">Holders</div><div class="info-value">' + ( holders !== '-' ? formatNumber ( holders ) : '-' ) + '</div></div>' ;
2026-04-07 23:22:12 -07:00
if ( CHAIN _ID === 138 ) {
try {
var pRes = await fetch ( TOKEN _AGGREGATION _API _BASE + '/v1/tokens/' + encodeURIComponent ( tokenAddress ) + '/pools?chainId=138' ) ;
if ( pRes . ok ) {
var pJ = await pRes . json ( ) ;
var plist = ( pJ && pJ . pools ) ? pJ . pools : [ ] ;
html += '<div class="card" style="margin-top: 1rem;"><h3>Pools (token-aggregation)</h3>' ;
html += '<p style="color:var(--text-light); font-size:0.9rem; margin-bottom:0.75rem;">Indexed PMM/DEX pools for this token on Chain 138. Open Routes for path quotes or Liquidity for access tooling.</p>' ;
html += '<div style="display:flex; flex-wrap:wrap; gap:0.5rem; margin-bottom:0.75rem;">' ;
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\');">Routes</button>' ;
html += '<button type="button" class="btn btn-secondary" onclick="showLiquidityAccess(); updatePath(\'/liquidity\');">Liquidity</button>' ;
html += '</div>' ;
if ( plist . length === 0 ) {
html += '<p style="color:var(--text-light);">No indexed pools for this token.</p>' ;
} else {
html += '<table class="table"><thead><tr><th>Pool</th><th>DEX</th><th>Pair</th><th>TVL (USD)</th></tr></thead><tbody>' ;
plist . forEach ( function ( pl ) {
var pAddr = pl . address || '' ;
var dex = pl . dex || '' ;
var t0s = pl . token0 && pl . token0 . symbol ? pl . token0 . symbol : shortenHash ( pl . token0 && pl . token0 . address || '' ) ;
var t1s = pl . token1 && pl . token1 . symbol ? pl . token1 . symbol : shortenHash ( pl . token1 && pl . token1 . address || '' ) ;
var tvl = pl . tvl != null ? Number ( pl . tvl ) . toLocaleString ( undefined , { maximumFractionDigits : 2 } ) : '—' ;
html += '<tr><td>' + explorerAddressLink ( pAddr , escapeHtml ( shortenHash ( pAddr ) ) , 'color: inherit; text-decoration: none;' ) + '</td><td>' + escapeHtml ( String ( dex ) ) + '</td><td>' + escapeHtml ( t0s + ' / ' + t1s ) + '</td><td>' + escapeHtml ( tvl ) + '</td></tr>' ;
} ) ;
html += '</tbody></table>' ;
}
html += '</div>' ;
}
} catch ( poolsErr ) { }
}
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 ;
2026-04-07 23:22:12 -07:00
var displayValue = formatUnitsLocalized ( val , dec , 6 ) ;
2026-03-24 18:11:08 -07:00
var txHash = tr . transaction _hash || tr . tx _hash || '' ;
2026-04-07 23:22:12 -07:00
return matchesExplorerFilter ( [ from , to , displayValue , txHash ] . join ( ' ' ) , transfersFilter ) ;
2026-03-24 18:11:08 -07:00
} ) : 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 ;
2026-04-07 23:22:12 -07:00
var displayValue = formatUnitsLocalized ( val , dec , 6 ) ;
2026-02-16 03:09:53 -08:00
var txHash = tr . transaction _hash || tr . tx _hash || '' ;
2026-04-07 23:22:12 -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 ( displayValue ) + '</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 ) ;
}
2026-04-07 23:22:12 -07:00
function toBigIntValue ( value ) {
if ( typeof value === 'bigint' ) return value ;
if ( typeof value === 'number' ) {
if ( ! isFinite ( value ) ) return 0 n ;
return BigInt ( Math . trunc ( value ) ) ;
2026-02-16 03:09:53 -08:00
}
2026-04-07 23:22:12 -07:00
var stringValue = String ( value == null ? '0' : value ) . trim ( ) ;
if ( ! stringValue ) return 0 n ;
try {
if ( /^0x/i . test ( stringValue ) || /^-?\d+$/ . test ( stringValue ) ) {
return BigInt ( stringValue ) ;
}
} catch ( e ) { }
var parsed = Number ( stringValue ) ;
if ( ! isFinite ( parsed ) ) return 0 n ;
return BigInt ( Math . trunc ( parsed ) ) ;
}
function formatUnits ( value , decimals , fractionDigits ) {
try {
var bigValue = toBigIntValue ( value ) ;
var precision = Math . max ( 0 , Math . min ( decimals , fractionDigits == null ? 6 : fractionDigits ) ) ;
var divisor = 10 n * * BigInt ( decimals ) ;
var whole = bigValue / divisor ;
var fraction = bigValue % divisor ;
if ( fraction < 0 ) fraction = - fraction ;
if ( precision === 0 ) return whole . toString ( ) ;
var scale = 10 n * * BigInt ( decimals - precision ) ;
var truncatedFraction = ( fraction / scale ) . toString ( ) . padStart ( precision , '0' ) . replace ( /0+$/ , '' ) ;
if ( ! truncatedFraction ) return whole . toString ( ) ;
return whole . toString ( ) + '.' + truncatedFraction ;
} catch ( e ) {
return '0' ;
}
}
function formatDecimalStringWithGrouping ( value ) {
var stringValue = String ( value == null ? '0' : value ) . trim ( ) ;
if ( ! stringValue ) return '0' ;
var negative = stringValue . charAt ( 0 ) === '-' ;
if ( negative ) stringValue = stringValue . slice ( 1 ) ;
var parts = stringValue . split ( '.' ) ;
var whole = ( parts [ 0 ] || '0' ) . replace ( /\B(?=(\d{3})+(?!\d))/g , ',' ) ;
var fraction = parts . length > 1 && parts [ 1 ] ? '.' + parts [ 1 ] : '' ;
return ( negative ? '-' : '' ) + whole + fraction ;
}
function formatUnitsLocalized ( value , decimals , fractionDigits ) {
return formatDecimalStringWithGrouping ( formatUnits ( value , decimals , fractionDigits ) ) ;
}
function parseDecimalToUnits ( value , decimals ) {
var stringValue = String ( value == null ? '' : value ) . trim ( ) ;
if ( ! stringValue || ! /^\d+(\.\d+)?$/ . test ( stringValue ) ) return null ;
var parts = stringValue . split ( '.' ) ;
var whole = BigInt ( parts [ 0 ] || '0' ) ;
var fraction = parts [ 1 ] || '' ;
if ( fraction . length > decimals ) return null ;
var paddedFraction = decimals > 0 ? fraction . padEnd ( decimals , '0' ) : '' ;
return whole * ( 10 n * * BigInt ( decimals ) ) + BigInt ( paddedFraction || '0' ) ;
}
function formatEther ( wei , unit = 'ether' ) {
return formatUnits ( wei , unit === 'gwei' ? 9 : 18 , 6 ) ;
2026-02-16 03:09:53 -08:00
}
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;">' +
2026-04-07 23:22:12 -07:00
'<div style="font-size:0.78rem; color:var(--text-light);">Read-only: indexed explorer + route APIs + docs. No private keys. Server-side operator or PMM/MCP tools stay disabled unless EXPLORER_AI_OPERATOR_TOOLS_ENABLED=1 on the backend.</div>' +
2026-03-27 13:37:53 -07:00
'<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 ) {
2026-04-07 23:22:12 -07:00
Promise . resolve ( CHAIN _ID === 138 ? fetchChain138TransactionDetail ( txHash ) : fetchAPIWithRetry ( ` ${ BLOCKSCOUT _API } /v2/transactions/ ${ txHash } ` ) . then ( function ( response ) {
return { transaction : normalizeTransaction ( response ) , rawTransaction : response } ;
} ) )
. then ( result => {
const payload = {
normalized : result && result . transaction ? result . transaction : null ,
raw : result && result . rawTransaction ? result . rawTransaction : null
} ;
const dataStr = safeJsonStringify ( payload ) ;
2026-02-16 03:09:53 -08:00
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 ;
}
2026-05-10 12:56:30 -07:00
var kbdModal = document . getElementById ( 'keyboardShortcutsModal' ) ;
if ( e . key === 'Escape' && kbdModal && kbdModal . style . display === 'block' ) {
e . preventDefault ( ) ;
if ( typeof closeKeyboardShortcutsModal === 'function' ) closeKeyboardShortcutsModal ( ) ;
return ;
}
var incModal = document . getElementById ( 'incidentLinksModal' ) ;
if ( e . key === 'Escape' && incModal && incModal . style . display === 'block' ) {
e . preventDefault ( ) ;
if ( typeof closeIncidentLinksModal === 'function' ) closeIncidentLinksModal ( ) ;
return ;
}
var contractHelpModal = document . getElementById ( 'contractVerifyHelpModal' ) ;
if ( e . key === 'Escape' && contractHelpModal && contractHelpModal . style . display === 'block' ) {
e . preventDefault ( ) ;
closeContractVerifyHelpModal ( ) ;
return ;
}
2026-03-27 12:02:36 -07:00
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 ( ) ;
2026-05-10 12:56:30 -07:00
initContractVerifyHelpModal ( ) ;
var kbdBackdrop = document . getElementById ( 'keyboardShortcutsBackdrop' ) ;
var kbdClose = document . getElementById ( 'keyboardShortcutsCloseBtn' ) ;
if ( kbdBackdrop ) kbdBackdrop . addEventListener ( 'click' , function ( ) { if ( typeof closeKeyboardShortcutsModal === 'function' ) closeKeyboardShortcutsModal ( ) ; } ) ;
if ( kbdClose ) kbdClose . addEventListener ( 'click' , function ( ev ) { ev . preventDefault ( ) ; if ( typeof closeKeyboardShortcutsModal === 'function' ) closeKeyboardShortcutsModal ( ) ; } ) ;
var incBackdrop = document . getElementById ( 'incidentLinksBackdrop' ) ;
var incClose = document . getElementById ( 'incidentLinksCloseBtn' ) ;
if ( incBackdrop ) incBackdrop . addEventListener ( 'click' , function ( ) { if ( typeof closeIncidentLinksModal === 'function' ) closeIncidentLinksModal ( ) ; } ) ;
if ( incClose ) incClose . addEventListener ( 'click' , function ( ev ) { ev . preventDefault ( ) ; if ( typeof closeIncidentLinksModal === 'function' ) closeIncidentLinksModal ( ) ; } ) ;
}
if ( typeof updateNavAriaCurrentFromPath === 'function' ) {
updateNavAriaCurrentFromPath ( window . location . pathname ) ;
}
if ( typeof restartWatchlistInstitutionNotifications === 'function' ) {
restartWatchlistInstitutionNotifications ( ) ;
}
if ( typeof applyInstitutionPrefsToDom === 'function' ) {
applyInstitutionPrefsToDom ( ) ;
2026-02-16 03:09:53 -08:00
}
2026-04-07 23:22:12 -07:00
ensureMissionControlHealthStrip ( ) ;
startMissionControlEventSource ( ) ;
refreshMissionControlHealthStrip ( ) ;
setInterval ( refreshMissionControlHealthStrip , 120000 ) ;
2026-02-16 03:09:53 -08:00
} ) ;