diff --git a/backend/api/gateway/gateway.go b/backend/api/gateway/gateway.go
index eb5ba25..a8fa25c 100644
--- a/backend/api/gateway/gateway.go
+++ b/backend/api/gateway/gateway.go
@@ -6,6 +6,8 @@ import (
"net/http"
"net/http/httputil"
"net/url"
+
+ httperrors "github.com/explorer/backend/libs/go-http-errors"
)
// Gateway represents the API gateway
@@ -51,13 +53,13 @@ func (g *Gateway) handleRequest(proxy *httputil.ReverseProxy) http.HandlerFunc {
// Authentication
if !g.auth.Authenticate(r) {
- http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ httperrors.WriteJSON(w, http.StatusUnauthorized, "UNAUTHORIZED", "Unauthorized")
return
}
// Rate limiting
if !g.rateLimiter.Allow(r) {
- http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
+ httperrors.WriteJSON(w, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "Rate limit exceeded")
return
}
diff --git a/backend/api/middleware/security.go b/backend/api/middleware/security.go
index 0e57b7b..921bf36 100644
--- a/backend/api/middleware/security.go
+++ b/backend/api/middleware/security.go
@@ -3,6 +3,8 @@ package middleware
import (
"net/http"
"strings"
+
+ httperrors "github.com/explorer/backend/libs/go-http-errors"
)
// SecurityMiddleware adds security headers
@@ -52,7 +54,7 @@ func (m *SecurityMiddleware) BlockWriteCalls(next http.Handler) http.Handler {
if !strings.Contains(path, "weth") && !strings.Contains(path, "wrap") && !strings.Contains(path, "unwrap") {
// Block other write operations for Track 1
if strings.HasPrefix(path, "/api/v1/track1") {
- http.Error(w, "Write operations not allowed for Track 1 (public)", http.StatusForbidden)
+ httperrors.WriteJSON(w, http.StatusForbidden, "FORBIDDEN", "Write operations not allowed for Track 1 (public)")
return
}
}
diff --git a/backend/api/rest/addresses.go b/backend/api/rest/addresses.go
index 0b918c0..c03d589 100644
--- a/backend/api/rest/addresses.go
+++ b/backend/api/rest/addresses.go
@@ -12,20 +12,20 @@ import (
// handleGetAddress handles GET /api/v1/addresses/{chain_id}/{address}
func (s *Server) handleGetAddress(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
return
}
// Parse address from URL
address := r.URL.Query().Get("address")
if address == "" {
- http.Error(w, "Address required", http.StatusBadRequest)
+ writeValidationError(w, fmt.Errorf("address required"))
return
}
// Validate address format
if !isValidAddress(address) {
- http.Error(w, "Invalid address format", http.StatusBadRequest)
+ writeValidationError(w, ErrInvalidAddress)
return
}
@@ -40,7 +40,7 @@ func (s *Server) handleGetAddress(w http.ResponseWriter, r *http.Request) {
s.chainID, address,
).Scan(&txCount)
if err != nil {
- http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
+ writeInternalError(w, "Database error")
return
}
diff --git a/backend/api/rest/config.go b/backend/api/rest/config.go
index 57fab5b..220151c 100644
--- a/backend/api/rest/config.go
+++ b/backend/api/rest/config.go
@@ -15,7 +15,7 @@ var dualChainTokenListJSON []byte
func (s *Server) handleConfigNetworks(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -27,7 +27,7 @@ func (s *Server) handleConfigNetworks(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleConfigTokenList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
return
}
w.Header().Set("Content-Type", "application/json")
diff --git a/backend/api/rest/errors.go b/backend/api/rest/errors.go
index d7cc7f5..2fd541a 100644
--- a/backend/api/rest/errors.go
+++ b/backend/api/rest/errors.go
@@ -49,3 +49,8 @@ func writeForbidden(w http.ResponseWriter) {
writeError(w, http.StatusForbidden, "FORBIDDEN", "Access denied")
}
+// writeMethodNotAllowed writes a 405 error response (JSON)
+func writeMethodNotAllowed(w http.ResponseWriter) {
+ writeError(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
+}
+
diff --git a/backend/api/rest/etherscan.go b/backend/api/rest/etherscan.go
index 28f5b92..021a9f4 100644
--- a/backend/api/rest/etherscan.go
+++ b/backend/api/rest/etherscan.go
@@ -13,7 +13,10 @@ import (
// This provides Etherscan-compatible API endpoints
func (s *Server) handleEtherscanAPI(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
+ return
+ }
+ if !s.requireDB(w) {
return
}
diff --git a/backend/api/rest/search.go b/backend/api/rest/search.go
index bd63601..53b3e30 100644
--- a/backend/api/rest/search.go
+++ b/backend/api/rest/search.go
@@ -8,7 +8,10 @@ import (
// handleSearch handles GET /api/v1/search
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
+ return
+ }
+ if !s.requireDB(w) {
return
}
diff --git a/backend/api/rest/server.go b/backend/api/rest/server.go
index b0c4027..351b39a 100644
--- a/backend/api/rest/server.go
+++ b/backend/api/rest/server.go
@@ -13,6 +13,7 @@ import (
"github.com/explorer/backend/auth"
"github.com/explorer/backend/api/middleware"
+ httpmiddleware "github.com/explorer/backend/libs/go-http-middleware"
"github.com/jackc/pgx/v5/pgxpool"
)
@@ -54,8 +55,12 @@ func (s *Server) Start(port int) error {
// Setup track routes with proper middleware
s.SetupTrackRoutes(mux, authMiddleware)
- // Initialize security middleware
- securityMiddleware := middleware.NewSecurityMiddleware()
+ // Security headers (reusable lib; CSP from env or explorer default)
+ csp := os.Getenv("CSP_HEADER")
+ if csp == "" {
+ csp = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; font-src 'self' https://cdnjs.cloudflare.com; img-src 'self' data: https:; connect-src 'self' https://explorer.d-bis.org https://rpc-http-pub.d-bis.org wss://rpc-ws-pub.d-bis.org http://192.168.11.221:8545 ws://192.168.11.221:8546;"
+ }
+ securityMiddleware := httpmiddleware.NewSecurity(csp)
// Add middleware for all routes (outermost to innermost)
handler := securityMiddleware.AddSecurityHeaders(
@@ -82,9 +87,13 @@ func (s *Server) addMiddleware(next http.Handler) http.Handler {
w.Header().Set("X-Explorer-Version", "1.0.0")
w.Header().Set("X-Powered-By", "SolaceScanScout")
- // Add CORS headers for API routes
+ // Add CORS headers for API routes (optional: set CORS_ALLOWED_ORIGIN to restrict, e.g. https://explorer.d-bis.org)
if strings.HasPrefix(r.URL.Path, "/api/") {
- w.Header().Set("Access-Control-Allow-Origin", "*")
+ origin := os.Getenv("CORS_ALLOWED_ORIGIN")
+ if origin == "" {
+ origin = "*"
+ }
+ w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-API-Key")
@@ -99,10 +108,22 @@ func (s *Server) addMiddleware(next http.Handler) http.Handler {
})
}
+// requireDB returns false and writes 503 if db is nil (e.g. in tests without DB)
+func (s *Server) requireDB(w http.ResponseWriter) bool {
+ if s.db == nil {
+ writeError(w, http.StatusServiceUnavailable, "service_unavailable", "database unavailable")
+ return false
+ }
+ return true
+}
+
// handleListBlocks handles GET /api/v1/blocks
func (s *Server) handleListBlocks(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
+ return
+ }
+ if !s.requireDB(w) {
return
}
@@ -132,7 +153,7 @@ func (s *Server) handleListBlocks(w http.ResponseWriter, r *http.Request) {
rows, err := s.db.Query(ctx, query, s.chainID, pageSize, offset)
if err != nil {
- http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
+ writeInternalError(w, "Database error")
return
}
defer rows.Close()
@@ -191,12 +212,15 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Explorer-Version", "1.0.0")
// Check database connection
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
- defer cancel()
-
dbStatus := "ok"
- if err := s.db.Ping(ctx); err != nil {
- dbStatus = "error: " + err.Error()
+ if s.db != nil {
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+ if err := s.db.Ping(ctx); err != nil {
+ dbStatus = "error: " + err.Error()
+ }
+ } else {
+ dbStatus = "unavailable"
}
health := map[string]interface{}{
diff --git a/backend/api/rest/stats.go b/backend/api/rest/stats.go
index ab7bf32..8d12af0 100644
--- a/backend/api/rest/stats.go
+++ b/backend/api/rest/stats.go
@@ -10,7 +10,10 @@ import (
// handleStats handles GET /api/v2/stats
func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
+ return
+ }
+ if !s.requireDB(w) {
return
}
diff --git a/backend/api/rest/transactions.go b/backend/api/rest/transactions.go
index 762a484..6857426 100644
--- a/backend/api/rest/transactions.go
+++ b/backend/api/rest/transactions.go
@@ -13,7 +13,10 @@ import (
// handleListTransactions handles GET /api/v1/transactions
func (s *Server) handleListTransactions(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ writeMethodNotAllowed(w)
+ return
+ }
+ if !s.requireDB(w) {
return
}
@@ -70,7 +73,7 @@ func (s *Server) handleListTransactions(w http.ResponseWriter, r *http.Request)
rows, err := s.db.Query(ctx, query, args...)
if err != nil {
- http.Error(w, fmt.Sprintf("Database error: %v", err), http.StatusInternalServerError)
+ writeInternalError(w, "Database error")
return
}
defer rows.Close()
@@ -178,7 +181,7 @@ func (s *Server) handleGetTransactionByHash(w http.ResponseWriter, r *http.Reque
)
if err != nil {
- http.Error(w, fmt.Sprintf("Transaction not found: %v", err), http.StatusNotFound)
+ writeNotFound(w, "Transaction")
return
}
diff --git a/backend/api/search/search.go b/backend/api/search/search.go
index 6223700..a69ac69 100644
--- a/backend/api/search/search.go
+++ b/backend/api/search/search.go
@@ -10,6 +10,7 @@ import (
"github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esapi"
+ httperrors "github.com/explorer/backend/libs/go-http-errors"
)
// SearchService handles unified search
@@ -131,13 +132,13 @@ func (s *SearchService) parseResult(source map[string]interface{}) SearchResult
// HandleSearch handles HTTP search requests
func (s *SearchService) HandleSearch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
- http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ httperrors.WriteJSON(w, http.StatusMethodNotAllowed, "METHOD_NOT_ALLOWED", "Method not allowed")
return
}
query := r.URL.Query().Get("q")
if query == "" {
- http.Error(w, "Query parameter 'q' is required", http.StatusBadRequest)
+ httperrors.WriteJSON(w, http.StatusBadRequest, "BAD_REQUEST", "Query parameter 'q' is required")
return
}
@@ -157,7 +158,7 @@ func (s *SearchService) HandleSearch(w http.ResponseWriter, r *http.Request) {
results, err := s.Search(r.Context(), query, chainID, limit)
if err != nil {
- http.Error(w, fmt.Sprintf("Search failed: %v", err), http.StatusInternalServerError)
+ httperrors.WriteJSON(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Search failed")
return
}
diff --git a/backend/libs/go-http-errors/errors.go b/backend/libs/go-http-errors/errors.go
new file mode 100644
index 0000000..ee43031
--- /dev/null
+++ b/backend/libs/go-http-errors/errors.go
@@ -0,0 +1,26 @@
+package httperrors
+
+import (
+ "encoding/json"
+ "net/http"
+)
+
+// ErrorResponse is the standard JSON error body for API responses.
+type ErrorResponse struct {
+ Error struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ } `json:"error"`
+}
+
+// WriteJSON writes a JSON error response with the given status code and message.
+func WriteJSON(w http.ResponseWriter, statusCode int, code, message string) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(statusCode)
+ json.NewEncoder(w).Encode(ErrorResponse{
+ Error: struct {
+ Code string `json:"code"`
+ Message string `json:"message"`
+ }{Code: code, Message: message},
+ })
+}
diff --git a/frontend/public/explorer-spa.js b/frontend/public/explorer-spa.js
new file mode 100644
index 0000000..bb7f8ca
--- /dev/null
+++ b/frontend/public/explorer-spa.js
@@ -0,0 +1,3532 @@
+ const API_BASE = '/api';
+ const FETCH_TIMEOUT_MS = 15000;
+ const RPC_HEALTH_TIMEOUT_MS = 5000;
+ const FETCH_MAX_RETRIES = 3;
+ const RETRY_DELAY_MS = 1000;
+ window.DEBUG_EXPLORER = false;
+ (function() {
+ var _log = console.log, _warn = console.warn;
+ console.log = function() { if (window.DEBUG_EXPLORER) _log.apply(console, arguments); };
+ console.warn = function() { if (window.DEBUG_EXPLORER) _warn.apply(console, arguments); };
+ })();
+ // RPC/WebSocket: VMID 2201 (public RPC). FQDN when HTTPS (avoids mixed content); IP when HTTP (e.g. http://192.168.11.140)
+ const RPC_IP = 'http://192.168.11.221:8545'; // Chain 138 - VMID 2201 besu-rpc-public-1
+ const RPC_WS_IP = 'ws://192.168.11.221:8546';
+ const RPC_FQDN = 'https://rpc-http-pub.d-bis.org'; // VMID 2201 - HTTPS
+ const RPC_WS_FQDN = 'wss://rpc-ws-pub.d-bis.org';
+ const RPC_URLS = (typeof window !== 'undefined' && window.location && window.location.protocol === 'https:')
+ ? [RPC_FQDN] : [RPC_IP];
+ const RPC_URL = (typeof window !== 'undefined' && window.location && window.location.protocol === 'https:') ? RPC_FQDN : RPC_IP;
+ const RPC_WS_URL = (typeof window !== 'undefined' && window.location && window.location.protocol === 'https:') ? RPC_WS_FQDN : RPC_WS_IP;
+ let _rpcUrlIndex = 0;
+ let _blocksScrollAnimationId = null;
+ async function getRpcUrl() {
+ if (RPC_URLS.length <= 1) return RPC_URLS[0];
+ const ac = new AbortController();
+ const t = setTimeout(() => ac.abort(), RPC_HEALTH_TIMEOUT_MS);
+ for (let i = 0; i < RPC_URLS.length; i++) {
+ const url = RPC_URLS[(_rpcUrlIndex + i) % RPC_URLS.length];
+ try {
+ const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_blockNumber', params: [], id: 1 }), signal: ac.signal });
+ clearTimeout(t);
+ if (r.ok) { _rpcUrlIndex = (_rpcUrlIndex + i) % RPC_URLS.length; return url; }
+ } catch (e) {}
+ }
+ clearTimeout(t);
+ return RPC_URLS[_rpcUrlIndex % RPC_URLS.length];
+ }
+ const CHAIN_ID = 138; // Hyperledger Besu ChainID 138
+ async function rpcCall(method, params) {
+ const url = await getRpcUrl();
+ const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method, params: params || [], id: 1 }) });
+ const j = await r.json();
+ if (j.error) throw new Error(j.error.message || 'RPC error');
+ return j.result;
+ }
+ const BLOCKSCOUT_API_ORIGIN = 'https://explorer.d-bis.org/api'; // fallback when not on explorer host
+ // Origins that serve the explorer (FQDN or VM IP): use explicit same-origin API URL so nginx proxy is used
+ const EXPLORER_ORIGINS = ['https://explorer.d-bis.org', 'http://explorer.d-bis.org', 'http://192.168.11.140', 'https://192.168.11.140'];
+ const BLOCKSCOUT_API = (typeof window !== 'undefined' && window.location && EXPLORER_ORIGINS.includes(window.location.origin)) ? (window.location.origin + '/api') : BLOCKSCOUT_API_ORIGIN;
+ const EXPLORER_ORIGIN = (typeof window !== 'undefined' && window.location && EXPLORER_ORIGINS.includes(window.location.origin)) ? window.location.origin : 'https://explorer.d-bis.org';
+ var I18N = {
+ en: { home: 'Home', blocks: 'Blocks', transactions: 'Transactions', bridge: 'Bridge', weth: 'WETH', tokens: 'Tokens', analytics: 'Analytics', operator: 'Operator', watchlist: 'Watchlist', searchPlaceholder: 'Address, tx hash, block number, or token/contract name...', connectWallet: 'Connect Wallet', darkMode: 'Dark mode', lightMode: 'Light mode', back: 'Back', exportCsv: 'Export CSV', tokenBalances: 'Token Balances', internalTxns: 'Internal Txns', readContract: 'Read contract', writeContract: 'Write contract', addToWatchlist: 'Add to watchlist', removeFromWatchlist: 'Remove from watchlist', checkApprovals: 'Check token approvals', copied: 'Copied' },
+ de: { home: 'Start', blocks: 'Blöcke', transactions: 'Transaktionen', bridge: 'Brücke', weth: 'WETH', tokens: 'Token', analytics: 'Analysen', operator: 'Operator', watchlist: 'Beobachtungsliste', searchPlaceholder: 'Adresse, Tx-Hash, Blocknummer oder Token/Vertrag…', connectWallet: 'Wallet verbinden', darkMode: 'Dunkelmodus', lightMode: 'Hellmodus', back: 'Zurück', exportCsv: 'CSV exportieren', tokenBalances: 'Token-Bestände', internalTxns: 'Interne Transaktionen', readContract: 'Vertrag lesen', writeContract: 'Vertrag schreiben', addToWatchlist: 'Zur Beobachtungsliste', removeFromWatchlist: 'Aus Beobachtungsliste entfernen', checkApprovals: 'Token-Freigaben prüfen', copied: 'Kopiert' },
+ fr: { home: 'Accueil', blocks: 'Blocs', transactions: 'Transactions', bridge: 'Pont', weth: 'WETH', tokens: 'Jetons', analytics: 'Analyses', operator: 'Opérateur', watchlist: 'Liste de suivi', searchPlaceholder: 'Adresse, hash de tx, numéro de bloc ou nom de token/contrat…', connectWallet: 'Connecter le portefeuille', darkMode: 'Mode sombre', lightMode: 'Mode clair', back: 'Retour', exportCsv: 'Exporter CSV', tokenBalances: 'Soldes de jetons', internalTxns: 'Transactions internes', readContract: 'Lire le contrat', writeContract: 'Écrire le contrat', addToWatchlist: 'Ajouter à la liste', removeFromWatchlist: 'Retirer de la liste', checkApprovals: 'Vérifier les approbations', copied: 'Copié' }
+ };
+ var currentLocale = (function(){ try { return localStorage.getItem('explorerLocale') || 'en'; } catch(e){ return 'en'; } })();
+ function t(key) { return (I18N[currentLocale] && I18N[currentLocale][key]) || I18N.en[key] || key; }
+ function setLocale(loc) { currentLocale = loc; try { localStorage.setItem('explorerLocale', loc); } catch(e){} if (typeof applyI18n === 'function') applyI18n(); }
+ function applyI18n() { document.querySelectorAll('[data-i18n]').forEach(function(el){ var k = el.getAttribute('data-i18n'); if (k) el.textContent = t(k); }); var searchIn = document.getElementById('searchInput'); if (searchIn) searchIn.placeholder = t('searchPlaceholder'); var localeSel = document.getElementById('localeSelect'); if (localeSel) localeSel.value = currentLocale; var wcBtn = document.getElementById('walletConnectBtn'); if (wcBtn) wcBtn.textContent = t('connectWallet'); }
+ window._renderWatchlist = function() { var container = document.getElementById('watchlistContent'); if (!container) return; var list = getWatchlist(); if (list.length === 0) { container.innerHTML = '
No addresses in watchlist. Open an address and click "Add to watchlist".
'; return; } var html = '| Address | Label | |
'; list.forEach(function(addr){ var label = getAddressLabel(addr) || ''; html += '| ' + escapeHtml(shortenHash(addr)) + ' | ' + escapeHtml(label) + ' | |
'; }); html += '
'; container.innerHTML = html; };
+ var KNOWN_ADDRESS_LABELS = { '0x89dd12025bfcd38a168455a44b400e913ed33be2': 'CCIP WETH9 Bridge', '0xe0e93247376aa097db308b92e6ba36ba015535d0': 'CCIP WETH10 Bridge', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': 'WETH9', '0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': 'WETH10', '0x8078a09637e47fa5ed34f626046ea2094a5cde5e': 'CCIP Router', '0x105f8a15b819948a89153505762444ee9f324684': 'CCIP Sender' };
+ function getAddressLabel(addr) { if (!addr) return ''; var lower = addr.toLowerCase(); if (KNOWN_ADDRESS_LABELS[lower]) return KNOWN_ADDRESS_LABELS[lower]; try { var j = localStorage.getItem('explorerAddressLabels'); if (!j) return ''; var m = JSON.parse(j); return m[lower] || ''; } catch(e){ return ''; } }
+ function formatAddressWithLabel(addr) { if (!addr) return ''; var label = getAddressLabel(addr); return label ? escapeHtml(label) + ' (' + escapeHtml(shortenHash(addr)) + ')' : escapeHtml(shortenHash(addr)); }
+ function copyToClipboard(val, msg) { if (!val) return; try { navigator.clipboard.writeText(String(val)); showToast(msg || 'Copied', 'success'); } catch(e) { showToast('Copy failed', 'error'); } }
+ function setAddressLabel(addr, label) { try { var j = localStorage.getItem('explorerAddressLabels') || '{}'; var m = JSON.parse(j); m[addr.toLowerCase()] = (label || '').trim(); localStorage.setItem('explorerAddressLabels', JSON.stringify(m)); return true; } catch(e){ return false; } }
+ function getWatchlist() { try { var j = localStorage.getItem('explorerWatchlist'); if (!j) return []; var a = JSON.parse(j); return Array.isArray(a) ? a : []; } catch(e){ return []; } }
+ function addToWatchlist(addr) { if (!addr || !/^0x[a-fA-F0-9]{40}$/i.test(addr)) return false; var a = getWatchlist(); var lower = addr.toLowerCase(); if (a.indexOf(lower) === -1) { a.push(lower); try { localStorage.setItem('explorerWatchlist', JSON.stringify(a)); return true; } catch(e){} } return false; }
+ function removeFromWatchlist(addr) { var a = getWatchlist().filter(function(x){ return x !== addr.toLowerCase(); }); try { localStorage.setItem('explorerWatchlist', JSON.stringify(a)); return true; } catch(e){ return false; } }
+ function isInWatchlist(addr) { return getWatchlist().indexOf((addr || '').toLowerCase()) !== -1; }
+ let currentView = 'home';
+ let currentDetailKey = '';
+ var _inNavHandler = false; // re-entrancy guard: prevents hashchange -> applyHashRoute -> stub from recursing
+ let provider = null;
+ let signer = null;
+ let userAddress = null;
+
+ // Tiered Architecture: Track and Authentication
+ let userTrack = 1; // Default to Track 1 (public)
+ let authToken = null;
+
+ // View switch helper: works even if rest of script fails. Do NOT set location.hash here (we use path-based URLs).
+ function switchToView(viewName) {
+ if (viewName !== 'blocks' && _blocksScrollAnimationId != null) {
+ cancelAnimationFrame(_blocksScrollAnimationId);
+ _blocksScrollAnimationId = null;
+ }
+ currentView = viewName;
+ var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens'];
+ 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');
+ }
+ // Expose nav handlers: re-entrancy guard prevents hashchange from calling stub again while we're inside
+ window.showHome = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('home'); if (window._showHome) window._showHome(); } finally { _inNavHandler = false; } };
+ window.showBlocks = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('blocks'); if (window._showBlocks) window._showBlocks(); } finally { _inNavHandler = false; } };
+ window.showTransactions = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('transactions'); if (window._showTransactions) window._showTransactions(); } finally { _inNavHandler = false; } };
+ window.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; } };
+ 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; } };
+ window.showBlockDetail = function(n) { if (window._showBlockDetail) window._showBlockDetail(n); };
+ window.showTransactionDetail = function(h) { if (window._showTransactionDetail) window._showTransactionDetail(h); };
+ window.showAddressDetail = function(a) { if (window._showAddressDetail) window._showAddressDetail(a); };
+ window.toggleDarkMode = function() { document.body.classList.toggle('dark-theme'); var icon = document.getElementById('themeIcon'); if (icon) icon.className = document.body.classList.contains('dark-theme') ? 'fas fa-sun' : 'fas fa-moon'; try { localStorage.setItem('explorerTheme', document.body.classList.contains('dark-theme') ? 'dark' : 'light'); } catch (e) {} };
+
+ // Feature flags
+ const FEATURE_FLAGS = {
+ ADDRESS_FULL_DETAIL: { track: 2 },
+ TOKEN_BALANCES: { track: 2 },
+ TX_HISTORY: { track: 2 },
+ INTERNAL_TXS: { track: 2 },
+ ENHANCED_SEARCH: { track: 2 },
+ ANALYTICS_DASHBOARD: { track: 3 },
+ FLOW_TRACKING: { track: 3 },
+ BRIDGE_ANALYTICS: { track: 3 },
+ OPERATOR_PANEL: { track: 4 },
+ };
+
+ function hasAccess(requiredTrack) {
+ return userTrack >= requiredTrack;
+ }
+
+ function isFeatureEnabled(featureName) {
+ const feature = FEATURE_FLAGS[featureName];
+ if (!feature) return false;
+ return hasAccess(feature.track);
+ }
+
+ // Load feature flags from API
+ async function loadFeatureFlags() {
+ try {
+ const response = await fetch('/api/v1/features', {
+ headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
+ });
+ if (response.ok) {
+ const data = await response.json();
+ userTrack = data.track || 1;
+ updateUIForTrack();
+ }
+ } catch (error) {
+ console.error('Failed to load feature flags:', error);
+ }
+ }
+
+ function updateUIForTrack() {
+ // Show/hide navigation items based on track
+ const analyticsNav = document.getElementById('analyticsNav');
+ const operatorNav = document.getElementById('operatorNav');
+ const moreWrap = document.getElementById('navDropdownMoreWrap');
+ if (analyticsNav) analyticsNav.style.display = hasAccess(3) ? 'block' : 'none';
+ if (operatorNav) operatorNav.style.display = hasAccess(4) ? 'block' : 'none';
+ if (moreWrap) moreWrap.style.display = (hasAccess(3) || hasAccess(4)) ? '' : 'none';
+ }
+
+ // Wallet authentication
+ async function connectWallet() {
+ if (typeof ethers === 'undefined') {
+ alert('Ethers.js not loaded. Please refresh the page.');
+ return;
+ }
+
+ try {
+ if (!window.ethereum) {
+ alert('MetaMask not detected. Please install MetaMask.');
+ return;
+ }
+
+ const provider = new ethers.providers.Web3Provider(window.ethereum);
+ const accounts = await provider.send("eth_requestAccounts", []);
+ const address = accounts[0];
+
+ // Request nonce
+ const nonceResp = await fetch('/api/v1/auth/nonce', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ address })
+ });
+ const nonceData = await nonceResp.json();
+
+ // Sign message
+ const message = `Sign this message to authenticate with SolaceScanScout Explorer.\n\nNonce: ${nonceData.nonce}`;
+ const signer = provider.getSigner();
+ const signature = await signer.signMessage(message);
+
+ // Authenticate
+ const authResp = await fetch('/api/v1/auth/wallet', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ address, signature, nonce: nonceData.nonce })
+ });
+
+ if (authResp.ok) {
+ const authData = await authResp.json();
+ authToken = authData.token;
+ userTrack = authData.track;
+ userAddress = address;
+ localStorage.setItem('authToken', authToken);
+ localStorage.setItem('userAddress', userAddress);
+ updateUIForTrack();
+ const walletBtn = document.getElementById('walletConnectBtn');
+ const walletStatus = document.getElementById('walletStatus');
+ const walletAddress = document.getElementById('walletAddress');
+ if (walletBtn) walletBtn.style.display = 'none';
+ if (walletStatus) walletStatus.style.display = 'flex';
+ if (walletAddress) walletAddress.textContent = shortenHash(address);
+ await loadFeatureFlags();
+ showToast('Wallet connected successfully!', 'success');
+ } else {
+ const errorData = await authResp.json();
+ alert('Authentication failed: ' + (errorData.error?.message || 'Unknown error'));
+ }
+ } catch (error) {
+ console.error('Wallet connection error:', error);
+ alert('Failed to connect wallet: ' + error.message);
+ }
+ }
+
+ // 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;
+ return {
+ 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,
+ name: blockscoutAddr.name || null,
+ ens_domain_name: blockscoutAddr.ens_domain_name || null,
+ creation_tx_hash: creationTx,
+ first_seen_at: firstSeen,
+ last_seen_at: lastSeen
+ };
+ }
+
+ // Skeleton loader function
+ function createSkeletonLoader(type) {
+ switch(type) {
+ case 'stats':
+ return `
+
+ `;
+ case 'table':
+ return `
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+ ${Array(5).fill(0).map(() => `
+
+ |
+ |
+ |
+ |
+
+ `).join('')}
+
+
+ `;
+ case 'detail':
+ return `
+
+
+
+ ${Array(8).fill(0).map(() => `
+
+ `).join('')}
+
+
+ `;
+ default:
+ return ' Loading...
';
+ }
+ }
+
+ // 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();
+ applyHashRoute();
+ window.addEventListener('popstate', function() { applyHashRoute(); });
+ window.addEventListener('hashchange', function() { applyHashRoute(); });
+ window.addEventListener('load', function() { applyHashRoute(); });
+ console.log('Loading stats, blocks, and transactions...');
+ loadStats();
+ loadLatestBlocks();
+ loadLatestTransactions();
+ startTransactionUpdates();
+
+ // Ethers is only needed for MetaMask/WETH; don't block feeds on it
+ try {
+ await ensureEthers();
+ console.log('Ethers ready.');
+ } catch (error) {
+ console.warn('Ethers not ready, continuing without MetaMask features:', error);
+ }
+
+ setTimeout(() => {
+ if (typeof ethers !== 'undefined' && typeof window.ethereum !== 'undefined') {
+ checkMetaMaskConnection();
+ }
+ }, 500);
+ });
+
+ // MetaMask Connection
+ let checkingMetaMask = false;
+ async function checkMetaMaskConnection() {
+ // Prevent multiple simultaneous checks
+ if (checkingMetaMask) {
+ console.log('checkMetaMaskConnection already in progress, skipping...');
+ return;
+ }
+ checkingMetaMask = true;
+
+ try {
+ // Ensure ethers is loaded before checking MetaMask
+ if (typeof ethers === 'undefined') {
+ try {
+ await ensureEthers();
+ // Wait a bit more to ensure ethers is fully initialized
+ await new Promise(resolve => setTimeout(resolve, 100));
+ } catch (error) {
+ console.warn('Ethers not available, skipping MetaMask check:', error);
+ return;
+ }
+ }
+
+ // Double-check ethers is available
+ if (typeof ethers === 'undefined') {
+ console.warn('Ethers still not available after ensureEthers(), skipping MetaMask check');
+ return;
+ }
+
+ if (typeof window.ethereum !== 'undefined') {
+ try {
+ const accounts = await window.ethereum.request({ method: 'eth_accounts' });
+ if (accounts.length > 0) {
+ await connectMetaMask();
+ }
+ } catch (error) {
+ console.error('Error checking MetaMask:', error);
+ }
+ }
+ } finally {
+ checkingMetaMask = false;
+ }
+ }
+
+ 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 = `
+
+ Connected: ${escapeHtml(shortenHash(userAddress))}
+
+ `;
+
+ // 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) {
+ console.error('Error connecting MetaMask:', error);
+ let errorMessage = error.message || 'Unknown error';
+ if (errorMessage.includes('ethers is not defined') || typeof ethers === 'undefined') {
+ errorMessage = 'Ethers library failed to load. Please refresh the page.';
+ }
+ alert('Failed to connect MetaMask: ' + errorMessage);
+ }
+ } finally {
+ connectingMetaMask = false;
+ }
+ }
+
+ async function switchToChain138() {
+ const chainId = '0x8A'; // 138 in hex
+ try {
+ await window.ethereum.request({
+ method: 'wallet_switchEthereumChain',
+ params: [{ chainId }],
+ });
+ } catch (switchError) {
+ // If chain doesn't exist, add it
+ if (switchError.code === 4902) {
+ try {
+ await window.ethereum.request({
+ method: 'wallet_addEthereumChain',
+ params: [{
+ chainId,
+ chainName: 'Chain 138',
+ nativeCurrency: {
+ name: 'ETH',
+ symbol: 'ETH',
+ decimals: 18
+ },
+ rpcUrls: RPC_URLS.length > 0 ? RPC_URLS : [RPC_URL],
+ blockExplorerUrls: [window.location.origin || 'https://explorer.d-bis.org']
+ }],
+ });
+ } catch (addError) {
+ console.error('Error adding chain:', 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 = `
+
+ MetaMask not connected
+
+ `;
+
+ document.getElementById('weth9WrapBtn').disabled = true;
+ document.getElementById('weth9UnwrapBtn').disabled = true;
+ document.getElementById('weth10WrapBtn').disabled = true;
+ document.getElementById('weth10UnwrapBtn').disabled = true;
+ }
+
+ async function refreshWETHBalances() {
+ if (!userAddress) return;
+
+ try {
+ await ensureEthers();
+
+ // Checksum addresses when ethers is available
+ if (typeof ethers !== 'undefined' && ethers.utils) {
+ try {
+ // Convert to lowercase first, then checksum
+ const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
+ WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
+ } catch (e) {
+ console.warn('Could not checksum WETH10 address:', e);
+ // Fallback to lowercase version
+ WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
+ }
+ } else {
+ // Fallback to lowercase if ethers not available
+ WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
+ }
+
+ // Get ETH balance
+ const ethBalance = await provider.getBalance(userAddress);
+ const ethBalanceFormatted = formatEther(ethBalance);
+
+ // Get WETH9 balance
+ const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, provider);
+ const weth9Balance = await weth9Contract.balanceOf(userAddress);
+ const weth9BalanceFormatted = formatEther(weth9Balance);
+
+ // Get WETH10 balance - use checksummed address
+ const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, provider);
+ const weth10Balance = await weth10Contract.balanceOf(userAddress);
+ const weth10BalanceFormatted = formatEther(weth10Balance);
+
+ // Update UI
+ document.getElementById('weth9EthBalance').textContent = ethBalanceFormatted + ' ETH';
+ document.getElementById('weth9TokenBalance').textContent = weth9BalanceFormatted + ' WETH9';
+ document.getElementById('weth10EthBalance').textContent = ethBalanceFormatted + ' ETH';
+ document.getElementById('weth10TokenBalance').textContent = weth10BalanceFormatted + ' WETH10';
+ } catch (error) {
+ console.error('Error refreshing balances:', error);
+ }
+ }
+
+ function wrapUnwrapErrorMessage(op, error) {
+ if (error && (error.code === 4001 || error.code === 'ACTION_REJECTED' || (error.message && /user rejected|user denied/i.test(error.message)))) return 'Transaction cancelled.';
+ if (error && error.reason) return error.reason;
+ return (error && error.message) ? error.message : 'Unknown error';
+ }
+ function setMaxWETH9(type) {
+ if (type === 'wrap') {
+ const ethBalance = document.getElementById('weth9EthBalance').textContent.replace(' ETH', '');
+ document.getElementById('weth9WrapAmount').value = parseFloat(ethBalance).toFixed(6);
+ } else {
+ const wethBalance = document.getElementById('weth9TokenBalance').textContent.replace(' WETH9', '');
+ document.getElementById('weth9UnwrapAmount').value = parseFloat(wethBalance).toFixed(6);
+ }
+ }
+
+ function setMaxWETH10(type) {
+ if (type === 'wrap') {
+ const ethBalance = document.getElementById('weth10EthBalance').textContent.replace(' ETH', '');
+ document.getElementById('weth10WrapAmount').value = parseFloat(ethBalance).toFixed(6);
+ } else {
+ const wethBalance = document.getElementById('weth10TokenBalance').textContent.replace(' WETH10', '');
+ document.getElementById('weth10UnwrapAmount').value = parseFloat(wethBalance).toFixed(6);
+ }
+ }
+
+ async function wrapWETH9() {
+ const amount = document.getElementById('weth9WrapAmount').value;
+ if (!amount || parseFloat(amount) <= 0) {
+ alert('Please enter a valid amount');
+ return;
+ }
+
+ if (!signer) {
+ alert('Please connect MetaMask first');
+ return;
+ }
+
+ try {
+ await ensureEthers();
+ const amountWei = ethers.utils.parseEther(amount);
+ 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 = ' Processing...';
+
+ const tx = await weth9Contract.deposit({ value: amountWei });
+ const receipt = await tx.wait();
+
+ btn.innerHTML = ' Success!';
+ document.getElementById('weth9WrapAmount').value = '';
+ await refreshWETHBalances();
+
+ setTimeout(() => {
+ btn.innerHTML = ' 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 = ' Wrap ETH to WETH9';
+ document.getElementById('weth9WrapBtn').disabled = false;
+ }
+ }
+
+ async function unwrapWETH9() {
+ const amount = document.getElementById('weth9UnwrapAmount').value;
+ if (!amount || parseFloat(amount) <= 0) {
+ alert('Please enter a valid amount');
+ return;
+ }
+
+ if (!signer) {
+ alert('Please connect MetaMask first');
+ return;
+ }
+
+ try {
+ await ensureEthers();
+ const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, signer);
+ const amountWei = ethers.utils.parseEther(amount);
+ 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 = ' Processing...';
+
+ const tx = await weth9Contract.withdraw(amountWei);
+ const receipt = await tx.wait();
+
+ btn.innerHTML = ' Success!';
+ document.getElementById('weth9UnwrapAmount').value = '';
+ await refreshWETHBalances();
+
+ setTimeout(() => {
+ btn.innerHTML = ' 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 = ' Unwrap WETH9 to ETH';
+ document.getElementById('weth9UnwrapBtn').disabled = false;
+ }
+ }
+
+ async function wrapWETH10() {
+ const amount = document.getElementById('weth10WrapAmount').value;
+ if (!amount || parseFloat(amount) <= 0) {
+ alert('Please enter a valid amount');
+ return;
+ }
+
+ if (!signer) {
+ alert('Please connect MetaMask first');
+ return;
+ }
+
+ try {
+ await ensureEthers();
+
+ // Ensure address is checksummed
+ if (typeof ethers !== 'undefined' && ethers.utils) {
+ try {
+ const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
+ WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
+ } catch (e) {
+ console.warn('Could not checksum WETH10 address:', e);
+ WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
+ }
+ } else {
+ WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
+ }
+
+ const amountWei = ethers.utils.parseEther(amount);
+ 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 = ' Processing...';
+
+ const tx = await weth10Contract.deposit({ value: amountWei });
+ const receipt = await tx.wait();
+
+ btn.innerHTML = ' Success!';
+ document.getElementById('weth10WrapAmount').value = '';
+ await refreshWETHBalances();
+
+ setTimeout(() => {
+ btn.innerHTML = ' 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 = ' Wrap ETH to WETH10';
+ document.getElementById('weth10WrapBtn').disabled = false;
+ }
+ }
+
+ async function unwrapWETH10() {
+ const amount = document.getElementById('weth10UnwrapAmount').value;
+ if (!amount || parseFloat(amount) <= 0) {
+ alert('Please enter a valid amount');
+ return;
+ }
+
+ if (!signer) {
+ alert('Please connect MetaMask first');
+ return;
+ }
+
+ try {
+ await ensureEthers();
+
+ // Ensure address is checksummed
+ if (typeof ethers !== 'undefined' && ethers.utils) {
+ try {
+ const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
+ WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
+ } catch (e) {
+ console.warn('Could not checksum WETH10 address:', e);
+ WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
+ }
+ } else {
+ WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
+ }
+
+ const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, signer);
+ const amountWei = ethers.utils.parseEther(amount);
+ 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 = ' Processing...';
+
+ const tx = await weth10Contract.withdraw(amountWei);
+ const receipt = await tx.wait();
+
+ btn.innerHTML = ' Success!';
+ document.getElementById('weth10UnwrapAmount').value = '';
+ await refreshWETHBalances();
+
+ setTimeout(() => {
+ btn.innerHTML = ' 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 = ' Unwrap WETH10 to ETH';
+ document.getElementById('weth10UnwrapBtn').disabled = false;
+ }
+ }
+
+ function showWETHTab(tab, clickedElement) {
+ document.querySelectorAll('.weth-tab-content').forEach(el => el.style.display = 'none');
+ document.querySelectorAll('.weth-tab').forEach(el => el.classList.remove('active'));
+
+ const tabElement = document.getElementById(`${tab}Tab`);
+ if (tabElement) {
+ tabElement.style.display = 'block';
+ }
+
+ // Update active tab - use clickedElement if provided, otherwise find by tab name
+ if (clickedElement) {
+ clickedElement.classList.add('active');
+ } else {
+ // Find the button that corresponds to this tab
+ const tabButtons = document.querySelectorAll('.weth-tab');
+ tabButtons.forEach(btn => {
+ if (btn.getAttribute('onclick')?.includes(`'${tab}'`)) {
+ btn.classList.add('active');
+ }
+ });
+ }
+ }
+ window.showWETHTab = showWETHTab;
+
+ async function showWETHUtilities() {
+ showView('weth');
+ if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'weth') updatePath('/weth');
+ if (userAddress) {
+ await refreshWETHBalances();
+ }
+ }
+ window._showWETHUtilities = showWETHUtilities;
+
+ async function showBridgeMonitoring() {
+ showView('bridge');
+ if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'bridge') updatePath('/bridge');
+ await refreshBridgeData();
+ }
+ window._showBridgeMonitoring = showBridgeMonitoring;
+
+ async function showHome() {
+ showView('home');
+ if ((window.location.pathname || '').replace(/^\//, '').replace(/\/$/, '') !== 'home') updatePath('/home');
+ await loadStats();
+ await loadLatestBlocks();
+ await loadLatestTransactions();
+ // Start real-time transaction updates
+ startTransactionUpdates();
+ }
+ window._showHome = showHome;
+
+ async function showBlocks() {
+ showView('blocks');
+ if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'blocks') updatePath('/blocks');
+ await loadAllBlocks();
+ }
+ window._showBlocks = showBlocks;
+
+ async function showTransactions() {
+ showView('transactions');
+ if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'transactions') updatePath('/transactions');
+ await loadAllTransactions();
+ }
+ window._showTransactions = showTransactions;
+
+ // Analytics view (Track 3+)
+ function showAnalytics() {
+ if (!hasAccess(3)) {
+ showToast('Analytics features require Track 3 access. Please connect your wallet and ensure you are approved.', 'error');
+ return;
+ }
+ switchToView('analytics');
+ }
+ window._showAnalytics = showAnalytics;
+
+ // Operator view (Track 4)
+ function showOperator() {
+ if (!hasAccess(4)) {
+ showToast('Operator features require Track 4 access. Please connect your wallet and ensure you are approved.', 'error');
+ return;
+ }
+ switchToView('operator');
+ }
+ window._showOperator = showOperator;
+
+ function showView(viewName) {
+ currentView = viewName;
+ var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens'];
+ if (detailViews.indexOf(viewName) === -1) currentDetailKey = '';
+ document.querySelectorAll('.detail-view').forEach(v => v.classList.remove('active'));
+ const homeView = document.getElementById('homeView');
+ if (homeView) homeView.style.display = viewName === 'home' ? 'block' : 'none';
+ if (viewName !== 'home') {
+ const targetView = document.getElementById(`${viewName}View`);
+ if (targetView) targetView.classList.add('active');
+ stopTransactionUpdates();
+ }
+ }
+ window.showView = showView;
+
+ function toggleDarkMode() {
+ document.body.classList.toggle('dark-theme');
+ var isDark = document.body.classList.contains('dark-theme');
+ try { localStorage.setItem('explorerTheme', isDark ? 'dark' : 'light'); } catch (e) {}
+ var icon = document.getElementById('themeIcon');
+ if (icon) {
+ icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
+ }
+ }
+ function applyStoredTheme() {
+ try {
+ var theme = localStorage.getItem('explorerTheme');
+ if (theme === 'dark') {
+ document.body.classList.add('dark-theme');
+ var icon = document.getElementById('themeIcon');
+ if (icon) icon.className = 'fas fa-sun';
+ }
+ } catch (e) {}
+ }
+ function updatePath(path) {
+ if (typeof history !== 'undefined' && history.pushState) {
+ history.pushState(null, '', path);
+ }
+ }
+ window.updatePath = updatePath;
+ function applyHashRoute() {
+ var route = '';
+ var fromPath = (window.location.pathname || '/').replace(/^\//, '').replace(/\/$/, '').replace(/^index\.html$/i, '');
+ var fromHash = (window.location.hash || '').replace(/^#/, '');
+ if (fromPath && fromPath !== '') {
+ route = fromPath;
+ } else if (fromHash) {
+ route = fromHash;
+ }
+ if (!route) { showHome(); updatePath('/home'); return; }
+ var parts = route.split('/').filter(Boolean);
+ var decode = function(s) { try { return decodeURIComponent(s); } catch (e) { return s; } };
+ if (parts[0] === 'block' && parts[1]) { var p1 = decode(parts[1]); if (currentDetailKey === 'block:' + p1) return; showBlockDetail(p1); return; }
+ if (parts[0] === 'tx' && parts[1]) { var p1 = decode(parts[1]); if (currentDetailKey === 'tx:' + p1) return; showTransactionDetail(p1); return; }
+ if (parts[0] === 'address' && parts[1]) { var p1 = decode(parts[1]); if (currentDetailKey === 'address:' + p1.toLowerCase()) return; showAddressDetail(p1); return; }
+ if (parts[0] === 'token' && parts[1]) { var p1 = decode(parts[1]); if (currentDetailKey === 'token:' + p1.toLowerCase()) return; showTokenDetail(p1); return; }
+ if (parts[0] === 'nft' && parts[1] && parts[2]) { var p1 = decode(parts[1]), p2 = decode(parts[2]); if (currentDetailKey === 'nft:' + p1.toLowerCase() + ':' + p2) return; showNftDetail(p1, p2); return; }
+ if (parts[0] === 'home') { if (currentView !== 'home') showHome(); return; }
+ if (parts[0] === 'blocks') { if (currentView !== 'blocks') showBlocks(); return; }
+ if (parts[0] === 'transactions') { if (currentView !== 'transactions') showTransactions(); return; }
+ if (parts[0] === 'bridge') { if (currentView !== 'bridge') showBridgeMonitoring(); return; }
+ if (parts[0] === 'weth') { if (currentView !== 'weth') showWETHUtilities(); return; }
+ if (parts[0] === 'watchlist') { if (currentView !== 'watchlist') showWatchlist(); return; }
+ if (parts[0] === 'tokens') { if (typeof showTokensList === 'function') showTokensList(); else focusSearchWithHint('token'); return; }
+ if (parts[0] === 'analytics') { if (currentView !== 'analytics') showAnalytics(); return; }
+ if (parts[0] === 'operator') { if (currentView !== 'operator') showOperator(); return; }
+ }
+ window.applyHashRoute = applyHashRoute;
+ var hasRouteOnReady = window.location.hash || ((window.location.pathname || '').replace(/^\//, '').replace(/\/$/, ''));
+ if (document.readyState !== 'loading' && hasRouteOnReady) { applyHashRoute(); }
+ window.toggleDarkMode = toggleDarkMode;
+
+ function focusSearchWithHint(kind) {
+ showHome();
+ var input = document.getElementById('searchInput');
+ if (input) {
+ input.focus();
+ if (kind === 'token') showToast('Enter token contract address (0x...) or search by name/symbol', 'info', 4000);
+ }
+ }
+ window.focusSearchWithHint = focusSearchWithHint;
+
+ function toggleNavMenu() {
+ var links = document.getElementById('navLinks');
+ var btn = document.getElementById('navToggle');
+ var icon = document.getElementById('navToggleIcon');
+ if (!links || !btn) return;
+ var isOpen = links.classList.toggle('nav-open');
+ btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
+ if (icon) icon.className = isOpen ? 'fas fa-times' : 'fas fa-bars';
+ }
+ function closeNavMenu() {
+ var links = document.getElementById('navLinks');
+ var btn = document.getElementById('navToggle');
+ var icon = document.getElementById('navToggleIcon');
+ if (links) links.classList.remove('nav-open');
+ if (btn) btn.setAttribute('aria-expanded', 'false');
+ if (icon) icon.className = 'fas fa-bars';
+ closeAllNavDropdowns();
+ }
+ function closeAllNavDropdowns() {
+ document.querySelectorAll('.nav-dropdown.open').forEach(function (el) {
+ el.classList.remove('open');
+ var trigger = el.querySelector('.nav-dropdown-trigger');
+ if (trigger) trigger.setAttribute('aria-expanded', 'false');
+ });
+ }
+ function initNavDropdowns() {
+ document.querySelectorAll('.nav-dropdown-trigger').forEach(function (trigger) {
+ trigger.addEventListener('click', function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var dropdown = trigger.closest('.nav-dropdown');
+ var wasOpen = dropdown && dropdown.classList.contains('open');
+ closeAllNavDropdowns();
+ if (dropdown && !wasOpen) {
+ dropdown.classList.add('open');
+ trigger.setAttribute('aria-expanded', 'true');
+ }
+ });
+ });
+ document.addEventListener('click', function () {
+ closeAllNavDropdowns();
+ });
+ document.getElementById('navLinks').addEventListener('click', function (e) {
+ if (e.target.closest('.nav-dropdown-menu')) e.stopPropagation();
+ });
+ }
+ window.toggleNavMenu = toggleNavMenu;
+ window.closeNavMenu = closeNavMenu;
+ window.closeAllNavDropdowns = closeAllNavDropdowns;
+
+ // Update breadcrumb navigation
+ function updateBreadcrumb(type, identifier, identifierExtra) {
+ let breadcrumbContainer;
+ let breadcrumbHTML = 'Home';
+ switch (type) {
+ case 'block':
+ breadcrumbContainer = document.getElementById('blockDetailBreadcrumb');
+ breadcrumbHTML += '/';
+ breadcrumbHTML += 'Blocks';
+ breadcrumbHTML += '/';
+ breadcrumbHTML += 'Block #' + escapeHtml(String(identifier)) + '';
+ break;
+ case 'transaction':
+ breadcrumbContainer = document.getElementById('transactionDetailBreadcrumb');
+ breadcrumbHTML += '/';
+ breadcrumbHTML += 'Transactions';
+ breadcrumbHTML += '/';
+ breadcrumbHTML += '' + escapeHtml(shortenHash(identifier)) + '';
+ break;
+ case 'address':
+ breadcrumbContainer = document.getElementById('addressDetailBreadcrumb');
+ breadcrumbHTML += '/Address/' + escapeHtml(shortenHash(identifier)) + '';
+ break;
+ case 'token':
+ breadcrumbContainer = document.getElementById('tokenDetailBreadcrumb');
+ breadcrumbHTML += '/';
+ breadcrumbHTML += 'Tokens';
+ breadcrumbHTML += '/';
+ breadcrumbHTML += 'Token ' + escapeHtml(shortenHash(identifier)) + '';
+ break;
+ case 'nft':
+ breadcrumbContainer = document.getElementById('nftDetailBreadcrumb');
+ breadcrumbHTML += '/';
+ breadcrumbHTML += '' + escapeHtml(shortenHash(identifier)) + '';
+ breadcrumbHTML += '/';
+ breadcrumbHTML += 'Token ID ' + (identifierExtra != null ? escapeHtml(String(identifierExtra)) : '') + '';
+ break;
+ default:
+ return;
+ }
+ if (breadcrumbContainer) {
+ breadcrumbContainer.innerHTML = breadcrumbHTML;
+ }
+ }
+
+ // Retry logic with exponential backoff
+ async function fetchAPIWithRetry(url, maxRetries = FETCH_MAX_RETRIES, retryDelay = RETRY_DELAY_MS) {
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
+ try {
+ return await fetchAPI(url);
+ } catch (error) {
+ const isLastAttempt = attempt === maxRetries - 1;
+ const isRetryable = error.name === 'AbortError' ||
+ (error.message && (error.message.includes('timeout') ||
+ error.message.includes('500') ||
+ error.message.includes('502') ||
+ error.message.includes('503') ||
+ error.message.includes('504') ||
+ error.message.includes('NetworkError')));
+
+ if (isLastAttempt || !isRetryable) {
+ throw error;
+ }
+
+ // Exponential backoff: 1s, 2s, 4s
+ const delay = retryDelay * Math.pow(2, attempt);
+ console.warn(`⚠️ API call failed (attempt ${attempt + 1}/${maxRetries}), retrying in ${delay}ms...`, error.message);
+ await new Promise(resolve => setTimeout(resolve, delay));
+ }
+ }
+ }
+
+ async function fetchAPI(url) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
+
+ try {
+ const response = await fetch(url, {
+ method: 'GET',
+ headers: { 'Accept': 'application/json' },
+ credentials: 'omit',
+ signal: controller.signal
+ });
+
+ clearTimeout(timeoutId);
+ if (!response.ok) {
+ let errorText = '';
+ try {
+ errorText = await response.text();
+ } catch (e) {
+ errorText = response.statusText;
+ }
+
+ // Log detailed error for debugging
+ const errorInfo = {
+ status: response.status,
+ statusText: response.statusText,
+ errorText: errorText.substring(0, 500),
+ url: url,
+ headers: Object.fromEntries(response.headers.entries())
+ };
+ console.error(`❌ API Error:`, errorInfo);
+
+ // For 400 errors, provide more context
+ if (response.status === 400) {
+ console.error('🔍 HTTP 400 Bad Request Details:');
+ console.error('URL:', url);
+ console.error('Response Headers:', errorInfo.headers);
+ console.error('Error Body:', errorText);
+ console.error('Possible causes:');
+ console.error('1. Invalid query parameters');
+ console.error('2. Missing required parameters');
+ console.error('3. API endpoint format incorrect');
+ console.error('4. CORS preflight failed');
+ console.error('5. Request method not allowed');
+
+ // Try to parse error if it's JSON
+ try {
+ const errorJson = JSON.parse(errorText);
+ console.error('Parsed Error:', errorJson);
+ } catch (e) {
+ // Not JSON, that's fine
+ }
+ }
+
+ throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`);
+ }
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return await response.json();
+ }
+ const text = await response.text();
+ try {
+ return JSON.parse(text);
+ } catch (e) {
+ throw new Error(`Invalid JSON response: ${text.substring(0, 100)}`);
+ }
+ } catch (error) {
+ clearTimeout(timeoutId);
+ if (error.name === 'AbortError') {
+ throw new Error('Request timeout. Please try again.');
+ }
+ throw error;
+ }
+ }
+
+ async function 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`);
+ }
+
+ statsGrid.innerHTML = `
+
+
Total Blocks
+
${formatNumber(stats.total_blocks || 0)}
+
+
+
Total Transactions
+
${formatNumber(stats.total_transactions || 0)}
+
+
+
Total Addresses
+
${formatNumber(stats.total_addresses || 0)}
+
+
+
Bridge Contracts
+
2 Active
+
+
+ `;
+ if (CHAIN_ID === 138) loadGasAndNetworkStats();
+ } catch (error) {
+ console.error('Failed to load stats:', error);
+ statsGrid.innerHTML = `
+
+
+
Total Transactions
+
-
+
+
+
+
Bridge Contracts
+
2 Active
+
+
+ `;
+ }
+ }
+
+ async function loadGasAndNetworkStats() {
+ var el = document.getElementById('networkStatValue');
+ var gasCard = document.getElementById('gasNetworkCard');
+ var gasContent = document.getElementById('gasNetworkContent');
+ 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) + '
' : '') + (blockTimeSec !== '-' ? 'Block: ' + escapeHtml(blockTimeSec) + '
' : '') + (tps !== '-' ? 'TPS: ' + escapeHtml(tps) : '') || 'Gas / TPS';
+ if (gasCard && gasContent && CHAIN_ID === 138) {
+ var curEl = document.getElementById('gasCurrentValue');
+ var tpsEl = document.getElementById('gasTpsValue');
+ var btEl = document.getElementById('gasBlockTimeValue');
+ var failedEl = document.getElementById('gasFailedRateValue');
+ var barsEl = document.getElementById('gasHistoryBars');
+ if (curEl) curEl.textContent = gasGwei !== '-' ? gasGwei : '—';
+ if (tpsEl) tpsEl.textContent = tps !== '-' ? tps : '—';
+ if (btEl) btEl.textContent = blockTimeSec !== '-' ? blockTimeSec : '—';
+ if (failedEl) {
+ var txsResp = await fetch(BLOCKSCOUT_API + '/v2/transactions?page=1&page_size=100').then(function(r) { return r.json(); }).catch(function() { return { items: [] }; });
+ var txs = txsResp.items || txsResp || [];
+ var failed = txs.filter(function(t) { var s = t.status; return s === 0 || s === '0' || (t.block && t.block.success === false); }).length;
+ failedEl.textContent = txs.length > 0 ? (100 * failed / txs.length).toFixed(2) + '%' : '—';
+ }
+ if (barsEl) {
+ var recent = blocks.slice(0, 10);
+ var fees = recent.map(function(bl) { var f = bl.base_fee_per_gas || bl.base_fee; return f != null ? Number(f) / 1e9 : 0; });
+ var maxFee = Math.max.apply(null, fees) || 1;
+ barsEl.innerHTML = fees.map(function(g, i) {
+ var pct = maxFee > 0 ? (g / maxFee * 100) : 0;
+ return '';
+ }).join('');
+ }
+ }
+ } catch (e) {
+ if (el) el.textContent = '-';
+ }
+ }
+
+ 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 '' +
+ '
#' + escapeHtml(String(d.blockNum)) + '
' +
+ '
' + escapeHtml(shortenHash(d.hash)) + '
' +
+ '
' +
+ '
Transactions' + escapeHtml(String(d.txCount)) + '
' +
+ '
Time' + escapeHtml(d.timeAgo) + '
' +
+ '
';
+ }
+
+ // 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 = 'API temporarily unavailable. ' + escapeHtml((blockscoutError.message || 'Unknown error').substring(0, 150)) + '
';
+ }
+ return;
+ }
+ if (blocks.length === 0 && container) {
+ container.innerHTML = 'Could not load blocks.
';
+ 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);
+
+ if (limitedBlocks.length === 0) {
+ if (container) container.innerHTML = 'No blocks found.
';
+ } else {
+ // Create HTML with duplicated blocks for seamless infinite loop
+ let html = '';
+ if (container) container.innerHTML = html;
+
+ // Setup auto-scroll animation
+ const scrollContainer = document.getElementById('blocksScrollContainer');
+ const scrollContent = scrollContainer?.querySelector('.blocks-scroll-content');
+ if (scrollContainer && scrollContent) {
+ const cardWidth = 200 + 16; // card width (200px) + gap (16px = 1rem)
+ const singleSetWidth = limitedBlocks.length * cardWidth;
+
+ // Use CSS transform for smooth animation
+ let scrollPosition = 0;
+ let isPaused = false;
+ const scrollSpeed = 0.4; // pixels per frame (adjust for speed)
+
+ scrollContainer.addEventListener('mouseenter', () => {
+ isPaused = true;
+ });
+
+ scrollContainer.addEventListener('mouseleave', () => {
+ isPaused = false;
+ });
+
+ if (_blocksScrollAnimationId != null) {
+ cancelAnimationFrame(_blocksScrollAnimationId);
+ _blocksScrollAnimationId = null;
+ }
+ function animateScroll() {
+ if (!isPaused && scrollContent) {
+ scrollPosition += scrollSpeed;
+ if (scrollPosition >= singleSetWidth) scrollPosition = 0;
+ scrollContent.style.transform = 'translateX(-' + scrollPosition + 'px)';
+ }
+ _blocksScrollAnimationId = requestAnimationFrame(animateScroll);
+ }
+ setTimeout(function() {
+ _blocksScrollAnimationId = requestAnimationFrame(animateScroll);
+ }, 500);
+ }
+
+ // Remove animation classes after animation completes
+ setTimeout(() => {
+ container.querySelectorAll('.new-block').forEach(card => {
+ card.classList.remove('new-block');
+ });
+ }, 1000);
+ }
+ } catch (error) {
+ console.error('Failed to load latest blocks:', error);
+ if (container) container.innerHTML = 'Failed to load blocks: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '.
';
+ } finally {
+ loadingBlocks = false;
+ }
+ }
+
+ // Store previous transaction hashes for real-time updates
+ let previousTransactionHashes = new Set();
+ let transactionUpdateInterval = null;
+
+ async function loadLatestTransactions() {
+ const container = document.getElementById('latestTransactions');
+ if (!container) return;
+
+ try {
+ let response;
+ let rawTransactions = [];
+
+ // For ChainID 138, use Blockscout API
+ if (CHAIN_ID === 138) {
+ try {
+ response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?filter=to&page=1&page_size=10`);
+ rawTransactions = Array.isArray(response?.items) ? response.items : (Array.isArray(response?.data) ? response.data : []);
+ } catch (apiErr) {
+ console.warn('Blockscout transactions API failed, trying RPC fallback:', apiErr.message);
+ try {
+ const blockNumHex = await rpcCall('eth_blockNumber');
+ const latest = parseInt(blockNumHex, 16);
+ for (let i = 0; i < Math.min(5, latest + 1) && rawTransactions.length < 10; i++) {
+ const b = await rpcCall('eth_getBlockByNumber', ['0x' + (latest - i).toString(16), true]);
+ if (b && b.transactions) {
+ b.transactions.forEach(tx => {
+ if (typeof tx === 'object' && rawTransactions.length < 10) rawTransactions.push({ hash: tx.hash, from: tx.from, to: tx.to || null, value: tx.value || '0x0', block_number: parseInt(b.number, 16), created_at: b.timestamp ? new Date(parseInt(b.timestamp, 16) * 1000).toISOString() : null });
+ });
+ }
+ }
+ response = { items: rawTransactions };
+ } catch (rpcErr) {
+ console.error('RPC transactions fallback failed:', rpcErr);
+ if (container) container.innerHTML = 'API temporarily unavailable.
';
+ 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);
+
+ // Check for new transactions
+ const currentHashes = new Set(limitedTransactions.map(tx => String(tx.hash || '')));
+ const newTransactions = limitedTransactions.filter(tx => !previousTransactionHashes.has(String(tx.hash || '')));
+
+ // Update previous hashes
+ previousTransactionHashes = currentHashes;
+
+ // Show skeleton loader only on first load
+ if (container.innerHTML.includes('skeleton') || container.innerHTML.includes('Loading')) {
+ container.innerHTML = createSkeletonLoader('table');
+ }
+
+ let html = '| Hash | From | To | Value | Block |
';
+
+ if (limitedTransactions.length === 0) {
+ html += '| No transactions found |
';
+ } else {
+ limitedTransactions.forEach((tx, index) => {
+ // Transaction is already normalized by adapter
+ const hash = String(tx.hash || 'N/A');
+ const from = String(tx.from || 'N/A');
+ const to = String(tx.to || 'N/A');
+ const value = tx.value || '0';
+ const blockNumber = tx.block_number || 'N/A';
+
+ const valueFormatted = formatEther(value);
+
+ // Add animation class for new transactions
+ const isNew = newTransactions.some(ntx => String(ntx.hash || '') === hash);
+ const animationClass = isNew ? 'new-transaction' : '';
+
+ html += '| ' + escapeHtml(shortenHash(hash)) + ' | ' + formatAddressWithLabel(from) + ' | ' + (to ? formatAddressWithLabel(to) : '-') + ' | ' + escapeHtml(valueFormatted) + ' ETH | ' + escapeHtml(String(blockNumber)) + ' |
';
+ });
+ }
+
+ html += '
';
+ 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 = 'Failed to load transactions: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '.
';
+ }
+ }
+
+ // 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;
+ 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 = ' Loading blocks...
';
+
+ let blocks = [];
+
+ // For ChainID 138, use Blockscout API
+ if (CHAIN_ID === 138) {
+ try {
+ const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks?page=${blocksListPage}&page_size=${LIST_PAGE_SIZE}`);
+ if (response && response.items) {
+ blocks = response.items.map(normalizeBlock).filter(b => b !== null);
+ }
+ } catch (error) {
+ console.error('Failed to load blocks from Blockscout:', error);
+ throw error;
+ }
+ } 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
+ }
+ }
+ }
+
+ let html = '| Block | Hash | Transactions | Timestamp |
';
+
+ if (blocks.length === 0) {
+ html += '| No blocks found |
';
+ } else {
+ blocks.forEach(function(block) {
+ var d = normalizeBlockDisplay(block);
+ html += '| ' + escapeHtml(String(d.blockNum)) + ' | ' + escapeHtml(shortenHash(d.hash)) + ' | ' + escapeHtml(String(d.txCount)) + ' | ' + escapeHtml(d.timestampFormatted) + ' |
';
+ });
+ }
+
+ var pagination = '';
+ pagination += '
Page ' + blocksListPage + '';
+ pagination += '
';
+ html += '
' + pagination;
+ container.innerHTML = html;
+ } catch (error) {
+ container.innerHTML = 'Failed to load blocks: ' + escapeHtml(error.message || 'Unknown error') + '.
';
+ }
+ }
+
+ 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 = ' Loading transactions...
';
+
+ let transactions = [];
+
+ // For ChainID 138, use Blockscout API
+ if (CHAIN_ID === 138) {
+ try {
+ const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?page=${transactionsListPage}&page_size=${LIST_PAGE_SIZE}`);
+ if (response && response.items) {
+ transactions = response.items.map(normalizeTransaction).filter(tx => tx !== null);
+ }
+ } catch (error) {
+ console.error('Failed to load transactions from Blockscout:', error);
+ throw error;
+ }
+ } 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
+ }
+ }
+ }
+
+ let html = '| Hash | From | To | Value | Block |
';
+
+ if (transactions.length === 0) {
+ html += '| No transactions found |
';
+ } else {
+ transactions.forEach(tx => {
+ const hash = String(tx.hash || 'N/A');
+ const from = String(tx.from || 'N/A');
+ const to = String(tx.to || 'N/A');
+ const value = tx.value || '0';
+ const blockNumber = tx.block_number || 'N/A';
+ const valueFormatted = formatEther(value);
+ html += '| ' + escapeHtml(shortenHash(hash)) + ' | ' + formatAddressWithLabel(from) + ' | ' + (to ? formatAddressWithLabel(to) : '-') + ' | ' + escapeHtml(valueFormatted) + ' ETH | ' + escapeHtml(String(blockNumber)) + ' |
';
+ });
+ }
+
+ var pagination = '';
+ pagination += '
Page ' + transactionsListPage + '';
+ pagination += '
';
+ html += '
' + pagination;
+ container.innerHTML = html;
+ } catch (error) {
+ container.innerHTML = 'Failed to load transactions: ' + escapeHtml(error.message || 'Unknown error') + '.
';
+ }
+ }
+
+ async function loadTokensList() {
+ var container = document.getElementById('tokensListContent');
+ if (!container) return;
+ try {
+ container.innerHTML = ' Loading tokens...
';
+ if (CHAIN_ID === 138) {
+ try {
+ var resp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/tokens?page=1&page_size=100').catch(function() { return null; });
+ var items = (resp && (resp.items || resp.data)) || (Array.isArray(resp) ? resp : null);
+ if (items && items.length > 0) {
+ var html = '| Token | Contract | Type |
';
+ items.forEach(function(t) {
+ var addr = (t.address && (t.address.hash || t.address)) || t.address_hash || t.token_address || t.contract_address_hash || '';
+ var name = t.name || t.symbol || '-';
+ var symbol = t.symbol || '';
+ var type = t.type || 'ERC-20';
+ if (!addr) return;
+ html += '| ' + escapeHtml(name) + (symbol ? ' (' + escapeHtml(symbol) + ')' : '') + ' | ' + escapeHtml(shortenHash(addr)) + ' | ' + escapeHtml(type) + ' |
';
+ });
+ html += '
';
+ container.innerHTML = html;
+ return;
+ }
+ } catch (e) {}
+ }
+ container.innerHTML = 'No token index available. Use the search bar to find tokens by name, symbol, or contract address (0x...).
';
+ } catch (err) {
+ container.innerHTML = 'Failed to load tokens. Use the search bar to find a token by address or name.
';
+ }
+ }
+ window._loadTokensList = loadTokensList;
+
+ async function refreshBridgeData() {
+ const container = document.getElementById('bridgeContent');
+ if (!container) return;
+
+ try {
+ container.innerHTML = ' Loading bridge data...
';
+
+ // Chain 138 Bridge Contracts
+ const WETH9_BRIDGE_138 = '0x971cD9D156f193df8051E48043C476e53ECd4693';
+ const WETH10_BRIDGE_138 = '0xe0E93247376aa097dB308B92e6Ba36bA015535D0';
+
+ // Ethereum Mainnet Bridge Contracts
+ const WETH9_BRIDGE_MAINNET = '0x2A0840e5117683b11682ac46f5CF5621E67269E3';
+ const WETH10_BRIDGE_MAINNET = '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03';
+
+ // Bridge routes configuration
+ const routes = {
+ weth9: {
+ 'BSC (56)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
+ 'Polygon (137)': '0xa780ef19a041745d353c9432f2a7f5a241335ffe',
+ 'Avalanche (43114)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
+ 'Base (8453)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
+ 'Arbitrum (42161)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
+ 'Optimism (10)': '0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
+ 'Ethereum Mainnet (1)': WETH9_BRIDGE_MAINNET
+ },
+ weth10: {
+ 'BSC (56)': '0x105f8a15b819948a89153505762444ee9f324684',
+ 'Polygon (137)': '0xdab0591e5e89295ffad75a71dcfc30c5625c4fa2',
+ 'Avalanche (43114)': '0x105f8a15b819948a89153505762444ee9f324684',
+ 'Base (8453)': '0x105f8a15b819948a89153505762444ee9f324684',
+ 'Arbitrum (42161)': '0x105f8a15b819948a89153505762444ee9f324684',
+ 'Optimism (10)': '0x105f8a15b819948a89153505762444ee9f324684',
+ 'Ethereum Mainnet (1)': WETH10_BRIDGE_MAINNET
+ }
+ };
+
+ // Build HTML
+ let html = `
+
+
CCIP Bridge Ecosystem
+
+ Cross-chain interoperability powered by Chainlink CCIP
+
+
+
+
+
+
+
+
+
CCIPWETH9Bridge
+
+ ${WETH9_BRIDGE_138}
+
+
+ Token: WETH9
+
+
+
+
CCIPWETH10Bridge
+
+ ${WETH10_BRIDGE_138}
+
+
+ Token: WETH10
+
+
+
+
+
+
+
+
+
+
+
+
+ | Destination Chain |
+ Chain ID |
+ Bridge Address |
+
+
+
+ `;
+
+ // Add WETH9 routes
+ for (const [chain, address] of Object.entries(routes.weth9)) {
+ const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
+ html += `
+
+ | ${chain.replace(/\s*\\(\\d+\\)/, '')} |
+ ${chainId} |
+ ${escapeHtml(shortenHash(address))} |
+
+ `;
+ }
+
+ html += `
+
+
+
+
+
+
+
+
+
+
+
+
+ | Destination Chain |
+ Chain ID |
+ Bridge Address |
+
+
+
+ `;
+
+ // Add WETH10 routes
+ for (const [chain, address] of Object.entries(routes.weth10)) {
+ const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
+ html += `
+
+ | ${chain.replace(/\s*\\(\\d+\\)/, '')} |
+ ${chainId} |
+ ${escapeHtml(shortenHash(address))} |
+
+ `;
+ }
+
+ html += `
+
+
+
+
+
+
+
+
+
+
+
CCIPWETH9Bridge
+
+ ${WETH9_BRIDGE_MAINNET}
+
+
+
+
+
CCIPWETH10Bridge
+
+ ${WETH10_BRIDGE_MAINNET}
+
+
+
+
+
+
+
+
+
Bridge Information
+
+
CCIP Bridge Ecosystem enables cross-chain transfers of WETH9 and WETH10 tokens using Chainlink CCIP (Cross-Chain Interoperability Protocol).
+
+
Supported Networks:
+
+ - Chain 138 - Source chain with both bridge contracts
+ - Ethereum Mainnet - Destination with dedicated bridge contracts
+ - BSC - Binance Smart Chain
+ - Polygon - Polygon PoS
+ - Avalanche - Avalanche C-Chain
+ - Base - Base L2
+ - Arbitrum - Arbitrum One
+ - Optimism - Optimism Mainnet
+
+
+
How to Use:
+
+ - Click on any bridge address to view detailed information and transaction history
+ - Use the bridge contracts to transfer WETH9 or WETH10 tokens between supported chains
+ - All transfers are secured by Chainlink CCIP infrastructure
+
+
+
CCIP Infrastructure:
+
+ - CCIP Router (Chain 138): 0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e
+ - CCIP Sender (Chain 138): 0x105F8A15b819948a89153505762444Ee9f324684
+
+
+
+ `;
+
+ container.innerHTML = html;
+ } catch (error) {
+ container.innerHTML = 'Failed to load bridge data: ' + escapeHtml(error.message) + '
';
+ }
+ }
+
+ function safeBlockNumber(v) { const n = String(v).replace(/[^0-9]/g, ''); return n ? n : null; }
+ function safeTxHash(v) { const s = String(v); return /^0x[a-fA-F0-9]{64}$/.test(s) ? s : null; }
+ function safeAddress(v) { const s = String(v); return /^0x[a-fA-F0-9]{40}$/i.test(s) ? s : null; }
+ async function showBlockDetail(blockNumber) {
+ const bn = safeBlockNumber(blockNumber);
+ if (!bn) { showToast('Invalid block number', 'error'); return; }
+ blockNumber = bn;
+ currentDetailKey = 'block:' + blockNumber;
+ showView('blockDetail');
+ updatePath('/block/' + blockNumber);
+ const container = document.getElementById('blockDetail');
+ updateBreadcrumb('block', blockNumber);
+ container.innerHTML = createSkeletonLoader('detail');
+
+ try {
+ let b;
+
+ // For ChainID 138, use Blockscout API directly
+ var rawBlockResponse = null;
+ if (CHAIN_ID === 138) {
+ try {
+ const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks/${blockNumber}`);
+ rawBlockResponse = response;
+ b = normalizeBlock(response);
+ if (!b) {
+ throw new Error('Block not found');
+ }
+ } catch (error) {
+ container.innerHTML = 'Failed to load block: ' + escapeHtml(error.message || 'Unknown error') + '.
';
+ 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 = `
+
+
Block #${b.number}
+
+
+
+
Block Number
+
${b.number}
+
+
+
Hash
+
${escapeHtml(b.hash || '')}
+
+
+
Parent Hash
+
${escapeHtml(b.parent_hash || '')}
+
+
+
Timestamp
+
${escapeHtml(timestamp)}
+
+
+
Miner
+
${formatAddressWithLabel(b.miner || '') || 'N/A'}
+
+
+
Transaction Count
+
${b.transaction_count || 0}
+
+
+
Gas Used
+
${formatNumber(b.gas_used || 0)} / ${formatNumber(b.gas_limit || 0)} (${gasUsedPercent}%)
+
+
+
Gas Limit
+
${formatNumber(b.gas_limit || 0)}
+
+ ${b.base_fee_per_gas ? `
+
+
Base Fee
+
${baseFeeGwei} Gwei
+
+ ` : ''}
+ ${b.burnt_fees && parseInt(b.burnt_fees) > 0 ? `
+
+
Burnt Fees
+
${burntFeesEth} ETH
+
+ ` : ''}
+
+
Size
+
${formatNumber(b.size || 0)} bytes
+
+
+
Difficulty
+
${formatNumber(b.difficulty || 0)}
+
+
+
Nonce
+
${escapeHtml(String(b.nonce || '0x0'))}
+
+ ${(rawBlockResponse && (rawBlockResponse.consensus !== undefined || rawBlockResponse.finality !== undefined || rawBlockResponse.validated !== undefined)) ? 'Finality / Consensus
' + escapeHtml(String(rawBlockResponse.consensus != null ? rawBlockResponse.consensus : (rawBlockResponse.finality != null ? rawBlockResponse.finality : rawBlockResponse.validated))) + '
' : ''}
+ `;
+ } else {
+ container.innerHTML = 'Block not found
';
+ }
+ } catch (error) {
+ container.innerHTML = 'Failed to load block: ' + escapeHtml(error.message) + '
';
+ }
+ }
+ window._showBlockDetail = showBlockDetail;
+ window.showBlockDetail = showBlockDetail;
+
+ async function showTransactionDetail(txHash) {
+ const th = safeTxHash(txHash);
+ if (!th) { showToast('Invalid transaction hash', 'error'); return; }
+ txHash = th;
+ currentDetailKey = 'tx:' + txHash;
+ showView('transactionDetail');
+ updatePath('/tx/' + txHash);
+ const container = document.getElementById('transactionDetail');
+ updateBreadcrumb('transaction', txHash);
+ container.innerHTML = createSkeletonLoader('detail');
+
+ try {
+ let t;
+ let rawTx = null;
+
+ if (CHAIN_ID === 138) {
+ try {
+ const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}`);
+ rawTx = response;
+ t = normalizeTransaction(response);
+ if (!t) throw new Error('Transaction not found');
+ } catch (error) {
+ container.innerHTML = 'Failed to load transaction: ' + escapeHtml(error.message || 'Unknown error') + '.
';
+ 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 = 'Transaction not found
';
+ return;
+ }
+
+ const timestamp = new Date(t.created_at).toLocaleString();
+ const valueEth = formatEther(t.value || '0');
+ const gasPriceGwei = t.gas_price ? (parseInt(t.gas_price) / 1e9).toFixed(2) : 'N/A';
+ const maxFeeGwei = t.max_fee_per_gas ? (parseInt(t.max_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
+ const priorityFeeGwei = t.max_priority_fee_per_gas ? (parseInt(t.max_priority_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
+ const burntFeeEth = t.tx_burnt_fee ? formatEther(t.tx_burnt_fee) : '0';
+ const totalFee = t.gas_used && t.gas_price ? formatEther((BigInt(t.gas_used) * BigInt(t.gas_price)).toString()) : '0';
+ const txType = t.type === 2 ? 'EIP-1559' : t.type === 1 ? 'EIP-2930' : 'Legacy';
+ const revertReason = t.revert_reason || (rawTx && (rawTx.revert_reason || rawTx.error || rawTx.result));
+ const inputHex = (t.input && t.input !== '0x') ? t.input : null;
+ const decodedInput = t.decoded_input || (rawTx && rawTx.decoded_input);
+ const toCellContent = t.to ? formatAddressWithLabel(t.to) + ' ' : 'N/A';
+
+ let mainHtml = `
+
+
Transaction
+
+
+
+
+
+
+
Transaction Hash
+
${escapeHtml(t.hash)}
+
+
+
+
Status
+
+
+ ${t.status === 1 ? 'Success' : t.status === 0 ? 'Failed' : 'Pending'}
+
+
+
+
+
Block Number
+
${escapeHtml(String(t.block_number || 'N/A'))}
+
+
+
Block Hash
+
${escapeHtml(t.block_hash || 'N/A')}
+
+
+
From
+
${formatAddressWithLabel(t.from || '')}
+
+
+
To
+
${toCellContent}
+
+
+
Value
+
${valueEth} ETH
+
+
+
Gas Used
+
${t.gas_used ? formatNumber(t.gas_used) : 'N/A'}
+
+
+
Gas Limit
+
${t.gas_limit ? formatNumber(t.gas_limit) : 'N/A'}
+
+ ${t.max_fee_per_gas ? `Max Fee Per Gas
${maxFeeGwei} Gwei
` : ''}
+ ${t.max_priority_fee_per_gas ? `Max Priority Fee
${priorityFeeGwei} Gwei
` : ''}
+ ${!t.max_fee_per_gas && t.gas_price ? `Gas Price
${gasPriceGwei} Gwei
` : ''}
+
+
Total Fee
+
${totalFee} ETH
+
+ ${t.tx_burnt_fee && parseInt(t.tx_burnt_fee) > 0 ? `Burnt Fee
${burntFeeEth} ETH
` : ''}
+
+
+ ${t.contract_address ? `Contract Address
${escapeHtml(t.contract_address)}
` : ''}
+ `;
+
+ if (revertReason && t.status !== 1) {
+ const reasonStr = typeof revertReason === 'string' ? revertReason : (revertReason.message || JSON.stringify(revertReason));
+ mainHtml += `
+
+
Revert Reason
+
${escapeHtml(reasonStr)}
+
+ `;
+ }
+
+ if (inputHex || decodedInput) {
+ mainHtml += `Input Data
`;
+ if (decodedInput && (decodedInput.method || decodedInput.params)) {
+ const method = decodedInput.method || decodedInput.name || 'Unknown';
+ mainHtml += `
Method: ${escapeHtml(method)}
`;
+ if (decodedInput.params && Array.isArray(decodedInput.params)) {
+ mainHtml += '
| Param | Value |
';
+ decodedInput.params.forEach(function(p) {
+ const name = (p.name || p.type || '');
+ const val = typeof p.value !== 'undefined' ? String(p.value) : (p.type || '');
+ mainHtml += '| ' + escapeHtml(name) + ' | ' + escapeHtml(val) + ' |
';
+ });
+ mainHtml += '
';
+ }
+ }
+ if (inputHex) {
+ mainHtml += `
Hex: ${escapeHtml(inputHex)}
`;
+ }
+ mainHtml += '
';
+ }
+
+ container.innerHTML = mainHtml;
+
+ if (CHAIN_ID === 138) {
+ const internalCard = document.createElement('div');
+ internalCard.className = 'card';
+ internalCard.style.marginTop = '1rem';
+ internalCard.innerHTML = ' Internal Transactions
Loading...
';
+ container.appendChild(internalCard);
+
+ const logsCard = document.createElement('div');
+ logsCard.className = 'card';
+ logsCard.style.marginTop = '1rem';
+ logsCard.innerHTML = ' Event Logs
Loading...
';
+ container.appendChild(logsCard);
+
+ Promise.all([
+ fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/internal-transactions`).catch(function() { return { items: [] }; }),
+ fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/internal_transactions`).catch(function() { return { items: [] }; }),
+ fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/logs`).catch(function() { return { items: [] }; }),
+ fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/log_entries`).catch(function() { return { items: [] }; })
+ ]).then(function(results) {
+ const internalResp = results[0].items ? results[0] : results[1];
+ const logsResp = results[2].items ? results[2] : results[3];
+ const internals = internalResp.items || [];
+ const logs = logsResp.items || logsResp.log_entries || [];
+
+ const internalEl = document.getElementById('txInternalTxs');
+ if (internalEl) {
+ if (internals.length === 0) {
+ internalEl.innerHTML = 'No internal transactions
';
+ } else {
+ let tbl = '| Type | From | To | Value |
';
+ internals.forEach(function(it) {
+ const from = it.from?.hash || it.from || 'N/A';
+ const to = it.to?.hash || it.to || 'N/A';
+ const val = it.value ? formatEther(it.value) : '0';
+ const type = it.type || it.call_type || 'call';
+ tbl += '| ' + escapeHtml(type) + ' | ' + escapeHtml(shortenHash(from)) + ' | ' + escapeHtml(shortenHash(to)) + ' | ' + escapeHtml(val) + ' ETH |
';
+ });
+ tbl += '
';
+ internalEl.innerHTML = tbl;
+ }
+ }
+
+ const logsEl = document.getElementById('txLogs');
+ if (logsEl) {
+ if (logs.length === 0) {
+ logsEl.innerHTML = 'No event logs
';
+ } else {
+ let tbl = '| Address | Topics | Data | Decoded |
';
+ logs.forEach(function(log, idx) {
+ const addr = log.address?.hash || log.address || 'N/A';
+ const topics = (log.topics && Array.isArray(log.topics)) ? log.topics : (log.topic0 ? [log.topic0] : []);
+ const topicsStr = topics.join(', ');
+ const data = log.data || log.raw_data || '0x';
+ tbl += '| ' + escapeHtml(shortenHash(addr)) + ' | ' + escapeHtml(String(topicsStr).substring(0, 80)) + (String(topicsStr).length > 80 ? '...' : '') + ' | ' + escapeHtml(String(data).substring(0, 66)) + (String(data).length > 66 ? '...' : '') + ' | — |
';
+ });
+ tbl += '
';
+ logsEl.innerHTML = tbl;
+ if (typeof ethers !== 'undefined' && ethers.utils) {
+ (function(logsList, txHash) {
+ var addrs = [];
+ logsList.forEach(function(l) { var a = l.address && (l.address.hash || l.address) || l.address; if (a && addrs.indexOf(a) === -1) addrs.push(a); });
+ var abiCache = {};
+ Promise.all(addrs.map(function(addr) {
+ if (!/^0x[a-f0-9]{40}$/i.test(addr)) return Promise.resolve();
+ return fetch(BLOCKSCOUT_API + '/v2/smart-contracts/' + addr).then(function(r) { return r.json(); }).catch(function() { return null; }).then(function(res) {
+ var abi = res && (res.abi || res.abi_json);
+ if (abi) abiCache[addr.toLowerCase()] = Array.isArray(abi) ? abi : (typeof abi === 'string' ? JSON.parse(abi) : abi);
+ });
+ })).then(function() {
+ logsList.forEach(function(log, idx) {
+ var addr = (log.address && (log.address.hash || log.address)) || log.address;
+ var topics = log.topics && Array.isArray(log.topics) ? log.topics : (log.topic0 ? [log.topic0] : []);
+ var data = log.data || log.raw_data || '0x';
+ var abi = addr ? abiCache[(addr + '').toLowerCase()] : null;
+ var decodedEl = document.getElementById('txLogDecoded' + idx);
+ if (!decodedEl || !abi) return;
+ try {
+ var iface = new ethers.utils.Interface(abi);
+ var parsed = iface.parseLog({ topics: topics, data: data });
+ if (parsed && parsed.name) {
+ var args = parsed.args && parsed.args.length ? parsed.args.map(function(a) { return String(a); }).join(', ') : '';
+ decodedEl.textContent = parsed.name + '(' + args + ')';
+ decodedEl.title = parsed.signature || '';
+ }
+ } catch (e) {}
+ });
+ });
+ })(logs, txHash);
+ }
+ }
+ }
+ });
+ }
+ } catch (error) {
+ container.innerHTML = 'Failed to load transaction: ' + escapeHtml(error.message) + '
';
+ }
+ }
+ function escapeHtml(str) {
+ if (str == null) return '';
+ const s = String(str);
+ const div = document.createElement('div');
+ div.textContent = s;
+ return div.innerHTML;
+ }
+ function exportTransactionCSV(txHash) {
+ fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions/' + txHash).then(function(r) {
+ var t = normalizeTransaction(r);
+ if (!t) return;
+ var rows = [['Field', 'Value'], ['hash', t.hash], ['from', t.from], ['to', t.to || ''], ['value', t.value || '0'], ['block_number', t.block_number || ''], ['status', t.status], ['gas_used', t.gas_used || ''], ['gas_limit', t.gas_limit || '']];
+ var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
+ var blob = new Blob([csv], { type: 'text/csv' });
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement('a'); a.href = url; a.download = 'transaction-' + txHash.substring(0, 10) + '.csv'; a.click();
+ URL.revokeObjectURL(url);
+ }).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
+ }
+ function exportBlocksCSV() {
+ fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/blocks?page=1&page_size=50').then(function(r) {
+ var items = r.items || r || [];
+ 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() {
+ fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions?page=1&page_size=50').then(function(r) {
+ var items = r.items || r || [];
+ var rows = [['Hash', 'From', 'To', 'Value', 'Block']];
+ 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 || '')]);
+ });
+ var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
+ var blob = new Blob([csv], { type: 'text/csv' });
+ var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'transactions.csv'; a.click();
+ URL.revokeObjectURL(a.href);
+ }).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
+ }
+ function exportAddressTransactionsCSV(addr) {
+ if (!addr || !/^0x[a-fA-F0-9]{40}$/i.test(addr)) { showToast('Invalid address', 'error'); return; }
+ fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions?address=' + encodeURIComponent(addr) + '&page=1&page_size=100').then(function(r) {
+ var items = r.items || r || [];
+ var rows = [['Hash', 'From', 'To', 'Value', 'Block', 'Status']];
+ items.forEach(function(tx) {
+ var t = normalizeTransaction(tx);
+ if (t) rows.push([t.hash || '', t.from || '', t.to || '', t.value || '0', String(t.block_number || ''), t.status === 1 ? 'Success' : 'Failed']);
+ });
+ var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
+ var blob = new Blob([csv], { type: 'text/csv' });
+ var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'address-' + addr.substring(0, 10) + '-transactions.csv'; a.click();
+ URL.revokeObjectURL(a.href);
+ showToast('CSV downloaded', 'success');
+ }).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
+ }
+ window.exportAddressTransactionsCSV = exportAddressTransactionsCSV;
+ function exportAddressTokenBalancesCSV(addr) {
+ if (!addr || !/^0x[a-fA-F0-9]{40}$/i.test(addr)) { showToast('Invalid address', 'error'); return; }
+ fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/token-balances').catch(function() { return { items: [] }; }).then(function(r) {
+ var items = Array.isArray(r) ? r : (r.items || r || []);
+ var rows = [['Token', 'Contract', 'Balance', 'Type']];
+ (items || []).forEach(function(b) {
+ var token = b.token || b;
+ var contract = token.address?.hash || token.address || b.token_contract_address_hash || 'N/A';
+ var symbol = token.symbol || token.name || '-';
+ var balance = b.value || b.balance || '0';
+ var decimals = token.decimals != null ? token.decimals : 18;
+ var divisor = Math.pow(10, parseInt(decimals, 10));
+ var displayBalance = (Number(balance) / divisor).toLocaleString(undefined, { maximumFractionDigits: 6 });
+ var type = token.type || b.token_type || 'ERC-20';
+ rows.push([symbol, contract, displayBalance, type]);
+ });
+ var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
+ var blob = new Blob([csv], { type: 'text/csv' });
+ var a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'address-' + addr.substring(0, 10) + '-token-balances.csv'; a.click();
+ URL.revokeObjectURL(a.href);
+ showToast('CSV downloaded', 'success');
+ }).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
+ }
+ window.exportAddressTokenBalancesCSV = exportAddressTokenBalancesCSV;
+ window._showTransactionDetail = showTransactionDetail;
+ window.showTransactionDetail = showTransactionDetail;
+
+ async function showAddressDetail(address) {
+ const addr = safeAddress(address);
+ if (!addr) { showToast('Invalid address', 'error'); return; }
+ address = addr;
+ currentDetailKey = 'address:' + address.toLowerCase();
+ showView('addressDetail');
+ updatePath('/address/' + address);
+ const container = document.getElementById('addressDetail');
+ updateBreadcrumb('address', address);
+ container.innerHTML = createSkeletonLoader('detail');
+
+ try {
+ // Validate address format
+ if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
+ container.innerHTML = 'Invalid address format
';
+ return;
+ }
+
+ let a;
+
+ // For ChainID 138, use Blockscout API directly
+ if (CHAIN_ID === 138) {
+ try {
+ const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/addresses/${address}`);
+ var raw = response && (response.data !== undefined ? response.data : response.address !== undefined ? response.address : response.items && response.items[0] !== undefined ? response.items[0] : response);
+ a = normalizeAddress(raw);
+ if (!a || !a.hash) {
+ throw new Error('Address not found');
+ }
+ } catch (error) {
+ container.innerHTML = 'Failed to load address: ' + escapeHtml(error.message || 'Unknown error') + '.
';
+ return;
+ }
+ } else {
+ const addr = await fetchAPIWithRetry(`${API_BASE}/v1/addresses/138/${address}`);
+ if (addr.data) {
+ a = addr.data;
+ } else {
+ throw new Error('Address not found');
+ }
+ }
+
+ if (a) {
+ const balanceEth = formatEther(a.balance || '0');
+ const isContract = !!a.is_contract;
+ const verifiedBadge = a.is_verified ? 'Verified' : '';
+ const contractLink = isContract ? `View contract on Blockscout` : '';
+ const savedLabel = getAddressLabel(address);
+ const inWatchlist = isInWatchlist(address);
+
+ container.innerHTML = `
+
+
+
+
Watchlist
+
+
+
+
+
Balance
+
${balanceEth} ETH
+
+
+
Transaction Count
+
${a.transaction_count || 0}
+
+
+
Token Count
+
${a.token_count || 0}
+
+
+
Type
+
${a.is_contract ? 'Contract' + verifiedBadge + (contractLink ? '
' + contractLink : '') : 'EOA'}
+
+ ${a.creation_tx_hash ? `Contract created in
${escapeHtml(shortenHash(a.creation_tx_hash))}
` : ''}
+ ${a.first_seen_at ? `First seen
${escapeHtml(typeof a.first_seen_at === 'string' ? a.first_seen_at : new Date(a.first_seen_at).toISOString())}
` : ''}
+ ${a.last_seen_at ? `Last seen
${escapeHtml(typeof a.last_seen_at === 'string' ? a.last_seen_at : new Date(a.last_seen_at).toISOString())}
` : ''}
+
+
+
+
+
+ ${isContract ? '' : ''}
+
+
+
+
Recent Transactions
+
+
+
Loading transactions...
+
+
+
+
Token Balances
+
+
+
Loading...
+
+
+
Internal Transactions
+
Loading...
+
+
+
NFT Inventory
+
Loading...
+
+ ${isContract ? 'Contract ABI & Bytecode
Loading...
' : ''}
+ `;
+
+ function switchAddressTab(tabName, addr) {
+ document.querySelectorAll('.address-tab-content').forEach(function(el) { el.style.display = 'none'; });
+ document.querySelectorAll('.tabs .tab').forEach(function(t) { t.classList.remove('active'); });
+ if (tabName === 'transactions') {
+ document.getElementById('addressTabTransactions').style.display = 'block';
+ document.getElementById('addrTabTxs').classList.add('active');
+ } else if (tabName === 'tokens') {
+ document.getElementById('addressTabTokens').style.display = 'block';
+ document.getElementById('addrTabTokens').classList.add('active');
+ loadAddressTokenBalances(addr);
+ } else if (tabName === 'internal') {
+ document.getElementById('addressTabInternal').style.display = 'block';
+ document.getElementById('addrTabInternal').classList.add('active');
+ loadAddressInternalTxns(addr);
+ } else if (tabName === 'nfts') {
+ document.getElementById('addressTabNfts').style.display = 'block';
+ document.getElementById('addrTabNfts').classList.add('active');
+ loadAddressNftInventory(addr);
+ } else if (tabName === 'contract') {
+ var contractPanel = document.getElementById('addressTabContract');
+ var contractTab = document.getElementById('addrTabContract');
+ if (contractPanel && contractTab) {
+ contractPanel.style.display = 'block';
+ contractTab.classList.add('active');
+ loadAddressContractInfo(addr);
+ }
+ }
+ }
+ window.switchAddressTab = switchAddressTab;
+
+ async function loadAddressTokenBalances(addr) {
+ const el = document.getElementById('addressTokenBalances');
+ if (!el || el.dataset.loaded === '1') return;
+ try {
+ const r = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/token-balances').catch(function() { return { items: [] }; });
+ const r2 = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/token_balances').catch(function() { return { items: [] }; });
+ const items = (r.items || r).length ? (r.items || r) : (r2.items || r2);
+ el.dataset.loaded = '1';
+ if (!items || items.length === 0) {
+ el.innerHTML = 'No token balances
';
+ return;
+ }
+ let tbl = '| Token | Contract | Balance | Type |
';
+ items.forEach(function(b) {
+ const token = b.token || b;
+ const contract = token.address?.hash || token.address || b.token_contract_address_hash || 'N/A';
+ const symbol = token.symbol || token.name || '-';
+ const balance = b.value || b.balance || '0';
+ const decimals = token.decimals != null ? token.decimals : 18;
+ const divisor = Math.pow(10, parseInt(decimals, 10));
+ const displayBalance = (Number(balance) / divisor).toLocaleString(undefined, { maximumFractionDigits: 6 });
+ const type = token.type || b.token_type || 'ERC-20';
+ tbl += '| ' + escapeHtml(symbol) + ' | ' + escapeHtml(shortenHash(contract)) + ' | ' + escapeHtml(displayBalance) + ' | ' + escapeHtml(type) + ' |
';
+ });
+ tbl += '
';
+ el.innerHTML = tbl;
+ } catch (e) {
+ el.innerHTML = 'Failed to load token balances
';
+ }
+ }
+
+ 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';
+ if (!items || items.length === 0) {
+ el.innerHTML = 'No NFT tokens
';
+ return;
+ }
+ var tbl = '| Contract | Token ID | Name / Symbol | Balance |
';
+ items.forEach(function(b) {
+ var token = b.token || b;
+ var contract = token.address?.hash || token.address || b.token_contract_address_hash || b.contract_address_hash || 'N/A';
+ var tokenId = b.token_id != null ? b.token_id : (b.tokenId != null ? b.tokenId : (b.id != null ? b.id : '-'));
+ var name = token.name || token.symbol || '-';
+ var balance = b.value != null ? b.value : (b.balance != null ? b.balance : '1');
+ tbl += '| ' + escapeHtml(shortenHash(contract)) + ' | ';
+ tbl += '' + (tokenId !== '-' ? '' + escapeHtml(String(tokenId)) + '' : '-') + ' | ';
+ tbl += '' + escapeHtml(name) + ' | ' + escapeHtml(String(balance)) + ' |
';
+ });
+ tbl += '
';
+ el.innerHTML = tbl;
+ } catch (e) {
+ el.innerHTML = 'Failed to load NFT inventory
';
+ }
+ }
+
+ 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';
+ if (!items || items.length === 0) {
+ el.innerHTML = 'No internal transactions
';
+ return;
+ }
+ let tbl = '| Block | From | To | Value | Tx Hash |
';
+ items.slice(0, 25).forEach(function(it) {
+ const from = it.from?.hash || it.from || 'N/A';
+ const to = it.to?.hash || it.to || 'N/A';
+ const val = it.value ? formatEther(it.value) : '0';
+ const block = it.block_number || it.block || '-';
+ const txHash = it.transaction_hash || it.tx_hash || '-';
+ tbl += '| ' + escapeHtml(block) + ' | ' + escapeHtml(shortenHash(from)) + ' | ' + escapeHtml(shortenHash(to)) + ' | ' + escapeHtml(val) + ' ETH | ' + (txHash !== '-' ? escapeHtml(shortenHash(txHash)) : '-') + ' |
';
+ });
+ tbl += '
';
+ el.innerHTML = tbl;
+ } catch (e) {
+ el.innerHTML = 'Failed to load internal transactions
';
+ }
+ }
+
+ async function loadAddressContractInfo(addr) {
+ const el = document.getElementById('addressContractInfo');
+ if (!el || el.dataset.loaded === '1') return;
+ try {
+ const urls = [
+ BLOCKSCOUT_API + '/v2/smart-contracts/' + addr,
+ BLOCKSCOUT_API + '/v2/contracts/' + addr
+ ];
+ let data = null;
+ for (var i = 0; i < urls.length; i++) {
+ try {
+ const r = await fetchAPIWithRetry(urls[i]);
+ if (r && (r.abi || r.bytecode || r.deployed_bytecode)) { data = r; break; }
+ } catch (e) {}
+ }
+ el.dataset.loaded = '1';
+ if (!data) {
+ el.innerHTML = 'Contract source not indexed. Verify on Blockscout
';
+ 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 = 'Verification: ' + (data.is_verified ? 'Verified' : 'Unverified') + '
';
+ html += '';
+ html += '
' + escapeHtml(abiStr) + '
';
+ html += 'Bytecode
';
+ html += '
' + escapeHtml(String(bytecode).substring(0, 500)) + (String(bytecode).length > 500 ? '...' : '') + '
';
+ 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 += ' Read contract
Call view/pure functions (requires ethers.js).
';
+ html += '
';
+ html += '
';
+ }
+ 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 += ' Write contract
Connect wallet to send transactions.
';
+ html += '
';
+ html += '
';
+ html += '
';
+ }
+ html += 'Read / Write contract on Blockscout
';
+ 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 = '';
+ inputs.forEach(function(inp, i) {
+ h += '';
+ });
+ h += '
';
+ 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 = '';
+ inputs.forEach(function(inp, i) {
+ h += '';
+ });
+ h += '
';
+ 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 = 'Failed to load contract info
';
+ }
+ }
+
+ 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) {
+ if (txs.data && txs.data.length > 0) {
+ let txHtml = '| Hash | Block | From | To | Value |
';
+ txs.data.forEach(function(tx) {
+ txHtml += '| ' + escapeHtml(shortenHash(tx.hash)) + ' | ' + escapeHtml(String(tx.block_number)) + ' | ' + formatAddressWithLabel(tx.from) + ' | ' + (tx.to ? formatAddressWithLabel(tx.to) : 'N/A') + ' | ' + escapeHtml(formatEther(tx.value || '0')) + ' ETH |
';
+ });
+ txHtml += '
';
+ txContainer.innerHTML = txHtml;
+ } else {
+ txContainer.innerHTML = 'No transactions found
';
+ }
+ }
+ } catch (e) {
+ const txContainer = document.getElementById('addressTransactions');
+ if (txContainer) txContainer.innerHTML = 'Failed to load transactions
';
+ }
+ } else {
+ container.innerHTML = 'Address not found
';
+ }
+ } catch (error) {
+ container.innerHTML = 'Failed to load address: ' + escapeHtml(error.message) + '
';
+ }
+ }
+ window._showAddressDetail = showAddressDetail;
+ window.showAddressDetail = showAddressDetail;
+
+ async function showTokenDetail(tokenAddress) {
+ if (!/^0x[a-fA-F0-9]{40}$/.test(tokenAddress)) return;
+ currentDetailKey = 'token:' + tokenAddress.toLowerCase();
+ showView('tokenDetail');
+ updatePath('/token/' + tokenAddress);
+ var container = document.getElementById('tokenDetail');
+ updateBreadcrumb('token', tokenAddress);
+ container.innerHTML = createSkeletonLoader('detail');
+
+ try {
+ var urls = [BLOCKSCOUT_API + '/v2/tokens/' + tokenAddress, BLOCKSCOUT_API + '/v2/token/' + tokenAddress];
+ var data = null;
+ for (var i = 0; i < urls.length; i++) {
+ try {
+ var r = await fetchAPIWithRetry(urls[i]);
+ if (r && (r.symbol || r.name || r.total_supply != null)) { data = r; break; }
+ } catch (e) {}
+ }
+ if (!data) {
+ container.innerHTML = 'Token not found or not indexed.
View as address
';
+ return;
+ }
+ var name = data.name || '-';
+ var symbol = data.symbol || '-';
+ var decimals = data.decimals != null ? data.decimals : 18;
+ var supply = data.total_supply != null ? data.total_supply : (data.total_supply_raw || '0');
+ var supplyNum = Number(supply) / Math.pow(10, parseInt(decimals, 10));
+ var holders = data.holders_count != null ? data.holders_count : (data.holder_count || '-');
+ var transfersResp = null;
+ try {
+ transfersResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/tokens/' + tokenAddress + '/transfers?page=1&page_size=10').catch(function() { return { items: [] }; });
+ } catch (e) {}
+ var transfers = (transfersResp && transfersResp.items) ? transfersResp.items : [];
+ var html = 'Contract
' + escapeHtml(tokenAddress) + '
';
+ html += 'Name
' + escapeHtml(name) + '
';
+ html += 'Symbol
' + escapeHtml(symbol) + '
';
+ html += '';
+ html += 'Total Supply
' + supplyNum.toLocaleString(undefined, { maximumFractionDigits: 6 }) + '
';
+ html += 'Holders
' + (holders !== '-' ? formatNumber(holders) : '-') + '
';
+ html += 'Recent Transfers
';
+ if (transfers.length === 0) {
+ html += '
No transfers
';
+ } else {
+ html += '
| From | To | Value | Tx |
';
+ transfers.forEach(function(tr) {
+ var from = tr.from?.hash || tr.from || '-';
+ var to = tr.to?.hash || tr.to || '-';
+ var val = tr.total?.value != null ? tr.total.value : (tr.value || '0');
+ var dec = tr.token?.decimals != null ? tr.token.decimals : decimals;
+ var v = Number(val) / Math.pow(10, parseInt(dec, 10));
+ var txHash = tr.transaction_hash || tr.tx_hash || '';
+ html += '| ' + escapeHtml(shortenHash(from)) + ' | ' + escapeHtml(shortenHash(to)) + ' | ' + escapeHtml(v.toLocaleString(undefined, { maximumFractionDigits: 6 })) + ' | ' + (txHash ? escapeHtml(shortenHash(txHash)) : '-') + ' |
';
+ });
+ html += '
';
+ }
+ html += '
';
+ container.innerHTML = html;
+ } catch (err) {
+ container.innerHTML = 'Failed to load token: ' + escapeHtml(err.message || 'Unknown') + '
';
+ }
+ }
+ window.showTokenDetail = showTokenDetail;
+
+ async function showNftDetail(contractAddress, tokenId) {
+ if (!/^0x[a-fA-F0-9]{40}$/.test(contractAddress)) return;
+ currentDetailKey = 'nft:' + contractAddress.toLowerCase() + ':' + tokenId;
+ showView('nftDetail');
+ updatePath('/nft/' + contractAddress + '/' + tokenId);
+ var container = document.getElementById('nftDetail');
+ updateBreadcrumb('nft', contractAddress, tokenId);
+ container.innerHTML = createSkeletonLoader('detail');
+
+ try {
+ var urls = [BLOCKSCOUT_API + '/v2/tokens/' + contractAddress + '/nft/' + tokenId, BLOCKSCOUT_API + '/v2/nft/' + contractAddress + '/' + tokenId];
+ var data = null;
+ for (var i = 0; i < urls.length; i++) {
+ try {
+ var r = await fetchAPIWithRetry(urls[i]);
+ if (r) { data = r; break; }
+ } catch (e) {}
+ }
+ var html = 'Contract
' + escapeHtml(contractAddress) + '
';
+ html += 'Token ID
' + escapeHtml(String(tokenId)) + '
';
+ if (data) {
+ if (data.metadata && data.metadata.image) {
+ html += '';
+ }
+ if (data.name) html += 'Name
' + escapeHtml(data.name) + '
';
+ if (data.description) html += 'Description
' + escapeHtml(data.description) + '
';
+ if (data.owner) { var ownerAddr = (data.owner.hash || data.owner); html += 'Owner
' + escapeHtml(ownerAddr) + '
'; }
+ if (data.metadata && data.metadata.attributes && Array.isArray(data.metadata.attributes)) {
+ html += 'Traits
';
+ data.metadata.attributes.forEach(function(attr) {
+ var traitType = (attr.trait_type || attr.traitType || ''); var val = (attr.value != null ? attr.value : '');
+ if (traitType || val) html += '' + escapeHtml(traitType) + ': ' + escapeHtml(String(val)) + '';
+ });
+ html += '
';
+ }
+ }
+ html += 'View on Blockscout
';
+ container.innerHTML = html;
+ } catch (err) {
+ container.innerHTML = 'Failed to load NFT: ' + escapeHtml(err.message || 'Unknown') + '
';
+ }
+ }
+ window.showNftDetail = showNftDetail;
+
+ function showSearchResultsList(items, query) {
+ showView('searchResults');
+ var container = document.getElementById('searchResultsContent');
+ if (!container) return;
+ var html = 'Found ' + items.length + ' result(s) for "' + escapeHtml(query) + '". Click a row to open.
';
+ html += '| Type | Value |
';
+ 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 += '| Token | ' + escapeHtml(shortenHash(tokenAddr)) + ' ' + (item.name || item.symbol ? ' (' + escapeHtml(item.name || item.symbol) + ')' : '') + ' |
';
+ return;
+ }
+ }
+ if (item.address_hash || item.hash) {
+ addr = item.address_hash || item.hash;
+ if (/^0x[a-f0-9]{40}$/i.test(addr)) {
+ html += '| Address | ' + escapeHtml(shortenHash(addr)) + ' |
';
+ return;
+ }
+ }
+ if (item.tx_hash || (item.hash && item.hash.length === 66)) {
+ txHash = item.tx_hash || item.hash;
+ if (/^0x[a-f0-9]{64}$/i.test(txHash)) {
+ html += '| Transaction | ' + escapeHtml(shortenHash(txHash)) + ' |
';
+ return;
+ }
+ }
+ if (item.block_number != null) {
+ blockNum = String(item.block_number);
+ html += '| Block | #' + escapeHtml(blockNum) + ' |
';
+ return;
+ }
+ html += '| ' + escapeHtml(type || 'Unknown') + ' | ' + escapeHtml(String(label).substring(0, 80)) + ' |
';
+ });
+ html += '
';
+ container.innerHTML = html;
+ }
+ window.showSearchResultsList = showSearchResultsList;
+
+ async function handleSearch(query) {
+ query = query.trim();
+ if (!query) {
+ showToast('Please enter a search query', 'info');
+ return;
+ }
+
+ const normalizedQuery = query.toLowerCase().replace(/\s/g, '');
+
+ try {
+ if (/^0x[a-f0-9]{40}$/i.test(normalizedQuery)) {
+ await showAddressDetail(normalizedQuery);
+ return;
+ }
+ if (/^0x[a-f0-9]{64}$/i.test(normalizedQuery)) {
+ await showTransactionDetail(normalizedQuery);
+ return;
+ }
+ if (/^\d+$/.test(query)) {
+ await showBlockDetail(query);
+ return;
+ }
+ if (/^0x[a-f0-9]+$/i.test(normalizedQuery)) {
+ const blockNum = parseInt(normalizedQuery, 16);
+ if (!isNaN(blockNum)) {
+ await showBlockDetail(blockNum.toString());
+ return;
+ }
+ }
+
+ if (CHAIN_ID === 138) {
+ var searchResults = null;
+ try {
+ searchResults = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/search?q=' + encodeURIComponent(query));
+ } catch (e) {}
+ if (searchResults && searchResults.items && searchResults.items.length > 0) {
+ showSearchResultsList(searchResults.items, query);
+ return;
+ }
+ if (/^0x[a-f0-9]{8,64}$/i.test(normalizedQuery)) {
+ try {
+ var txResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions/' + normalizedQuery);
+ if (txResp && (txResp.hash || txResp.tx_hash)) {
+ var fullHash = txResp.hash || txResp.tx_hash;
+ await showTransactionDetail(fullHash);
+ return;
+ }
+ } catch (e) {}
+ showToast('No unique result for partial hash. Use at least 0x + 8 hex characters, or full tx hash (0x + 64 hex).', 'info');
+ return;
+ }
+ }
+
+ showToast('Invalid search. Try address (0x...40 hex), tx hash (0x...64 hex or 0x+8 hex), block number, or token/contract name.', 'error');
+ } catch (error) {
+ console.error('Search error:', error);
+ showToast('Failed to load search results: ' + (error.message || 'Unknown error'), 'error');
+ }
+ }
+ window.handleSearch = handleSearch;
+
+ function getTimeAgo(date) {
+ if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
+ return 'N/A';
+ }
+
+ const now = new Date();
+ const diffMs = now - date;
+ const diffSecs = Math.floor(diffMs / 1000);
+ const diffMins = Math.floor(diffSecs / 60);
+ const diffHours = Math.floor(diffMins / 60);
+ const diffDays = Math.floor(diffHours / 24);
+
+ if (diffSecs < 60) {
+ return `${diffSecs}s ago`;
+ } else if (diffMins < 60) {
+ return `${diffMins}m ago`;
+ } else if (diffHours < 24) {
+ return `${diffHours}h ago`;
+ } else if (diffDays < 7) {
+ return `${diffDays}d ago`;
+ } else {
+ return date.toLocaleDateString();
+ }
+ }
+
+ function formatNumber(num) {
+ return parseInt(num || 0).toLocaleString();
+ }
+
+ function shortenHash(hash, length = 10) {
+ if (!hash) return 'N/A';
+ // Convert to string if it's not already
+ const hashStr = String(hash);
+ if (hashStr.length <= length * 2 + 2) return hashStr;
+ return hashStr.substring(0, length + 2) + '...' + hashStr.substring(hashStr.length - length);
+ }
+
+ function formatEther(wei, unit = 'ether') {
+ if (typeof wei === 'string' && wei.startsWith('0x')) {
+ wei = BigInt(wei);
+ }
+ const weiNum = typeof wei === 'bigint' ? Number(wei) : parseFloat(wei);
+ const ether = weiNum / Math.pow(10, unit === 'gwei' ? 9 : 18);
+ return ether.toFixed(6).replace(/\.?0+$/, '');
+ }
+
+ // Export functions
+ function exportBlockData(blockNumber) {
+ // Fetch block data and export as JSON
+ fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks/${blockNumber}`)
+ .then(response => {
+ const block = normalizeBlock(response);
+ const dataStr = JSON.stringify(block, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `block-${blockNumber}.json`;
+ link.click();
+ URL.revokeObjectURL(url);
+ })
+ .catch(error => {
+ alert('Failed to export block data: ' + error.message);
+ });
+ }
+
+ function exportTransactionData(txHash) {
+ // Fetch transaction data and export as JSON
+ fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}`)
+ .then(response => {
+ const tx = normalizeTransaction(response);
+ const dataStr = JSON.stringify(tx, null, 2);
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(dataBlob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `transaction-${txHash.substring(0, 10)}.json`;
+ link.click();
+ URL.revokeObjectURL(url);
+ })
+ .catch(error => {
+ alert('Failed to export transaction data: ' + error.message);
+ });
+ }
+
+ // Global error handler
+ window.addEventListener('error', (event) => {
+ console.error('Global error:', event.error);
+ if (typeof showToast === 'function') {
+ showToast('An error occurred. Please refresh the page.', 'error');
+ }
+ });
+
+ window.addEventListener('unhandledrejection', (event) => {
+ console.error('Unhandled promise rejection:', event.reason);
+ if (typeof showToast === 'function') {
+ showToast('A network error occurred. Please try again.', 'error');
+ }
+ });
+
+ function setLiveRegion(text) {
+ var el = document.getElementById('explorerLiveRegion');
+ if (el) el.textContent = text || '';
+ }
+ // Toast notification function
+ function showToast(message, type = 'info', duration = 3000) {
+ if (type === 'error') setLiveRegion(message);
+ const toast = document.createElement('div');
+ toast.className = `toast toast-${type}`;
+ toast.style.cssText = `
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ padding: 1rem 1.5rem;
+ background: ${type === 'error' ? '#fee2e2' : type === 'success' ? '#d1fae5' : '#dbeafe'};
+ color: ${type === 'error' ? '#ef4444' : type === 'success' ? '#10b981' : '#3b82f6'};
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
+ z-index: 10000;
+ animation: slideIn 0.3s ease-out;
+ `;
+ toast.textContent = message;
+ document.body.appendChild(toast);
+
+ setTimeout(() => {
+ toast.style.animation = 'slideOut 0.3s ease-out';
+ setTimeout(() => toast.remove(), 300);
+ }, duration);
+ }
+
+ // Add CSS for toast animations
+ const style = document.createElement('style');
+ style.textContent = `
+ @keyframes slideIn {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+ }
+ @keyframes slideOut {
+ from { transform: translateX(0); opacity: 1; }
+ to { transform: translateX(100%); opacity: 0; }
+ }
+ `;
+ document.head.appendChild(style);
+
+ // Search input and button handlers; mobile nav close on link click
+ document.addEventListener('DOMContentLoaded', () => {
+ const searchInput = document.getElementById('searchInput');
+ const searchBtn = document.getElementById('searchBtn');
+ if (searchInput) {
+ searchInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ handleSearch(e.target.value);
+ }
+ });
+ }
+ if (searchBtn) {
+ searchBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ if (searchInput) handleSearch(searchInput.value);
+ });
+ }
+ var navLinks = document.getElementById('navLinks');
+ if (navLinks) {
+ navLinks.addEventListener('click', function(e) {
+ if (e.target.closest('a')) closeNavMenu();
+ });
+ initNavDropdowns();
+ }
+ });
diff --git a/frontend/public/index.html b/frontend/public/index.html
index 5cc4c95..d420696 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -6,7 +6,7 @@
-
+
SolaceScanScout | The Defi Oracle Meta Explorer | d-bis.org
@@ -179,15 +179,67 @@
}
.nav-links {
display: flex;
- gap: 2rem;
+ gap: 0.5rem;
list-style: none;
+ align-items: center;
}
- .nav-links a {
+ .nav-links > li { position: relative; }
+ .nav-links a, .nav-dropdown-trigger {
color: white;
text-decoration: none;
transition: opacity 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.35rem;
}
- .nav-links a:hover { opacity: 0.8; }
+ .nav-links a:hover, .nav-dropdown-trigger:hover { opacity: 0.9; }
+ .nav-dropdown-trigger {
+ background: none;
+ border: none;
+ cursor: pointer;
+ font: inherit;
+ padding: 0.5rem 0.25rem;
+ }
+ .nav-dropdown-trigger i.fa-chevron-down {
+ font-size: 0.7rem;
+ transition: transform 0.2s;
+ }
+ .nav-dropdown.open .nav-dropdown-trigger i.fa-chevron-down { transform: rotate(180deg); }
+ .nav-dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ min-width: 200px;
+ background: rgba(30, 41, 59, 0.98);
+ border-radius: 8px;
+ box-shadow: 0 10px 40px rgba(0,0,0,0.25);
+ padding: 0.5rem 0;
+ list-style: none;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-6px);
+ transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
+ z-index: 1001;
+ margin-top: 0.25rem;
+ }
+ .nav-dropdown.open .nav-dropdown-menu {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ }
+ .nav-dropdown-menu a {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 1rem;
+ color: rgba(255,255,255,0.95);
+ white-space: nowrap;
+ }
+ .nav-dropdown-menu a:hover {
+ background: rgba(255,255,255,0.1);
+ opacity: 1;
+ }
+ .nav-dropdown-menu li { margin: 0; }
.search-box {
flex: 1;
max-width: 600px;
@@ -725,15 +777,25 @@
.nav-toggle:focus { outline: 2px solid rgba(255,255,255,0.5); }
@media (max-width: 900px) {
.nav-toggle { display: block; }
- .nav-links { display: none; flex-direction: column; position: absolute; top: 100%; left: 0; right: 0; background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); padding: 1rem; gap: 0.5rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999; }
+ .nav-links { display: none; flex-direction: column; align-items: stretch; position: absolute; top: 100%; left: 0; right: 0; background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); padding: 1rem; gap: 0; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999; }
.nav-links.nav-open { display: flex; }
.nav-container { position: relative; flex-wrap: wrap; }
+ .nav-dropdown { position: static; }
+ .nav-dropdown-menu { position: static; opacity: 1; visibility: visible; transform: none; background: rgba(0,0,0,0.2); border-radius: 6px; margin: 0.25rem 0 0 0.5rem; padding: 0.25rem 0; max-height: 0; overflow: hidden; transition: max-height 0.2s; }
+ .nav-dropdown.open .nav-dropdown-menu { max-height: 400px; }
+ .nav-dropdown-trigger { width: 100%; justify-content: space-between; padding: 0.6rem 0.5rem; }
}
@media (max-width: 768px) {
.nav-container { flex-direction: column; gap: 1rem; align-items: stretch; }
.search-box { max-width: 100%; margin: 0; }
.nav-links { flex-wrap: wrap; justify-content: center; }
}
+ #gasNetworkContent { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
+ @media (max-width: 600px) {
+ #gasNetworkContent { grid-template-columns: 1fr; }
+ }
+ .btn-copy { background: none; border: none; cursor: pointer; padding: 0.25rem; margin-left: 0.35rem; color: var(--text-light); vertical-align: middle; }
+ .btn-copy:hover { color: var(--primary); }
@@ -756,16 +818,38 @@
+
@@ -785,6 +869,26 @@
+
+
+
+
+
Current base fee
+
—
+
TPS: —
+
Block time: —
+
Failed (recent): —
+
+
+
Gas history (last 10 blocks)
+
+
+
+
+
@@ -1053,3126 +1157,67 @@
+
+
+
+
+
+
+
+
+
+
+
Add addresses from their detail page to track them here.
+
+
+
+
+
+
+
+
+
Analytics dashboard (Track 3). Network stats, flow tracking, and bridge analytics coming soon.
+
+
+
+
+
+
+
+
+
Operator panel (Track 4). Configuration and operational controls coming soon.
+
+
+
-
+