Files
explorer-monorepo/frontend/public/index.html
defiQUG 2b956a5a83 Frontend: complete task list (C1–L4), security, a11y, L1 block card helper
- React: response.ok checks (address, transaction, search); block number validation; stable Table keys; API modules (addresses, transactions, blocks normalizer)
- SPA: escapeHtml/safe URLs/onclick; getRpcUrl in rpcCall; cancel blocks rAF on view change; named constants; hash route decode
- SPA: createBlockCardHtml + normalizeBlockDisplay (L1); DEBUG console gating; aria-live for errors; token/block/tx detail escaping
- Docs: FRONTEND_REVIEW.md, FRONTEND_TASKS_AND_REVIEW.md; favicons; .gitignore *.tsbuildinfo

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-10 18:43:37 -08:00

4179 lines
223 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<!-- CSP: unsafe-eval required by ethers.js v5 UMD from CDN (uses new Function for ABI). Our code avoids eval/string setTimeout. -->
<meta http-equiv="Content-Security-Policy" content="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;">
<title>SolaceScanScout | The Defi Oracle Meta Explorer | d-bis.org</title>
<meta name="description" content="SolaceScanScout - The Defi Oracle Meta Explorer. Comprehensive blockchain explorer for ChainID 138 with cross-chain bridge monitoring, WETH utilities, and real-time transaction tracking.">
<meta name="keywords" content="blockchain explorer, ChainID 138, CCIP bridge, WETH, DeFi Oracle, SolaceScanScout, blockchain, ethereum, blockscout">
<meta name="author" content="SolaceScanScout">
<meta name="application-name" content="SolaceScanScout">
<meta name="theme-color" content="#667eea">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://explorer.d-bis.org/">
<meta property="og:title" content="SolaceScanScout - The Defi Oracle Meta Explorer">
<meta property="og:description" content="Comprehensive blockchain explorer for ChainID 138 with cross-chain bridge monitoring and WETH utilities.">
<meta property="og:image" content="https://explorer.d-bis.org/og-image.png">
<meta property="og:site_name" content="SolaceScanScout">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:url" content="https://explorer.d-bis.org/">
<meta name="twitter:title" content="SolaceScanScout - The Defi Oracle Meta Explorer">
<meta name="twitter:description" content="Comprehensive blockchain explorer for ChainID 138 with cross-chain bridge monitoring and WETH utilities.">
<meta name="twitter:image" content="https://explorer.d-bis.org/og-image.png">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Load ethers.js with multiple fallback CDNs -->
<script>
(function() {
var ethersLoaded = false;
var cdnIndex = 0;
var cdns = [
'https://cdn.jsdelivr.net/npm/ethers@5.7.2/dist/ethers.umd.min.js',
'https://unpkg.com/ethers@5.7.2/dist/ethers.umd.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.umd.min.js'
];
function loadEthers() {
if (ethersLoaded || typeof ethers !== 'undefined') {
window.ethersReady = true;
console.log('✅ Ethers.js library loaded successfully');
window.dispatchEvent(new Event('ethersReady'));
return;
}
if (cdnIndex >= cdns.length) {
console.error('❌ All ethers.js CDNs failed to load');
// Try one more time with a different approach
setTimeout(function() {
if (typeof ethers === 'undefined') {
console.error('Ethers.js failed to load from all sources');
} else {
window.ethersReady = true;
window.dispatchEvent(new Event('ethersReady'));
}
}, 1000);
return;
}
var script = document.createElement('script');
script.src = cdns[cdnIndex];
script.async = false; // Load synchronously to ensure it's available
script.onload = function() {
// Double check after load
setTimeout(function() {
if (typeof ethers !== 'undefined') {
ethersLoaded = true;
window.ethersReady = true;
console.log('✅ Ethers.js loaded from:', cdns[cdnIndex]);
window.dispatchEvent(new Event('ethersReady'));
} else {
console.warn('Script loaded but ethers not defined, trying next CDN...');
cdnIndex++;
loadEthers();
}
}, 100);
};
script.onerror = function() {
console.warn('Failed to load from:', cdns[cdnIndex]);
cdnIndex++;
loadEthers();
};
document.head.appendChild(script);
}
// Start loading immediately
loadEthers();
// Also check periodically in case it loads via another method
var checkInterval = setInterval(function() {
if (typeof ethers !== 'undefined' && !ethersLoaded) {
clearInterval(checkInterval);
ethersLoaded = true;
window.ethersReady = true;
console.log('✅ Ethers.js detected');
window.dispatchEvent(new Event('ethersReady'));
}
}, 100);
// Clear interval after 20 seconds
setTimeout(function() {
clearInterval(checkInterval);
}, 20000);
})();
</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #667eea;
--secondary: #764ba2;
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
--bridge-blue: #3b82f6;
--dark: #1f2937;
--light: #f9fafb;
--border: #e5e7eb;
--text: #111827;
--text-light: #6b7280;
}
body.dark-theme {
--light: #111827;
--border: #374151;
--text: #f9fafb;
--text-light: #9ca3af;
background: #0f172a;
color: var(--text);
}
body.dark-theme .stat-card,
body.dark-theme .card,
body.dark-theme .block-card {
background: #1e293b;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
body.dark-theme .table th { background: #334155; color: var(--text); }
body.dark-theme .table tr:hover { background: #1e293b; }
body.dark-theme .skeleton { background: linear-gradient(90deg, #334155 25%, #475569 50%, #334155 75%); }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: var(--light);
color: var(--text);
line-height: 1.6;
}
.navbar {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
position: sticky;
top: 0;
z-index: 1000;
}
.nav-container {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-links {
display: flex;
gap: 2rem;
list-style: none;
}
.nav-links a {
color: white;
text-decoration: none;
transition: opacity 0.2s;
}
.nav-links a:hover { opacity: 0.8; }
.search-box {
flex: 1;
max-width: 600px;
margin: 0 2rem;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: none;
border-radius: 8px;
font-size: 1rem;
background: rgba(255,255,255,0.2);
color: white;
backdrop-filter: blur(10px);
}
.search-input::placeholder { color: rgba(255,255,255,0.7); }
.search-input:focus {
outline: none;
background: rgba(255,255,255,0.3);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.stat-card {
background: white;
padding: 1rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
/* Block animation */
@keyframes blockPulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.02); opacity: 0.95; }
}
@keyframes slideInFromTop {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.table tbody tr {
animation: slideInFromTop 0.3s ease-out;
}
.table tbody tr.new-block {
animation: blockPulse 1s ease-in-out;
background: rgba(74, 144, 226, 0.05);
}
.table tbody tr.new-transaction {
animation: slideInFromTop 0.5s ease-out;
background: rgba(74, 144, 226, 0.05);
}
/* Horizontal scrolling blocks */
.blocks-scroll-container {
display: flex;
overflow-x: hidden;
overflow-y: hidden;
gap: 1rem;
padding: 0.5rem 0;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
position: relative;
width: 100%;
}
.blocks-scroll-container::-webkit-scrollbar {
display: none;
}
.blocks-scroll-content {
display: flex;
gap: 1rem;
will-change: transform;
}
.blocks-scroll-container::-webkit-scrollbar {
height: 8px;
}
.blocks-scroll-container::-webkit-scrollbar-track {
background: var(--light);
border-radius: 4px;
}
.blocks-scroll-container::-webkit-scrollbar-thumb {
background: var(--primary);
border-radius: 4px;
}
.blocks-scroll-container::-webkit-scrollbar-thumb:hover {
background: var(--primary-dark);
}
.block-card {
min-width: 200px;
max-width: 200px;
background: white;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
border: 2px solid transparent;
flex-shrink: 0;
}
.block-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
border-color: var(--primary);
}
.block-card.new-block {
animation: blockPulse 1s ease-in-out;
border-color: var(--primary);
background: rgba(74, 144, 226, 0.05);
}
.block-number {
font-size: 1.25rem;
font-weight: bold;
color: var(--primary);
margin-bottom: 0.5rem;
}
.block-hash {
font-size: 0.75rem;
color: var(--text-light);
font-family: monospace;
margin-bottom: 0.5rem;
word-break: break-all;
}
.block-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.875rem;
}
.block-info-item {
display: flex;
justify-content: space-between;
color: var(--text);
}
.block-info-label {
color: var(--text-light);
font-size: 0.75rem;
}
.block-info-value {
font-weight: 600;
color: var(--text);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.stat-card.bridge-card {
border-left: 4px solid var(--bridge-blue);
}
.stat-label {
color: var(--text-light);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: var(--primary);
}
.stat-value.bridge-value {
color: var(--bridge-blue);
}
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 1.25rem;
margin-bottom: 1rem;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--border);
}
.card-title {
font-size: 1.5rem;
font-weight: bold;
color: var(--text);
}
.tabs {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
border-bottom: 2px solid var(--border);
flex-wrap: wrap;
}
.tab {
padding: 1rem 1.5rem;
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
color: var(--text-light);
border-bottom: 3px solid transparent;
transition: all 0.2s;
}
.tab.active {
color: var(--primary);
border-bottom-color: var(--primary);
font-weight: 600;
}
.bridge-tab.active {
color: var(--bridge-blue);
border-bottom-color: var(--bridge-blue);
}
.weth-tab.active {
color: var(--success);
border-bottom-color: var(--success);
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th {
text-align: left;
padding: 0.5rem 0.75rem;
background: var(--light);
font-weight: 600;
color: var(--text);
border-bottom: 2px solid var(--border);
font-size: 0.875rem;
}
.table td {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border);
font-size: 0.875rem;
}
.table tr:hover { background: var(--light); }
.hash {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
color: var(--primary);
word-break: break-all;
}
.hash:hover { text-decoration: underline; cursor: pointer; }
.badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.875rem;
font-weight: 600;
}
.badge-success { background: #d1fae5; color: var(--success); }
.badge-warning { background: #fef3c7; color: var(--warning); }
.badge-danger { background: #fee2e2; color: var(--danger); }
.badge-chain {
background: #dbeafe;
color: var(--bridge-blue);
}
.loading {
text-align: center;
padding: 3rem;
color: var(--text-light);
}
.loading i {
font-size: 2rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Skeleton Loaders */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton-text {
height: 1em;
margin: 0.5rem 0;
}
.skeleton-title {
height: 1.5em;
width: 60%;
margin-bottom: 1rem;
}
.skeleton-table-row {
height: 3rem;
margin: 0.5rem 0;
}
/* Breadcrumb Navigation */
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 1rem 0;
margin-bottom: 1rem;
font-size: 0.9rem;
}
.breadcrumb a {
color: var(--primary);
text-decoration: none;
transition: opacity 0.2s;
}
.breadcrumb a:hover {
opacity: 0.7;
}
.breadcrumb-separator {
color: var(--text-light);
}
.breadcrumb-current {
color: var(--text);
font-weight: 600;
}
.error {
background: #fee2e2;
color: var(--danger);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
}
.success {
background: #d1fae5;
color: var(--success);
padding: 1rem;
border-radius: 8px;
margin: 1rem 0;
}
.bridge-chain-card {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
padding: 1.5rem;
border-radius: 12px;
margin-bottom: 1rem;
}
.weth-card {
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
padding: 1.5rem;
border-radius: 12px;
margin-bottom: 1.5rem;
}
.weth-form {
background: white;
padding: 1.5rem;
border-radius: 8px;
margin-top: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
color: var(--text);
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 2px solid var(--border);
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-input:focus {
outline: none;
border-color: var(--primary);
}
.form-input-group {
display: flex;
gap: 0.5rem;
}
.form-input-group .form-input {
flex: 1;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover { background: var(--secondary); }
.btn-bridge {
background: var(--bridge-blue);
color: white;
}
.btn-bridge:hover { background: #2563eb; }
.btn-success {
background: var(--success);
color: white;
}
.btn-success:hover { background: #059669; }
.btn-warning {
background: var(--warning);
color: white;
}
.btn-warning:hover { background: #d97706; }
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.balance-display {
background: var(--light);
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
.balance-row {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
border-bottom: 1px solid var(--border);
}
.balance-row:last-child {
border-bottom: none;
}
.balance-label {
color: var(--text-light);
}
.balance-value {
font-weight: bold;
color: var(--text);
}
.chain-name {
font-size: 1.25rem;
font-weight: bold;
color: var(--bridge-blue);
margin-bottom: 0.5rem;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.chain-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.chain-stat {
font-size: 0.875rem;
}
.chain-stat-label {
color: var(--text-light);
}
.chain-stat-value {
font-weight: bold;
color: var(--text);
margin-top: 0.25rem;
}
.detail-view {
display: none;
}
.detail-view.active { display: block; }
.info-row {
display: flex;
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.info-label {
font-weight: 600;
min-width: 200px;
color: var(--text-light);
}
.info-value {
flex: 1;
word-break: break-all;
}
.metamask-status {
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.metamask-status.connected {
background: #d1fae5;
color: var(--success);
}
.metamask-status.disconnected {
background: #fee2e2;
color: var(--danger);
}
.nav-toggle { display: none; background: none; border: none; color: white; padding: 0.5rem; cursor: pointer; font-size: 1.5rem; }
.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.nav-open { display: flex; }
.nav-container { position: relative; flex-wrap: wrap; }
}
@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; }
}
</style>
</head>
<body>
<nav class="navbar">
<div class="nav-container">
<div class="logo">
<i class="fas fa-cube"></i>
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
<span>SolaceScanScout</span>
<span style="font-size: 0.75rem; font-weight: normal; opacity: 0.9;">The Defi Oracle Meta Explorer</span>
</div>
</div>
<div class="search-box" style="display: flex; gap: 0.5rem;">
<label for="searchInput" class="sr-only">Search blockchain explorer</label>
<input type="text" class="search-input" id="searchInput" placeholder="Address, tx hash, block number, or token/contract name..." aria-label="Search blockchain explorer" aria-describedby="search-help-text" style="flex: 1;">
<button id="searchBtn" class="btn btn-primary" style="padding: 0.5rem 1rem; white-space: nowrap;" aria-label="Search">
<i class="fas fa-search"></i>
</button>
<span id="search-help-text" class="sr-only">Search by address (0x...40 hex), transaction hash (0x...64 hex), block number, or token/contract name</span>
</div>
<button type="button" class="nav-toggle" id="navToggle" aria-label="Toggle menu" aria-expanded="false" onclick="toggleNavMenu()"><i class="fas fa-bars" id="navToggleIcon"></i></button>
<ul class="nav-links" id="navLinks">
<li><a href="#/home" onclick="showHome();" aria-label="Navigate to home page"><i class="fas fa-home" aria-hidden="true"></i> Home</a></li>
<li><a href="#/blocks" onclick="showBlocks();" aria-label="View all blocks"><i class="fas fa-cubes" aria-hidden="true"></i> Blocks</a></li>
<li><a href="#/transactions" onclick="showTransactions();" aria-label="View all transactions"><i class="fas fa-exchange-alt" aria-hidden="true"></i> Transactions</a></li>
<li><a href="#/bridge" onclick="showBridgeMonitoring();" aria-label="View bridge monitoring"><i class="fas fa-bridge" aria-hidden="true"></i> Bridge</a></li>
<li><a href="#/weth" onclick="showWETHUtilities();" aria-label="View WETH utilities"><i class="fas fa-coins" aria-hidden="true"></i> WETH</a></li>
<li><a href="#/tokens" onclick="if(typeof focusSearchWithHint==='function')focusSearchWithHint('token');else showHome();" aria-label="View token list"><i class="fas fa-tag" aria-hidden="true"></i> Tokens</a></li>
<li id="analyticsNav" style="display: none;"><a href="#/analytics" onclick="showAnalytics();" aria-label="View analytics"><i class="fas fa-chart-line" aria-hidden="true"></i> Analytics</a></li>
<li id="operatorNav" style="display: none;"><a href="#/operator" onclick="showOperator();" aria-label="View operator panel"><i class="fas fa-cog" aria-hidden="true"></i> Operator</a></li>
</ul>
<div style="display: flex; align-items: center; gap: 0.75rem;">
<button id="themeToggle" type="button" class="btn btn-primary" onclick="toggleDarkMode()" style="padding: 0.5rem 0.75rem; background: rgba(255,255,255,0.2);" aria-label="Toggle dark mode" title="Toggle dark/light theme"><i class="fas fa-moon" id="themeIcon"></i></button>
<div id="walletConnect" style="display: flex; align-items: center; gap: 0.5rem;">
<button id="walletConnectBtn" class="btn btn-primary" onclick="connectWallet()" style="display: none;" aria-label="Connect wallet">Connect Wallet</button>
<div id="walletStatus" style="display: none; padding: 0.5rem 1rem; background: var(--success); color: white; border-radius: 8px; font-size: 0.875rem;">
<i class="fas fa-wallet"></i> <span id="walletAddress"></span>
</div>
</div>
</div>
</div>
</nav>
<div id="explorerLiveRegion" aria-live="polite" aria-atomic="true" class="sr-only" role="status"></div>
<div class="container" id="mainContent">
<!-- Home View -->
<div id="homeView">
<div class="stats-grid" id="statsGrid">
<!-- Stats loaded dynamically -->
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Latest Blocks</h2>
<button class="btn btn-primary" onclick="showBlocks()" aria-label="View all blocks">View All</button>
</div>
<div id="latestBlocks">
<div class="loading"><i class="fas fa-spinner"></i> Loading blocks...</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Latest Transactions</h2>
<button class="btn btn-primary" onclick="showTransactions()" aria-label="View all transactions">View All</button>
</div>
<div id="latestTransactions">
<div class="loading"><i class="fas fa-spinner"></i> Loading transactions...</div>
</div>
</div>
</div>
<!-- WETH Utilities View -->
<div id="wethView" class="detail-view">
<div class="card">
<div class="card-header">
<h2 class="card-title"><i class="fas fa-coins"></i> WETH9 & WETH10 Utilities</h2>
<button class="btn btn-success" onclick="refreshWETHBalances()" aria-label="Refresh WETH balances"><i class="fas fa-sync-alt" aria-hidden="true"></i> Refresh</button>
</div>
<!-- MetaMask Connection Status -->
<div id="metamaskStatus" class="metamask-status disconnected">
<i class="fas fa-wallet"></i>
<span>MetaMask not connected</span>
<button class="btn btn-success" onclick="connectMetaMask()" style="margin-left: auto;" aria-label="Connect MetaMask wallet">Connect MetaMask</button>
</div>
<div class="tabs">
<button class="tab weth-tab active" onclick="showWETHTab('weth9', this)" aria-label="Switch to WETH9 tab" role="tab" aria-selected="true">WETH9</button>
<button class="tab weth-tab" onclick="showWETHTab('weth10', this)" aria-label="Switch to WETH10 tab" role="tab" aria-selected="false">WETH10</button>
<button class="tab weth-tab" onclick="showWETHTab('info', this)" aria-label="Switch to information tab" role="tab" aria-selected="false">Information</button>
</div>
<!-- WETH9 Tab -->
<div id="weth9Tab" class="weth-tab-content">
<div class="weth-card">
<div class="chain-name">WETH9 Token</div>
<div style="color: var(--text-light); margin-bottom: 1rem;">
Contract: <span class="hash" onclick="showAddressDetail('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')" style="cursor: pointer;">0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2</span>
</div>
<div class="balance-display" id="weth9Balance">
<div class="balance-row">
<span class="balance-label">ETH Balance:</span>
<span class="balance-value" id="weth9EthBalance">-</span>
</div>
<div class="balance-row">
<span class="balance-label">WETH9 Balance:</span>
<span class="balance-value" id="weth9TokenBalance">-</span>
</div>
</div>
<div class="weth-form">
<h3 style="margin-bottom: 1rem;">Wrap ETH → WETH9</h3>
<div class="form-group">
<label for="weth9WrapAmount" class="form-label">Amount (ETH)</label>
<div class="form-input-group">
<input type="number" class="form-input" id="weth9WrapAmount" placeholder="0.0" step="0.000001" min="0" aria-label="WETH9 wrap amount">
<button class="btn btn-primary" onclick="setMaxWETH9('wrap')" aria-label="Set maximum WETH9 wrap amount">MAX</button>
</div>
</div>
<button class="btn btn-success" onclick="wrapWETH9()" id="weth9WrapBtn" disabled aria-label="Wrap ETH to WETH9">
<i class="fas fa-arrow-right"></i> Wrap ETH to WETH9
</button>
</div>
<div class="weth-form">
<h3 style="margin-bottom: 1rem;">Unwrap WETH9 → ETH</h3>
<div class="form-group">
<label for="weth9UnwrapAmount" class="form-label">Amount (WETH9)</label>
<div class="form-input-group">
<input type="number" class="form-input" id="weth9UnwrapAmount" placeholder="0.0" step="0.000001" min="0" aria-label="WETH9 unwrap amount">
<button class="btn btn-primary" onclick="setMaxWETH9('unwrap')" aria-label="Set maximum WETH9 unwrap amount">MAX</button>
</div>
</div>
<button class="btn btn-warning" onclick="unwrapWETH9()" id="weth9UnwrapBtn" disabled aria-label="Unwrap WETH9 to ETH">
<i class="fas fa-arrow-left"></i> Unwrap WETH9 to ETH
</button>
</div>
</div>
</div>
<!-- WETH10 Tab -->
<div id="weth10Tab" class="weth-tab-content" style="display: none;">
<div class="weth-card">
<div class="chain-name">WETH10 Token</div>
<div style="color: var(--text-light); margin-bottom: 1rem;">
Contract: <span class="hash" onclick="showAddressDetail('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')" style="cursor: pointer;">0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f</span>
</div>
<div class="balance-display" id="weth10Balance">
<div class="balance-row">
<span class="balance-label">ETH Balance:</span>
<span class="balance-value" id="weth10EthBalance">-</span>
</div>
<div class="balance-row">
<span class="balance-label">WETH10 Balance:</span>
<span class="balance-value" id="weth10TokenBalance">-</span>
</div>
</div>
<div class="weth-form">
<h3 style="margin-bottom: 1rem;">Wrap ETH → WETH10</h3>
<div class="form-group">
<label for="weth10WrapAmount" class="form-label">Amount (ETH)</label>
<div class="form-input-group">
<input type="number" class="form-input" id="weth10WrapAmount" placeholder="0.0" step="0.000001" min="0" aria-label="WETH10 wrap amount">
<button class="btn btn-primary" onclick="setMaxWETH10('wrap')" aria-label="Set maximum WETH10 wrap amount">MAX</button>
</div>
</div>
<button class="btn btn-success" onclick="wrapWETH10()" id="weth10WrapBtn" disabled aria-label="Wrap ETH to WETH10">
<i class="fas fa-arrow-right"></i> Wrap ETH to WETH10
</button>
</div>
<div class="weth-form">
<h3 style="margin-bottom: 1rem;">Unwrap WETH10 → ETH</h3>
<div class="form-group">
<label for="weth10UnwrapAmount" class="form-label">Amount (WETH10)</label>
<div class="form-input-group">
<input type="number" class="form-input" id="weth10UnwrapAmount" placeholder="0.0" step="0.000001" min="0" aria-label="WETH10 unwrap amount">
<button class="btn btn-primary" onclick="setMaxWETH10('unwrap')" aria-label="Set maximum WETH10 unwrap amount">MAX</button>
</div>
</div>
<button class="btn btn-warning" onclick="unwrapWETH10()" id="weth10UnwrapBtn" disabled aria-label="Unwrap WETH10 to ETH">
<i class="fas fa-arrow-left"></i> Unwrap WETH10 to ETH
</button>
</div>
</div>
</div>
<!-- Information Tab -->
<div id="wethInfoTab" class="weth-tab-content" style="display: none;">
<div class="card">
<h3>About WETH9 and WETH10</h3>
<div style="margin-top: 1rem; line-height: 1.8;">
<p><strong>WETH9</strong> and <strong>WETH10</strong> are wrapped versions of native ETH (Ether) that allow you to use ETH in smart contracts and DeFi protocols.</p>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">What is Wrapping?</h4>
<p>Wrapping ETH converts your native ETH into an ERC-20 token (WETH9 or WETH10) that can be used in DeFi applications, smart contracts, and cross-chain bridging.</p>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">Contract Addresses</h4>
<ul style="margin-left: 2rem; margin-top: 0.5rem;">
<li><strong>WETH9:</strong> <span class="hash">0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2</span></li>
<li><strong>WETH10:</strong> <span class="hash">0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f</span></li>
</ul>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">How to Use</h4>
<ol style="margin-left: 2rem; margin-top: 0.5rem;">
<li>Connect your MetaMask wallet</li>
<li>Select WETH9 or WETH10 tab</li>
<li>Enter the amount to wrap or unwrap</li>
<li>Confirm the transaction in MetaMask</li>
</ol>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">Cross-Chain Bridging</h4>
<p>Both WETH9 and WETH10 can be bridged to other chains using the CCIP bridge contracts:</p>
<ul style="margin-left: 2rem; margin-top: 0.5rem;">
<li><strong>WETH9 Bridge:</strong> <span class="hash">0x89dd12025bfCD38A168455A44B400e913ED33BE2</span></li>
<li><strong>WETH10 Bridge:</strong> <span class="hash">0xe0E93247376aa097dB308B92e6Ba36bA015535D0</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Bridge Monitoring View -->
<div id="bridgeView" class="detail-view">
<div class="card">
<div class="card-header">
<h2 class="card-title"><i class="fas fa-bridge"></i> Bridge Monitoring</h2>
<button class="btn btn-primary" onclick="refreshBridgeData()" aria-label="Refresh bridge data"><i class="fas fa-sync-alt" aria-hidden="true"></i> Refresh</button>
</div>
<div id="bridgeContent">
<div class="loading"><i class="fas fa-spinner"></i> Loading bridge data...</div>
</div>
</div>
</div>
<!-- Other views -->
<div id="blocksView" class="detail-view">
<div class="card">
<div class="card-header">
<h2 class="card-title">All Blocks</h2>
<button type="button" class="btn btn-primary" onclick="exportBlocksCSV()" style="padding: 0.5rem 1rem;"><i class="fas fa-file-csv"></i> Export CSV</button>
</div>
<div id="blocksList">
<div class="loading"><i class="fas fa-spinner"></i> Loading blocks...</div>
</div>
</div>
</div>
<div id="transactionsView" class="detail-view">
<div class="card">
<div class="card-header">
<h2 class="card-title">All Transactions</h2>
<button type="button" class="btn btn-primary" onclick="exportTransactionsListCSV()" style="padding: 0.5rem 1rem;"><i class="fas fa-file-csv"></i> Export CSV</button>
</div>
<div id="transactionsList">
<div class="loading"><i class="fas fa-spinner"></i> Loading transactions...</div>
</div>
</div>
</div>
<div id="blockDetailView" class="detail-view">
<div class="breadcrumb" id="blockDetailBreadcrumb"></div>
<div class="card">
<div class="card-header">
<button class="btn btn-secondary" onclick="showBlocks()" aria-label="Go back to blocks view"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
<h2 class="card-title">Block Details</h2>
</div>
<div id="blockDetail"></div>
</div>
</div>
<div id="transactionDetailView" class="detail-view">
<div class="breadcrumb" id="transactionDetailBreadcrumb"></div>
<div class="card">
<div class="card-header">
<button class="btn btn-secondary" onclick="showTransactions()" aria-label="Go back to transactions view"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
<h2 class="card-title">Transaction Details</h2>
</div>
<div id="transactionDetail"></div>
</div>
</div>
<div id="addressDetailView" class="detail-view">
<div class="breadcrumb" id="addressDetailBreadcrumb"></div>
<div class="card">
<div class="card-header">
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back to home page"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
<h2 class="card-title">Address Details</h2>
</div>
<div id="addressDetail"></div>
</div>
</div>
<div id="tokenDetailView" class="detail-view">
<div class="breadcrumb" id="tokenDetailBreadcrumb"></div>
<div class="card">
<div class="card-header">
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back"><i class="fas fa-arrow-left"></i> Back</button>
<h2 class="card-title">Token</h2>
</div>
<div id="tokenDetail"></div>
</div>
</div>
<div id="nftDetailView" class="detail-view">
<div class="breadcrumb" id="nftDetailBreadcrumb"></div>
<div class="card">
<div class="card-header">
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back"><i class="fas fa-arrow-left"></i> Back</button>
<h2 class="card-title">NFT</h2>
</div>
<div id="nftDetail"></div>
</div>
</div>
</div>
<script>
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://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;
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 (would fire hashchange and cause infinite recursion).
function switchToView(viewName) {
if (viewName !== 'blocks' && _blocksScrollAnimationId != null) {
cancelAnimationFrame(_blocksScrollAnimationId);
_blocksScrollAnimationId = null;
}
currentView = viewName;
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail'];
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.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() {};
window.showTransactionDetail = function() {};
window.showAddressDetail = function() {};
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');
if (analyticsNav) analyticsNav.style.display = hasAccess(3) ? 'block' : 'none';
if (operatorNav) operatorNav.style.display = hasAccess(4) ? 'block' : 'none';
}
// Wallet authentication
async function connectWallet() {
if (typeof ethers === 'undefined') {
alert('Ethers.js not loaded. Please refresh the page.');
return;
}
try {
if (!window.ethereum) {
alert('MetaMask not detected. Please install MetaMask.');
return;
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const accounts = await provider.send("eth_requestAccounts", []);
const address = accounts[0];
// Request nonce
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;
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
};
}
// Skeleton loader function
function createSkeletonLoader(type) {
switch(type) {
case 'stats':
return `
<div class="stats-grid">
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
<div class="stat-card">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text" style="width: 80%;"></div>
</div>
</div>
`;
case 'table':
return `
<table class="table">
<thead>
<tr>
<th><div class="skeleton skeleton-text"></div></th>
<th><div class="skeleton skeleton-text"></div></th>
<th><div class="skeleton skeleton-text"></div></th>
<th><div class="skeleton skeleton-text"></div></th>
</tr>
</thead>
<tbody>
${Array(5).fill(0).map(() => `
<tr>
<td><div class="skeleton skeleton-text"></div></td>
<td><div class="skeleton skeleton-text"></div></td>
<td><div class="skeleton skeleton-text"></div></td>
<td><div class="skeleton skeleton-text"></div></td>
</tr>
`).join('')}
</tbody>
</table>
`;
case 'detail':
return `
<div class="card">
<div class="card-header">
<div class="skeleton skeleton-title" style="width: 40%;"></div>
</div>
<div class="card-body">
${Array(8).fill(0).map(() => `
<div class="info-row">
<div class="info-label">
<div class="skeleton skeleton-text" style="width: 120px;"></div>
</div>
<div class="info-value">
<div class="skeleton skeleton-text" style="width: 200px;"></div>
</div>
</div>
`).join('')}
</div>
</div>
`;
default:
return '<div class="loading"><i class="fas fa-spinner fa-spin"></i> Loading...</div>';
}
}
// 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();
if (window.location.hash) applyHashRoute();
window.addEventListener('hashchange', function() { if (window.location.hash) applyHashRoute(); });
window.addEventListener('load', function() { if (window.location.hash) 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 = `
<i class="fas fa-check-circle"></i>
<span>Connected: ${escapeHtml(shortenHash(userAddress))}</span>
<button class="btn btn-warning" onclick="disconnectMetaMask()" style="margin-left: auto;" aria-label="Disconnect MetaMask wallet">Disconnect</button>
`;
// 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 = `
<i class="fas fa-wallet"></i>
<span>MetaMask not connected</span>
<button class="btn btn-success" onclick="connectMetaMask()" style="margin-left: auto;" aria-label="Connect MetaMask wallet">Connect MetaMask</button>
`;
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 = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth9Contract.deposit({ value: amountWei });
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth9WrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH9';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error wrapping WETH9:', error);
alert('Wrap WETH9: ' + wrapUnwrapErrorMessage('wrap', error));
document.getElementById('weth9WrapBtn').innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH9';
document.getElementById('weth9WrapBtn').disabled = false;
}
}
async function unwrapWETH9() {
const amount = document.getElementById('weth9UnwrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
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 = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth9Contract.withdraw(amountWei);
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth9UnwrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH9 to ETH';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error unwrapping WETH9:', error);
alert('Unwrap WETH9: ' + wrapUnwrapErrorMessage('unwrap', error));
document.getElementById('weth9UnwrapBtn').innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH9 to ETH';
document.getElementById('weth9UnwrapBtn').disabled = false;
}
}
async function wrapWETH10() {
const amount = document.getElementById('weth10WrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
// Ensure address is checksummed
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
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 = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth10Contract.deposit({ value: amountWei });
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth10WrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH10';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error wrapping WETH10:', error);
alert('Wrap WETH10: ' + wrapUnwrapErrorMessage('wrap', error));
document.getElementById('weth10WrapBtn').innerHTML = '<i class="fas fa-arrow-right"></i> Wrap ETH to WETH10';
document.getElementById('weth10WrapBtn').disabled = false;
}
}
async function unwrapWETH10() {
const amount = document.getElementById('weth10UnwrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
// Ensure address is checksummed
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
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 = '<i class="fas fa-spinner fa-spin"></i> Processing...';
const tx = await weth10Contract.withdraw(amountWei);
const receipt = await tx.wait();
btn.innerHTML = '<i class="fas fa-check"></i> Success!';
document.getElementById('weth10UnwrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH10 to ETH';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error unwrapping WETH10:', error);
alert('Unwrap WETH10: ' + wrapUnwrapErrorMessage('unwrap', error));
document.getElementById('weth10UnwrapBtn').innerHTML = '<i class="fas fa-arrow-left"></i> Unwrap WETH10 to ETH';
document.getElementById('weth10UnwrapBtn').disabled = false;
}
}
function showWETHTab(tab, clickedElement) {
document.querySelectorAll('.weth-tab-content').forEach(el => el.style.display = 'none');
document.querySelectorAll('.weth-tab').forEach(el => el.classList.remove('active'));
const tabElement = document.getElementById(`${tab}Tab`);
if (tabElement) {
tabElement.style.display = 'block';
}
// Update active tab - use clickedElement if provided, otherwise find by tab name
if (clickedElement) {
clickedElement.classList.add('active');
} else {
// Find the button that corresponds to this tab
const tabButtons = document.querySelectorAll('.weth-tab');
tabButtons.forEach(btn => {
if (btn.getAttribute('onclick')?.includes(`'${tab}'`)) {
btn.classList.add('active');
}
});
}
}
window.showWETHTab = showWETHTab;
async function showWETHUtilities() {
showView('weth');
if ((window.location.hash || '').replace(/^#/, '').split('/')[0] !== 'weth') window.location.hash = '#/weth';
if (userAddress) {
await refreshWETHBalances();
}
}
window._showWETHUtilities = showWETHUtilities;
async function showBridgeMonitoring() {
showView('bridge');
if ((window.location.hash || '').replace(/^#/, '').split('/')[0] !== 'bridge') window.location.hash = '#/bridge';
await refreshBridgeData();
}
window._showBridgeMonitoring = showBridgeMonitoring;
async function showHome() {
showView('home');
if ((window.location.hash || '').replace(/^#/, '') !== 'home') window.location.hash = '#/home';
await loadStats();
await loadLatestBlocks();
await loadLatestTransactions();
// Start real-time transaction updates
startTransactionUpdates();
}
window._showHome = showHome;
async function showBlocks() {
showView('blocks');
if ((window.location.hash || '').replace(/^#/, '').split('/')[0] !== 'blocks') window.location.hash = '#/blocks';
await loadAllBlocks();
}
window._showBlocks = showBlocks;
async function showTransactions() {
showView('transactions');
if ((window.location.hash || '').replace(/^#/, '').split('/')[0] !== 'transactions') window.location.hash = '#/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;
}
showView('analytics');
// TODO: Load analytics dashboard
document.getElementById('mainContent').innerHTML = `
<div class="card">
<div class="card-header">
<h2 class="card-title"><i class="fas fa-chart-line"></i> Analytics Dashboard</h2>
</div>
<div class="card-body">
<p>Analytics dashboard coming soon. Track 3 features enabled.</p>
</div>
</div>
`;
}
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;
}
showView('operator');
// TODO: Load operator panel
document.getElementById('mainContent').innerHTML = `
<div class="card">
<div class="card-header">
<h2 class="card-title"><i class="fas fa-cog"></i> Operator Panel</h2>
</div>
<div class="card-body">
<p>Operator panel coming soon. Track 4 features enabled.</p>
</div>
</div>
`;
}
window._showOperator = showOperator;
function showView(viewName) {
currentView = viewName;
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail'];
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 applyHashRoute() {
var hash = (window.location.hash || '').replace(/^#/, '');
if (!hash) return;
var parts = hash.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] === '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;
if (document.readyState !== 'loading' && window.location.hash) { 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';
}
window.toggleNavMenu = toggleNavMenu;
window.closeNavMenu = closeNavMenu;
// Update breadcrumb navigation
function updateBreadcrumb(type, identifier, identifierExtra) {
let breadcrumbContainer;
let breadcrumbHTML = '<a href="#/home">Home</a>';
switch (type) {
case 'block':
breadcrumbContainer = document.getElementById('blockDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="#/blocks">Blocks</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += `<span class="breadcrumb-current">Block #${identifier}</span>`;
break;
case 'transaction':
breadcrumbContainer = document.getElementById('transactionDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="#/transactions">Transactions</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'address':
breadcrumbContainer = document.getElementById('addressDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><span>Address</span><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'token':
breadcrumbContainer = document.getElementById('tokenDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="#/tokens">Tokens</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Token ' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'nft':
breadcrumbContainer = document.getElementById('nftDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="#/address/' + encodeURIComponent(identifier) + '">' + escapeHtml(shortenHash(identifier)) + '</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Token ID ' + (identifierExtra != null ? escapeHtml(String(identifierExtra)) : '') + '</span>';
break;
default:
return;
}
if (breadcrumbContainer) {
breadcrumbContainer.innerHTML = breadcrumbHTML;
}
}
function updateBreadcrumb(type, identifier, identifierExtra) {
let breadcrumbContainer;
let breadcrumbHTML = '<a href="#/home">Home</a>';
switch (type) {
case 'block':
breadcrumbContainer = document.getElementById('blockDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><a href="#/blocks">Blocks</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">Block #' + identifier + '</span>';
break;
case 'transaction':
breadcrumbContainer = document.getElementById('transactionDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><a href="#/transactions">Transactions</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'address':
breadcrumbContainer = document.getElementById('addressDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><span class="breadcrumb-separator">Address</span><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'token':
breadcrumbContainer = document.getElementById('tokenDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><a href="#/tokens">Tokens</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">Token ' + escapeHtml(shortenHash(identifier)) + '</span>';
break;
case 'nft':
breadcrumbContainer = document.getElementById('nftDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span><a href="#/address/' + encodeURIComponent(identifier) + '">' + escapeHtml(shortenHash(identifier)) + '</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">Token ID ' + (identifierExtra != null ? escapeHtml(String(identifierExtra)) : '') + '</span>';
break;
default:
return;
}
if (breadcrumbContainer) breadcrumbContainer.innerHTML = breadcrumbHTML;
}
// Retry logic with exponential backoff
async function fetchAPIWithRetry(url, maxRetries = FETCH_MAX_RETRIES, retryDelay = RETRY_DELAY_MS) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetchAPI(url);
} catch (error) {
const isLastAttempt = attempt === maxRetries - 1;
const isRetryable = error.name === 'AbortError' ||
(error.message && (error.message.includes('timeout') ||
error.message.includes('500') ||
error.message.includes('502') ||
error.message.includes('503') ||
error.message.includes('504') ||
error.message.includes('NetworkError')));
if (isLastAttempt || !isRetryable) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delay = retryDelay * Math.pow(2, attempt);
console.warn(`⚠️ API call failed (attempt ${attempt + 1}/${maxRetries}), retrying in ${delay}ms...`, error.message);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
async function fetchAPI(url) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
try {
const response = await fetch(url, {
method: 'GET',
headers: { 'Accept': 'application/json' },
credentials: 'omit',
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
let errorText = '';
try {
errorText = await response.text();
} catch (e) {
errorText = response.statusText;
}
// Log detailed error for debugging
const errorInfo = {
status: response.status,
statusText: response.statusText,
errorText: errorText.substring(0, 500),
url: url,
headers: Object.fromEntries(response.headers.entries())
};
console.error(`❌ API Error:`, errorInfo);
// 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 = `
<div class="stat-card">
<div class="stat-label">Total Blocks</div>
<div class="stat-value">${formatNumber(stats.total_blocks || 0)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Transactions</div>
<div class="stat-value">${formatNumber(stats.total_transactions || 0)}</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Addresses</div>
<div class="stat-value">${formatNumber(stats.total_addresses || 0)}</div>
</div>
<div class="stat-card bridge-card">
<div class="stat-label">Bridge Contracts</div>
<div class="stat-value bridge-value">2 Active</div>
</div>
<div class="stat-card" id="networkStatCard">
<div class="stat-label">Network</div>
<div class="stat-value" id="networkStatValue" style="font-size: 1rem;">Loading...</div>
</div>
`;
if (CHAIN_ID === 138) loadGasAndNetworkStats();
} catch (error) {
console.error('Failed to load stats:', error);
statsGrid.innerHTML = `
<div class="stat-card">
<div class="stat-label">Total Blocks</div>
<div class="stat-value">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Transactions</div>
<div class="stat-value">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Total Addresses</div>
<div class="stat-value">-</div>
</div>
<div class="stat-card bridge-card">
<div class="stat-label">Bridge Contracts</div>
<div class="stat-value bridge-value">2 Active</div>
</div>
<div class="stat-card"><div class="stat-label">Network</div><div class="stat-value" style="font-size: 1rem;">-</div></div>
`;
}
}
async function loadGasAndNetworkStats() {
var el = document.getElementById('networkStatValue');
if (!el) return;
try {
var blocksResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/blocks?page=1&page_size=5');
var blocks = blocksResp.items || blocksResp || [];
var statsResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/stats').catch(function() { return {}; });
var totalTxs = statsResp.total_transactions || 0;
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);
el.innerHTML = (gasGwei !== '-' ? 'Gas: ' + escapeHtml(gasGwei) + '<br/>' : '') + (blockTimeSec !== '-' ? 'Block: ' + escapeHtml(blockTimeSec) + '<br/>' : '') + (tps !== '-' ? 'TPS: ' + escapeHtml(tps) : '') || 'Gas / TPS';
} catch (e) {
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 '<div class="block-card ' + escapeHtml(animationClass) + '" onclick="showBlockDetail(\'' + escapeHtml(String(d.blockNum)) + '\')">' +
'<div class="block-number">#' + escapeHtml(String(d.blockNum)) + '</div>' +
'<div class="block-hash">' + escapeHtml(shortenHash(d.hash)) + '</div>' +
'<div class="block-info">' +
'<div class="block-info-item"><span class="block-info-label">Transactions</span><span class="block-info-value">' + escapeHtml(String(d.txCount)) + '</span></div>' +
'<div class="block-info-item"><span class="block-info-label">Time</span><span class="block-info-value">' + escapeHtml(d.timeAgo) + '</span></div>' +
'</div></div>';
}
// Prevent multiple simultaneous calls
let loadingBlocks = false;
async function loadLatestBlocks() {
const container = document.getElementById('latestBlocks');
if (!container) return;
// Prevent multiple simultaneous calls
if (loadingBlocks) {
console.log('loadLatestBlocks already in progress, skipping...');
return;
}
loadingBlocks = true;
try {
let blocks = [];
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
try {
const blockscoutUrl = `${BLOCKSCOUT_API}/v2/blocks?page=1&page_size=10`;
console.log('Fetching blocks from Blockscout:', blockscoutUrl);
const response = await fetchAPIWithRetry(blockscoutUrl);
const raw = (response && (response.items || response.data || response.blocks)) || (Array.isArray(response) ? response : null);
if (raw && Array.isArray(raw)) {
blocks = raw.slice(0, 10).map(normalizeBlock).filter(b => b !== null);
console.log(`✅ Loaded ${blocks.length} blocks from Blockscout`);
} else if (response && typeof response === 'object') {
blocks = [];
console.warn('Blockscout blocks response empty or unexpected shape:', Object.keys(response || {}));
}
} catch (blockscoutError) {
console.warn('Blockscout API failed, trying RPC fallback:', blockscoutError.message);
try {
const blockNumHex = await rpcCall('eth_blockNumber');
const latestBlock = parseInt(blockNumHex, 16);
if (!isNaN(latestBlock) && latestBlock >= 0) {
for (let i = 0; i < Math.min(10, latestBlock + 1); i++) {
const bn = latestBlock - i;
const b = await rpcCall('eth_getBlockByNumber', ['0x' + bn.toString(16), false]);
if (b && b.number) blocks.push({ number: parseInt(b.number, 16), hash: b.hash, timestamp: b.timestamp ? (typeof b.timestamp === 'string' ? parseInt(b.timestamp, 16) * 1000 : b.timestamp) : null, transaction_count: b.transactions ? b.transactions.length : 0 });
}
if (blocks.length > 0) console.log('Loaded ' + blocks.length + ' blocks via RPC fallback');
}
} catch (rpcErr) {
console.error('RPC fallback also failed:', rpcErr);
if (container) {
container.innerHTML = '<div class="error">API temporarily unavailable. ' + escapeHtml((blockscoutError.message || 'Unknown error').substring(0, 150)) + ' <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
}
return;
}
if (blocks.length === 0 && container) {
container.innerHTML = '<div class="error">Could not load blocks. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
return;
}
}
} else {
// For other networks, use Etherscan-compatible API
const blockData = await fetchAPI(`${API_BASE}?module=block&action=eth_block_number`);
if (!blockData || !blockData.result) {
throw new Error('Invalid response from API');
}
const latestBlock = parseInt(blockData.result, 16);
if (isNaN(latestBlock) || latestBlock < 0) {
throw new Error('Invalid block number');
}
// Fetch blocks one by one
for (let i = 0; i < 10 && latestBlock - i >= 0; i++) {
const blockNum = latestBlock - i;
try {
const block = await fetchAPI(`${API_BASE}?module=block&action=eth_get_block_by_number&tag=0x${blockNum.toString(16)}&boolean=false`);
if (block && block.result) {
blocks.push({
number: blockNum,
hash: block.result.hash,
timestamp: block.result.timestamp,
transaction_count: block.result.transactions ? block.result.transactions.length : 0
});
}
} catch (e) {
console.warn(`Failed to load block ${blockNum}:`, e);
}
}
}
const limitedBlocks = blocks.slice(0, 10);
if (limitedBlocks.length === 0) {
if (container) container.innerHTML = '<div style="text-align: center; padding: 2rem; color: var(--text-light);">No blocks found. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
} else {
// Create HTML with duplicated blocks for seamless infinite loop
let html = '<div class="blocks-scroll-container" id="blocksScrollContainer">';
html += '<div class="blocks-scroll-content">';
// First set of blocks (with animations for first 3)
limitedBlocks.forEach(function(block, index) {
var animationClass = index < 3 ? 'new-block' : '';
html += createBlockCardHtml(block, { animationClass: animationClass });
});
// Duplicate blocks for seamless infinite loop
limitedBlocks.forEach(function(block) {
html += createBlockCardHtml(block, {});
});
html += '</div></div>';
if (container) container.innerHTML = html;
// Setup auto-scroll animation
const scrollContainer = document.getElementById('blocksScrollContainer');
const scrollContent = scrollContainer?.querySelector('.blocks-scroll-content');
if (scrollContainer && scrollContent) {
const cardWidth = 200 + 16; // card width (200px) + gap (16px = 1rem)
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 = '<div class="error">Failed to load blocks: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '. <button type="button" class="btn btn-primary" onclick="loadLatestBlocks()" style="margin-top: 0.5rem;">Retry</button></div>';
} finally {
loadingBlocks = false;
}
}
// Store previous transaction hashes for real-time updates
let previousTransactionHashes = new Set();
let transactionUpdateInterval = null;
async function loadLatestTransactions() {
const container = document.getElementById('latestTransactions');
if (!container) return;
try {
let response;
let rawTransactions = [];
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
try {
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 = '<div class="error">API temporarily unavailable. <button type="button" class="btn btn-primary" onclick="loadLatestTransactions()" style="margin-top: 0.5rem;">Retry</button></div>';
return;
}
}
} else {
response = await fetchAPIWithRetry(`${API_BASE}/v2/transactions?page=1&page_size=10`);
rawTransactions = Array.isArray(response?.items) ? response.items : (Array.isArray(response?.data) ? response.data : []);
}
// Normalize transactions using adapter
const transactions = rawTransactions.map(normalizeTransaction).filter(tx => tx !== null);
// Limit to 10 transactions
const limitedTransactions = transactions.slice(0, 10);
// 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 = '<table class="table"><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Value</th><th>Block</th></tr></thead><tbody>';
if (limitedTransactions.length === 0) {
html += '<tr><td colspan="5" style="text-align: center; padding: 1rem;">No transactions found</td></tr>';
} else {
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 += '<tr class="' + animationClass + '" onclick="showTransactionDetail(\'' + escapeHtml(hash) + '\')" style="cursor: pointer;"><td class="hash">' + escapeHtml(shortenHash(hash)) + '</td><td class="hash">' + escapeHtml(shortenHash(from)) + '</td><td class="hash">' + escapeHtml(shortenHash(to)) + '</td><td>' + escapeHtml(valueFormatted) + ' ETH</td><td>' + escapeHtml(String(blockNumber)) + '</td></tr>';
});
}
html += '</tbody></table>';
if (container) container.innerHTML = html;
if (container) {
setTimeout(() => {
container.querySelectorAll('.new-transaction').forEach(row => {
row.classList.remove('new-transaction');
});
}, 500);
}
} catch (error) {
console.error('Failed to load latest transactions:', error);
if (container) container.innerHTML = '<div class="error">Failed to load transactions: ' + escapeHtml((error.message || 'Unknown error').substring(0, 200)) + '. <button type="button" class="btn btn-primary" onclick="loadLatestTransactions()" style="margin-top: 0.5rem;">Retry</button></div>';
}
}
// Real-time transaction updates
function startTransactionUpdates() {
// Clear any existing interval
if (transactionUpdateInterval) {
clearInterval(transactionUpdateInterval);
}
// Update transactions every 5 seconds
transactionUpdateInterval = setInterval(() => {
if (currentView === 'home') {
loadLatestTransactions();
}
}, 5000);
}
function stopTransactionUpdates() {
if (transactionUpdateInterval) {
clearInterval(transactionUpdateInterval);
transactionUpdateInterval = null;
}
}
async function loadAllBlocks() {
const container = document.getElementById('blocksList');
if (!container) return;
try {
container.innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading blocks...</div>';
let blocks = [];
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
try {
// Fetch blocks from Blockscout API (paginated)
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks?page=1&page_size=50`);
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 = '<table class="table"><thead><tr><th>Block</th><th>Hash</th><th>Transactions</th><th>Timestamp</th></tr></thead><tbody>';
if (blocks.length === 0) {
html += '<tr><td colspan="4" style="text-align: center; padding: 2rem;">No blocks found</td></tr>';
} else {
blocks.forEach(function(block) {
var d = normalizeBlockDisplay(block);
html += '<tr onclick="showBlockDetail(\'' + escapeHtml(String(d.blockNum)) + '\')" style="cursor: pointer;"><td>' + escapeHtml(String(d.blockNum)) + '</td><td class="hash">' + escapeHtml(shortenHash(d.hash)) + '</td><td>' + escapeHtml(String(d.txCount)) + '</td><td>' + escapeHtml(d.timestampFormatted) + '</td></tr>';
});
}
html += '</tbody></table>';
container.innerHTML = html;
} catch (error) {
container.innerHTML = '<div class="error">Failed to load blocks: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showBlocks()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
}
}
async function loadAllTransactions() {
const container = document.getElementById('transactionsList');
if (!container) return;
try {
container.innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading transactions...</div>';
let transactions = [];
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
try {
// Fetch transactions from Blockscout API (paginated)
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?page=1&page_size=50`);
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 = '<table class="table"><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Value</th><th>Block</th></tr></thead><tbody>';
if (transactions.length === 0) {
html += '<tr><td colspan="5" style="text-align: center; padding: 2rem;">No transactions found</td></tr>';
} else {
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 += '<tr onclick="showTransactionDetail(\'' + escapeHtml(hash) + '\')" style="cursor: pointer;"><td class="hash">' + escapeHtml(shortenHash(hash)) + '</td><td class="hash">' + escapeHtml(shortenHash(from)) + '</td><td class="hash">' + escapeHtml(shortenHash(to)) + '</td><td>' + escapeHtml(valueFormatted) + ' ETH</td><td>' + escapeHtml(String(blockNumber)) + '</td></tr>';
});
}
html += '</tbody></table>';
container.innerHTML = html;
} catch (error) {
container.innerHTML = '<div class="error">Failed to load transactions: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showTransactions()" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
}
}
async function refreshBridgeData() {
const container = document.getElementById('bridgeContent');
if (!container) return;
try {
container.innerHTML = '<div class="loading"><i class="fas fa-spinner"></i> Loading bridge data...</div>';
// Chain 138 Bridge Contracts
const WETH9_BRIDGE_138 = '0x89dd12025bfCD38A168455A44B400e913ED33BE2';
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 = `
<div class="bridge-chain-card">
<div class="chain-name"><i class="fas fa-network-wired"></i> CCIP Bridge Ecosystem</div>
<div style="color: var(--text-light); margin-bottom: 1rem;">
Cross-chain interoperability powered by Chainlink CCIP
</div>
</div>
<!-- Chain 138 Bridges -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-home"></i> Chain 138 (Source Chain)</h3>
</div>
<div class="chain-info" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH9Bridge</div>
<div class="chain-stat-value">
<span class="hash" onclick="showAddressDetail('${WETH9_BRIDGE_138}')" style="cursor: pointer; font-size: 0.9rem;">${WETH9_BRIDGE_138}</span>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
Token: <span class="hash" onclick="showAddressDetail('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')" style="cursor: pointer;">WETH9</span>
</div>
</div>
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH10Bridge</div>
<div class="chain-stat-value">
<span class="hash" onclick="showAddressDetail('${WETH10_BRIDGE_138}')" style="cursor: pointer; font-size: 0.9rem;">${WETH10_BRIDGE_138}</span>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
Token: <span class="hash" onclick="showAddressDetail('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')" style="cursor: pointer;">WETH10</span>
</div>
</div>
</div>
</div>
<!-- WETH9 Bridge Routes -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-route"></i> CCIPWETH9Bridge Routes</h3>
<span class="badge badge-success">7 Destinations</span>
</div>
<div style="overflow-x: auto;">
<table class="table">
<thead>
<tr>
<th>Destination Chain</th>
<th>Chain ID</th>
<th>Bridge Address</th>
</tr>
</thead>
<tbody>
`;
// Add WETH9 routes
for (const [chain, address] of Object.entries(routes.weth9)) {
const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
html += `
<tr>
<td><strong>${chain.replace(/\s*\\(\\d+\\)/, '')}</strong></td>
<td>${chainId}</td>
<td><span class="hash" onclick="showAddressDetail('${escapeHtml(address)}')" style="cursor: pointer;">${escapeHtml(shortenHash(address))}</span></td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
</div>
<!-- WETH10 Bridge Routes -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-route"></i> CCIPWETH10Bridge Routes</h3>
<span class="badge badge-success">7 Destinations</span>
</div>
<div style="overflow-x: auto;">
<table class="table">
<thead>
<tr>
<th>Destination Chain</th>
<th>Chain ID</th>
<th>Bridge Address</th>
</tr>
</thead>
<tbody>
`;
// Add WETH10 routes
for (const [chain, address] of Object.entries(routes.weth10)) {
const chainId = chain.match(/\\((\d+)\\)/)?.[1] || '';
html += `
<tr>
<td><strong>${chain.replace(/\s*\\(\\d+\\)/, '')}</strong></td>
<td>${chainId}</td>
<td><span class="hash" onclick="showAddressDetail('${escapeHtml(address)}')" style="cursor: pointer;">${escapeHtml(shortenHash(address))}</span></td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
</div>
<!-- Ethereum Mainnet Bridges -->
<div class="card" style="margin-bottom: 1.5rem;">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-ethereum"></i> Ethereum Mainnet Bridges</h3>
</div>
<div class="chain-info" style="grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem;">
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH9Bridge</div>
<div class="chain-stat-value">
<span class="hash" onclick="window.open('https://etherscan.io/address/${WETH9_BRIDGE_MAINNET}', '_blank')" style="cursor: pointer; font-size: 0.9rem;">${WETH9_BRIDGE_MAINNET}</span>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
<a href="https://etherscan.io/address/${WETH9_BRIDGE_MAINNET}" target="_blank" style="color: var(--primary);">View on Etherscan</a>
</div>
</div>
<div class="chain-stat" style="background: var(--light); padding: 1rem; border-radius: 8px;">
<div class="chain-stat-label" style="font-weight: 600; margin-bottom: 0.5rem;">CCIPWETH10Bridge</div>
<div class="chain-stat-value">
<span class="hash" onclick="window.open('https://etherscan.io/address/${WETH10_BRIDGE_MAINNET}', '_blank')" style="cursor: pointer; font-size: 0.9rem;">${WETH10_BRIDGE_MAINNET}</span>
</div>
<div style="margin-top: 0.5rem; font-size: 0.85rem; color: var(--text-light);">
<a href="https://etherscan.io/address/${WETH10_BRIDGE_MAINNET}" target="_blank" style="color: var(--primary);">View on Etherscan</a>
</div>
</div>
</div>
</div>
<!-- Bridge Information -->
<div class="card">
<h3><i class="fas fa-info-circle"></i> Bridge Information</h3>
<div style="line-height: 1.8;">
<p><strong>CCIP Bridge Ecosystem</strong> enables cross-chain transfers of WETH9 and WETH10 tokens using Chainlink CCIP (Cross-Chain Interoperability Protocol).</p>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">Supported Networks:</h4>
<ul style="margin-left: 2rem; margin-top: 0.5rem;">
<li><strong>Chain 138</strong> - Source chain with both bridge contracts</li>
<li><strong>Ethereum Mainnet</strong> - Destination with dedicated bridge contracts</li>
<li><strong>BSC</strong> - Binance Smart Chain</li>
<li><strong>Polygon</strong> - Polygon PoS</li>
<li><strong>Avalanche</strong> - Avalanche C-Chain</li>
<li><strong>Base</strong> - Base L2</li>
<li><strong>Arbitrum</strong> - Arbitrum One</li>
<li><strong>Optimism</strong> - Optimism Mainnet</li>
</ul>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">How to Use:</h4>
<ol style="margin-left: 2rem; margin-top: 0.5rem;">
<li>Click on any bridge address to view detailed information and transaction history</li>
<li>Use the bridge contracts to transfer WETH9 or WETH10 tokens between supported chains</li>
<li>All transfers are secured by Chainlink CCIP infrastructure</li>
</ol>
<h4 style="margin-top: 1.5rem; margin-bottom: 0.5rem;">CCIP Infrastructure:</h4>
<ul style="margin-left: 2rem; margin-top: 0.5rem;">
<li><strong>CCIP Router (Chain 138)</strong>: <span class="hash" onclick="showAddressDetail('0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e')" style="cursor: pointer;">0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e</span></li>
<li><strong>CCIP Sender (Chain 138)</strong>: <span class="hash" onclick="showAddressDetail('0x105F8A15b819948a89153505762444Ee9f324684')" style="cursor: pointer;">0x105F8A15b819948a89153505762444Ee9f324684</span></li>
</ul>
</div>
</div>
`;
container.innerHTML = html;
} catch (error) {
container.innerHTML = '<div class="error">Failed to load bridge data: ' + escapeHtml(error.message) + '</div>';
}
}
function safeBlockNumber(v) { const n = String(v).replace(/[^0-9]/g, ''); return n ? n : null; }
function safeTxHash(v) { const s = String(v); return /^0x[a-fA-F0-9]{64}$/.test(s) ? s : null; }
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');
window.location.hash = '#/block/' + blockNumber;
try { window.history.replaceState(null, '', '#' + '/block/' + blockNumber); } catch (e) {}
const container = document.getElementById('blockDetail');
updateBreadcrumb('block', blockNumber);
container.innerHTML = createSkeletonLoader('detail');
try {
let b;
// For ChainID 138, use Blockscout API directly
if (CHAIN_ID === 138) {
try {
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks/${blockNumber}`);
b = normalizeBlock(response);
if (!b) {
throw new Error('Block not found');
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load block: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showBlockDetail(\'' + escapeHtml(String(blockNumber)) + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
return;
}
} else {
const block = await fetchAPIWithRetry(`${API_BASE}/v1/blocks/138/${blockNumber}`);
if (block.data) {
b = block.data;
} else {
throw new Error('Block not found');
}
}
if (b) {
const timestamp = new Date(b.timestamp).toLocaleString();
const gasUsedPercent = b.gas_limit ? ((parseInt(b.gas_used || 0) / parseInt(b.gas_limit)) * 100).toFixed(2) : '0';
const baseFeeGwei = b.base_fee_per_gas ? (parseInt(b.base_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
const burntFeesEth = b.burnt_fees ? formatEther(b.burnt_fees) : '0';
container.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Block #${b.number}</h2>
<button onclick="exportBlockData(${b.number})" class="btn btn-primary" style="padding: 0.5rem 1rem;">
<i class="fas fa-download"></i> Export
</button>
</div>
<div class="info-row">
<div class="info-label">Block Number</div>
<div class="info-value">${b.number}</div>
</div>
<div class="info-row">
<div class="info-label">Hash</div>
<div class="info-value hash">${escapeHtml(b.hash || '')}</div>
</div>
<div class="info-row">
<div class="info-label">Parent Hash</div>
<div class="info-value hash" onclick="showBlockDetail('${escapeHtml(String(parseInt(b.number) - 1))}')" style="cursor: pointer;">${escapeHtml(b.parent_hash || '')}</div>
</div>
<div class="info-row">
<div class="info-label">Timestamp</div>
<div class="info-value">${escapeHtml(timestamp)}</div>
</div>
<div class="info-row">
<div class="info-label">Miner</div>
<div class="info-value hash" onclick="showAddressDetail('${escapeHtml(b.miner || '')}')" style="cursor: pointer;">${escapeHtml(b.miner || 'N/A')}</div>
</div>
<div class="info-row">
<div class="info-label">Transaction Count</div>
<div class="info-value">${b.transaction_count || 0}</div>
</div>
<div class="info-row">
<div class="info-label">Gas Used</div>
<div class="info-value">${formatNumber(b.gas_used || 0)} / ${formatNumber(b.gas_limit || 0)} (${gasUsedPercent}%)</div>
</div>
<div class="info-row">
<div class="info-label">Gas Limit</div>
<div class="info-value">${formatNumber(b.gas_limit || 0)}</div>
</div>
${b.base_fee_per_gas ? `
<div class="info-row">
<div class="info-label">Base Fee</div>
<div class="info-value">${baseFeeGwei} Gwei</div>
</div>
` : ''}
${b.burnt_fees && parseInt(b.burnt_fees) > 0 ? `
<div class="info-row">
<div class="info-label">Burnt Fees</div>
<div class="info-value">${burntFeesEth} ETH</div>
</div>
` : ''}
<div class="info-row">
<div class="info-label">Size</div>
<div class="info-value">${formatNumber(b.size || 0)} bytes</div>
</div>
<div class="info-row">
<div class="info-label">Difficulty</div>
<div class="info-value">${formatNumber(b.difficulty || 0)}</div>
</div>
<div class="info-row">
<div class="info-label">Nonce</div>
<div class="info-value">${escapeHtml(String(b.nonce || '0x0'))}</div>
</div>
`;
} else {
container.innerHTML = '<div class="error">Block not found</div>';
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load block: ' + escapeHtml(error.message) + '</div>';
}
}
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');
window.location.hash = '#/tx/' + txHash;
try { window.history.replaceState(null, '', '#' + '/tx/' + txHash); } catch (e) {}
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 = '<div class="error">Failed to load transaction: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showTransactionDetail(\'' + escapeHtml(String(txHash)) + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
return;
}
} else {
const tx = await fetchAPIWithRetry(`${API_BASE}/v1/transactions/138/${txHash}`);
if (tx.data) t = tx.data;
else throw new Error('Transaction not found');
}
if (!t) {
container.innerHTML = '<div class="error">Transaction not found</div>';
return;
}
const timestamp = new Date(t.created_at).toLocaleString();
const valueEth = formatEther(t.value || '0');
const gasPriceGwei = t.gas_price ? (parseInt(t.gas_price) / 1e9).toFixed(2) : 'N/A';
const maxFeeGwei = t.max_fee_per_gas ? (parseInt(t.max_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
const priorityFeeGwei = t.max_priority_fee_per_gas ? (parseInt(t.max_priority_fee_per_gas) / 1e9).toFixed(2) : 'N/A';
const burntFeeEth = t.tx_burnt_fee ? formatEther(t.tx_burnt_fee) : '0';
const totalFee = t.gas_used && t.gas_price ? formatEther((BigInt(t.gas_used) * BigInt(t.gas_price)).toString()) : '0';
const txType = t.type === 2 ? 'EIP-1559' : t.type === 1 ? 'EIP-2930' : 'Legacy';
const revertReason = t.revert_reason || (rawTx && (rawTx.revert_reason || rawTx.error || rawTx.result));
const inputHex = (t.input && t.input !== '0x') ? t.input : null;
const decodedInput = t.decoded_input || (rawTx && rawTx.decoded_input);
let mainHtml = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h2 style="margin: 0;">Transaction</h2>
<div style="display: flex; gap: 0.5rem;">
<button onclick="exportTransactionData('${t.hash}')" class="btn btn-primary" style="padding: 0.5rem 1rem;">
<i class="fas fa-download"></i> JSON
</button>
<button onclick="exportTransactionCSV('${t.hash}')" class="btn btn-primary" style="padding: 0.5rem 1rem;">
<i class="fas fa-file-csv"></i> CSV
</button>
</div>
</div>
<div class="info-row">
<div class="info-label">Transaction Hash</div>
<div class="info-value hash">${t.hash}</div>
</div>
<div class="info-row">
<div class="info-label">Type</div>
<div class="info-value"><span class="badge badge-primary">${txType}</span></div>
</div>
<div class="info-row">
<div class="info-label">Status</div>
<div class="info-value">
<span class="badge ${t.status === 1 ? 'badge-success' : 'badge-danger'}">
${t.status === 1 ? 'Success' : t.status === 0 ? 'Failed' : 'Pending'}
</span>
</div>
</div>
<div class="info-row">
<div class="info-label">Block Number</div>
<div class="info-value hash" onclick="showBlockDetail('${escapeHtml(String(t.block_number || ''))}')" style="cursor: pointer;">${escapeHtml(String(t.block_number || 'N/A'))}</div>
</div>
<div class="info-row">
<div class="info-label">Block Hash</div>
<div class="info-value hash">${escapeHtml(t.block_hash || 'N/A')}</div>
</div>
<div class="info-row">
<div class="info-label">From</div>
<div class="info-value hash" onclick="showAddressDetail('${escapeHtml(t.from || '')}')" style="cursor: pointer;">${escapeHtml(t.from || '')}</div>
</div>
<div class="info-row">
<div class="info-label">To</div>
<div class="info-value hash" onclick="showAddressDetail('${escapeHtml(t.to || 'N/A')}')" style="cursor: pointer;">${escapeHtml(t.to || 'N/A')}</div>
</div>
<div class="info-row">
<div class="info-label">Value</div>
<div class="info-value">${valueEth} ETH</div>
</div>
<div class="info-row">
<div class="info-label">Gas Used</div>
<div class="info-value">${t.gas_used ? formatNumber(t.gas_used) : 'N/A'}</div>
</div>
<div class="info-row">
<div class="info-label">Gas Limit</div>
<div class="info-value">${t.gas_limit ? formatNumber(t.gas_limit) : 'N/A'}</div>
</div>
${t.max_fee_per_gas ? `<div class="info-row"><div class="info-label">Max Fee Per Gas</div><div class="info-value">${maxFeeGwei} Gwei</div></div>` : ''}
${t.max_priority_fee_per_gas ? `<div class="info-row"><div class="info-label">Max Priority Fee</div><div class="info-value">${priorityFeeGwei} Gwei</div></div>` : ''}
${!t.max_fee_per_gas && t.gas_price ? `<div class="info-row"><div class="info-label">Gas Price</div><div class="info-value">${gasPriceGwei} Gwei</div></div>` : ''}
<div class="info-row">
<div class="info-label">Total Fee</div>
<div class="info-value">${totalFee} ETH</div>
</div>
${t.tx_burnt_fee && parseInt(t.tx_burnt_fee) > 0 ? `<div class="info-row"><div class="info-label">Burnt Fee</div><div class="info-value">${burntFeeEth} ETH</div></div>` : ''}
<div class="info-row"><div class="info-label">Nonce</div><div class="info-value">${t.nonce || 'N/A'}</div></div>
<div class="info-row"><div class="info-label">Timestamp</div><div class="info-value">${timestamp}</div></div>
${t.contract_address ? `<div class="info-row"><div class="info-label">Contract Address</div><div class="info-value hash" onclick="showAddressDetail('${escapeHtml(t.contract_address)}')" style="cursor: pointer;">${escapeHtml(t.contract_address)}</div></div>` : ''}
`;
if (revertReason && t.status !== 1) {
const reasonStr = typeof revertReason === 'string' ? revertReason : (revertReason.message || JSON.stringify(revertReason));
mainHtml += `
<div class="card" style="margin-top: 1rem; border-left: 4px solid var(--danger);">
<h3 style="color: var(--danger); margin-bottom: 0.5rem;"><i class="fas fa-exclamation-triangle"></i> Revert Reason</h3>
<pre style="background: var(--light); padding: 1rem; border-radius: 8px; overflow-x: auto; font-size: 0.875rem;">${escapeHtml(reasonStr)}</pre>
</div>
`;
}
if (inputHex || decodedInput) {
mainHtml += `<div class="card" style="margin-top: 1rem;"><h3>Input Data</h3>`;
if (decodedInput && (decodedInput.method || decodedInput.params)) {
const method = decodedInput.method || decodedInput.name || 'Unknown';
mainHtml += `<p style="margin-bottom: 0.5rem;"><strong>Method:</strong> ${escapeHtml(method)}</p>`;
if (decodedInput.params && Array.isArray(decodedInput.params)) {
mainHtml += '<table class="table"><thead><tr><th>Param</th><th>Value</th></tr></thead><tbody>';
decodedInput.params.forEach(function(p) {
const name = (p.name || p.type || '');
const val = typeof p.value !== 'undefined' ? String(p.value) : (p.type || '');
mainHtml += '<tr><td>' + escapeHtml(name) + '</td><td class="hash" style="word-break: break-all;">' + escapeHtml(val) + '</td></tr>';
});
mainHtml += '</tbody></table>';
}
}
if (inputHex) {
mainHtml += `<p style="margin-top: 0.5rem;"><strong>Hex:</strong> <code id="txInputHex" style="word-break: break-all; font-size: 0.8rem;">${escapeHtml(inputHex)}</code> <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'txInputHex\').textContent); showToast(\'Copied\', \'success\');">Copy</button></p>`;
}
mainHtml += '</div>';
}
container.innerHTML = mainHtml;
if (CHAIN_ID === 138) {
const internalCard = document.createElement('div');
internalCard.className = 'card';
internalCard.style.marginTop = '1rem';
internalCard.innerHTML = '<h3><i class="fas fa-sitemap"></i> Internal Transactions</h3><div id="txInternalTxs" class="loading">Loading...</div>';
container.appendChild(internalCard);
const logsCard = document.createElement('div');
logsCard.className = 'card';
logsCard.style.marginTop = '1rem';
logsCard.innerHTML = '<h3><i class="fas fa-list"></i> Event Logs</h3><div id="txLogs" class="loading">Loading...</div>';
container.appendChild(logsCard);
Promise.all([
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/internal-transactions`).catch(function() { return { items: [] }; }),
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/internal_transactions`).catch(function() { return { items: [] }; }),
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/logs`).catch(function() { return { items: [] }; }),
fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}/log_entries`).catch(function() { return { items: [] }; })
]).then(function(results) {
const internalResp = results[0].items ? results[0] : results[1];
const logsResp = results[2].items ? results[2] : results[3];
const internals = internalResp.items || [];
const logs = logsResp.items || logsResp.log_entries || [];
const internalEl = document.getElementById('txInternalTxs');
if (internalEl) {
if (internals.length === 0) {
internalEl.innerHTML = '<p style="color: var(--text-light);">No internal transactions</p>';
} else {
let tbl = '<table class="table"><thead><tr><th>Type</th><th>From</th><th>To</th><th>Value</th></tr></thead><tbody>';
internals.forEach(function(it) {
const from = it.from?.hash || it.from || 'N/A';
const to = it.to?.hash || it.to || 'N/A';
const val = it.value ? formatEther(it.value) : '0';
const type = it.type || it.call_type || 'call';
tbl += '<tr><td>' + escapeHtml(type) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(from) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(from)) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(to) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(to)) + '</td><td>' + escapeHtml(val) + ' ETH</td></tr>';
});
tbl += '</tbody></table>';
internalEl.innerHTML = tbl;
}
}
const logsEl = document.getElementById('txLogs');
if (logsEl) {
if (logs.length === 0) {
logsEl.innerHTML = '<p style="color: var(--text-light);">No event logs</p>';
} else {
let tbl = '<table class="table"><thead><tr><th>Address</th><th>Topics</th><th>Data</th></tr></thead><tbody>';
logs.forEach(function(log) {
const addr = log.address?.hash || log.address || 'N/A';
const topics = (log.topics && Array.isArray(log.topics)) ? log.topics.join(', ') : (log.topic0 || log.topics || '-');
const data = log.data || log.raw_data || '0x';
tbl += '<tr><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(addr) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(addr)) + '</td><td style="word-break: break-all; font-size: 0.75rem;">' + escapeHtml(String(topics).substring(0, 80)) + (String(topics).length > 80 ? '...' : '') + '</td><td style="word-break: break-all; font-size: 0.75rem;">' + escapeHtml(String(data).substring(0, 66)) + (String(data).length > 66 ? '...' : '') + '</td></tr>';
});
tbl += '</tbody></table>';
logsEl.innerHTML = tbl;
}
}
});
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load transaction: ' + escapeHtml(error.message) + '</div>';
}
}
function escapeHtml(str) {
if (str == null) return '';
const s = String(str);
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
}
function exportTransactionCSV(txHash) {
fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions/' + txHash).then(function(r) {
var t = normalizeTransaction(r);
if (!t) return;
var rows = [['Field', 'Value'], ['hash', t.hash], ['from', t.from], ['to', t.to || ''], ['value', t.value || '0'], ['block_number', t.block_number || ''], ['status', t.status], ['gas_used', t.gas_used || ''], ['gas_limit', t.gas_limit || '']];
var csv = rows.map(function(row) { return row.map(function(cell) { return '"' + String(cell).replace(/"/g, '""') + '"'; }).join(','); }).join('\n');
var blob = new Blob([csv], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a'); a.href = url; a.download = 'transaction-' + txHash.substring(0, 10) + '.csv'; a.click();
URL.revokeObjectURL(url);
}).catch(function(e) { showToast('Export failed: ' + (e.message || 'Unknown'), 'error'); });
}
function exportBlocksCSV() {
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'); });
}
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');
window.location.hash = '#/address/' + address;
try { window.history.replaceState(null, '', '#' + '/address/' + address); } catch (e) {}
const container = document.getElementById('addressDetail');
updateBreadcrumb('address', address);
container.innerHTML = createSkeletonLoader('detail');
try {
// Validate address format
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) {
container.innerHTML = '<div class="error">Invalid address format</div>';
return;
}
let a;
// For ChainID 138, use Blockscout API directly
if (CHAIN_ID === 138) {
try {
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/addresses/${address}`);
var raw = response && (response.data !== undefined ? response.data : response.address !== undefined ? response.address : response.items && response.items[0] !== undefined ? response.items[0] : response);
a = normalizeAddress(raw);
if (!a || !a.hash) {
throw new Error('Address not found');
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load address: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showAddressDetail(\'' + address + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
return;
}
} else {
const addr = await fetchAPIWithRetry(`${API_BASE}/v1/addresses/138/${address}`);
if (addr.data) {
a = addr.data;
} else {
throw new Error('Address not found');
}
}
if (a) {
const balanceEth = formatEther(a.balance || '0');
const isContract = !!a.is_contract;
const verifiedBadge = a.is_verified ? '<span class="badge badge-success" style="margin-left: 0.5rem;">Verified</span>' : '';
const contractLink = isContract ? `<a href="https://explorer.d-bis.org/address/${address}/contract" target="_blank" rel="noopener" style="color: var(--primary); font-size: 0.875rem;">View contract on Blockscout</a>` : '';
container.innerHTML = `
<div class="info-row">
<div class="info-label">Address</div>
<div class="info-value hash">${address}</div>
</div>
<div class="info-row">
<div class="info-label">Balance</div>
<div class="info-value">${balanceEth} ETH</div>
</div>
<div class="info-row">
<div class="info-label">Transaction Count</div>
<div class="info-value">${a.transaction_count || 0}</div>
</div>
<div class="info-row">
<div class="info-label">Token Count</div>
<div class="info-value">${a.token_count || 0}</div>
</div>
<div class="info-row">
<div class="info-label">Type</div>
<div class="info-value">${a.is_contract ? '<span class="badge badge-success">Contract</span>' + verifiedBadge + (contractLink ? '<br/>' + contractLink : '') : '<span class="badge badge-primary">EOA</span>'}</div>
</div>
<div class="tabs" style="margin-top: 1.5rem;">
<button class="tab active" onclick="switchAddressTab('transactions', '${address}')" id="addrTabTxs" aria-selected="true">Transactions</button>
<button class="tab" onclick="switchAddressTab('tokens', '${address}')" id="addrTabTokens">Token Balances</button>
<button class="tab" onclick="switchAddressTab('internal', '${address}')" id="addrTabInternal">Internal Txns</button>
${isContract ? '<button class="tab" onclick="switchAddressTab(\'contract\', \'' + address + '\')" id="addrTabContract">Contract (ABI / Bytecode)</button>' : ''}
</div>
<div id="addressTabTransactions" class="address-tab-content card" style="margin-top: 1rem;">
<h3>Recent Transactions</h3>
<div id="addressTransactions" class="loading">Loading transactions...</div>
</div>
<div id="addressTabTokens" class="address-tab-content card" style="margin-top: 1rem; display: none;">
<h3>Token Balances</h3>
<div id="addressTokenBalances" class="loading">Loading...</div>
</div>
<div id="addressTabInternal" class="address-tab-content card" style="margin-top: 1rem; display: none;">
<h3>Internal Transactions</h3>
<div id="addressInternalTxns" class="loading">Loading...</div>
</div>
${isContract ? '<div id="addressTabContract" class="address-tab-content card" style="margin-top: 1rem; display: none;"><h3>Contract ABI & Bytecode</h3><div id="addressContractInfo" class="loading">Loading...</div></div>' : ''}
`;
function switchAddressTab(tabName, addr) {
document.querySelectorAll('.address-tab-content').forEach(function(el) { el.style.display = 'none'; });
document.querySelectorAll('.tabs .tab').forEach(function(t) { t.classList.remove('active'); });
if (tabName === 'transactions') {
document.getElementById('addressTabTransactions').style.display = 'block';
document.getElementById('addrTabTxs').classList.add('active');
} else if (tabName === 'tokens') {
document.getElementById('addressTabTokens').style.display = 'block';
document.getElementById('addrTabTokens').classList.add('active');
loadAddressTokenBalances(addr);
} else if (tabName === 'internal') {
document.getElementById('addressTabInternal').style.display = 'block';
document.getElementById('addrTabInternal').classList.add('active');
loadAddressInternalTxns(addr);
} else if (tabName === '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 = '<p style="color: var(--text-light);">No token balances</p>';
return;
}
let tbl = '<table class="table"><thead><tr><th>Token</th><th>Contract</th><th>Balance</th><th>Type</th></tr></thead><tbody>';
items.forEach(function(b) {
const 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 += '<tr><td><a href="#/token/' + encodeURIComponent(contract) + '">' + escapeHtml(symbol) + '</a></td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(contract) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(contract)) + '</td><td>' + escapeHtml(displayBalance) + '</td><td>' + escapeHtml(type) + '</td></tr>';
});
tbl += '</tbody></table>';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '<p class="error">Failed to load token balances</p>';
}
}
async function loadAddressInternalTxns(addr) {
const el = document.getElementById('addressInternalTxns');
if (!el || el.dataset.loaded === '1') return;
try {
const r = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/internal-transactions').catch(function() { return { items: [] }; });
const r2 = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/addresses/' + addr + '/internal_transactions').catch(function() { return { items: [] }; });
const items = (r.items || r).length ? (r.items || r) : (r2.items || r2);
el.dataset.loaded = '1';
if (!items || items.length === 0) {
el.innerHTML = '<p style="color: var(--text-light);">No internal transactions</p>';
return;
}
let tbl = '<table class="table"><thead><tr><th>Block</th><th>From</th><th>To</th><th>Value</th><th>Tx Hash</th></tr></thead><tbody>';
items.slice(0, 25).forEach(function(it) {
const 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 += '<tr><td onclick="showBlockDetail(\'' + escapeHtml(block) + '\')" style="cursor: pointer;">' + escapeHtml(block) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(from) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(from)) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(to) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(to)) + '</td><td>' + escapeHtml(val) + ' ETH</td><td class="hash" onclick="showTransactionDetail(\'' + escapeHtml(txHash) + '\')" style="cursor: pointer;">' + (txHash !== '-' ? escapeHtml(shortenHash(txHash)) : '-') + '</td></tr>';
});
tbl += '</tbody></table>';
el.innerHTML = tbl;
} catch (e) {
el.innerHTML = '<p class="error">Failed to load internal transactions</p>';
}
}
async function loadAddressContractInfo(addr) {
const el = document.getElementById('addressContractInfo');
if (!el || el.dataset.loaded === '1') return;
try {
const urls = [
BLOCKSCOUT_API + '/v2/smart-contracts/' + addr,
BLOCKSCOUT_API + '/v2/contracts/' + addr
];
let data = null;
for (var i = 0; i < urls.length; i++) {
try {
const r = await fetchAPIWithRetry(urls[i]);
if (r && (r.abi || r.bytecode || r.deployed_bytecode)) { data = r; break; }
} catch (e) {}
}
el.dataset.loaded = '1';
if (!data) {
el.innerHTML = '<p style="color: var(--text-light);">Contract source not indexed. <a href="https://explorer.d-bis.org/address/' + encodeURIComponent(addr) + '/contract" target="_blank" rel="noopener">Verify on Blockscout</a></p>';
return;
}
const abi = data.abi || data.abi_interface || [];
const abiStr = typeof abi === 'string' ? abi : JSON.stringify(abi, null, 2);
const bytecode = data.bytecode || data.deployed_bytecode || data.creation_bytecode || '-';
let html = '<p><strong>Verification:</strong> ' + (data.is_verified ? '<span class="badge badge-success">Verified</span>' : '<span class="badge badge-warning">Unverified</span>') + '</p>';
html += '<div style="margin-top: 1rem;"><h4>ABI <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'contractAbiText\').textContent); showToast(\'Copied\', \'success\');">Copy</button> <a href="javascript:void(0)" onclick="var blob=new Blob([document.getElementById(\'contractAbiText\').textContent],{type:\'application/json\'}); var a=document.createElement(\'a\'); a.href=URL.createObjectURL(blob); a.download=\'abi-' + addr.substring(0,10) + '.json\'; a.click(); URL.revokeObjectURL(a.href); return false;">Download</a></h4>';
html += '<pre id="contractAbiText" style="background: var(--light); padding: 1rem; border-radius: 8px; overflow: auto; max-height: 300px; font-size: 0.75rem;">' + escapeHtml(abiStr) + '</pre></div>';
html += '<div style="margin-top: 1rem;"><h4>Bytecode <button type="button" class="btn btn-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;" onclick="navigator.clipboard.writeText(document.getElementById(\'contractBytecodeText\').textContent); showToast(\'Copied\', \'success\');">Copy</button></h4>';
html += '<pre id="contractBytecodeText" style="background: var(--light); padding: 1rem; border-radius: 8px; overflow: auto; max-height: 150px; font-size: 0.7rem; word-break: break-all;">' + escapeHtml(String(bytecode).substring(0, 500)) + (String(bytecode).length > 500 ? '...' : '') + '</pre></div>';
html += '<p style="margin-top: 0.5rem;"><a href="https://explorer.d-bis.org/address/' + encodeURIComponent(addr) + '/contract" target="_blank" rel="noopener" style="color: var(--primary);">Read / Write contract on Blockscout</a></p>';
el.innerHTML = html;
} catch (e) {
el.innerHTML = '<p class="error">Failed to load contract info</p>';
}
}
try {
let txs;
if (CHAIN_ID === 138) {
const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?address=${address}&page=1&page_size=10`);
const rawTxs = response.items || [];
txs = { data: rawTxs.map(normalizeTransaction).filter(tx => tx !== null) };
} else {
txs = await fetchAPIWithRetry(`${API_BASE}/v1/transactions?from_address=${address}&page_size=10`);
}
const txContainer = document.getElementById('addressTransactions');
if (txContainer) {
if (txs.data && txs.data.length > 0) {
let txHtml = '<table class="table"><thead><tr><th>Hash</th><th>Block</th><th>From</th><th>To</th><th>Value</th></tr></thead><tbody>';
txs.data.forEach(function(tx) {
txHtml += '<tr onclick="showTransactionDetail(\'' + escapeHtml(tx.hash) + '\')" style="cursor: pointer;"><td class="hash">' + escapeHtml(shortenHash(tx.hash)) + '</td><td>' + escapeHtml(String(tx.block_number)) + '</td><td class="hash">' + escapeHtml(shortenHash(tx.from)) + '</td><td class="hash">' + escapeHtml(shortenHash(tx.to || 'N/A')) + '</td><td>' + escapeHtml(formatEther(tx.value || '0')) + ' ETH</td></tr>';
});
txHtml += '</tbody></table>';
txContainer.innerHTML = txHtml;
} else {
txContainer.innerHTML = '<p>No transactions found</p>';
}
}
} catch (e) {
const txContainer = document.getElementById('addressTransactions');
if (txContainer) txContainer.innerHTML = '<p>Failed to load transactions</p>';
}
} else {
container.innerHTML = '<div class="error">Address not found</div>';
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load address: ' + escapeHtml(error.message) + '</div>';
}
}
window.showAddressDetail = showAddressDetail;
async function showTokenDetail(tokenAddress) {
if (!/^0x[a-fA-F0-9]{40}$/.test(tokenAddress)) return;
currentDetailKey = 'token:' + tokenAddress.toLowerCase();
showView('tokenDetail');
window.location.hash = '#/token/' + tokenAddress;
try { window.history.replaceState(null, '', '#' + '/token/' + tokenAddress); } catch (e) {}
var container = document.getElementById('tokenDetail');
updateBreadcrumb('token', tokenAddress);
container.innerHTML = createSkeletonLoader('detail');
try {
var urls = [BLOCKSCOUT_API + '/v2/tokens/' + tokenAddress, BLOCKSCOUT_API + '/v2/token/' + tokenAddress];
var data = null;
for (var i = 0; i < urls.length; i++) {
try {
var r = await fetchAPIWithRetry(urls[i]);
if (r && (r.symbol || r.name || r.total_supply != null)) { data = r; break; }
} catch (e) {}
}
if (!data) {
container.innerHTML = '<p class="error">Token not found or not indexed.</p><p><a href="#/address/' + encodeURIComponent(tokenAddress) + '">View as address</a></p>';
return;
}
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 = '<div class="info-row"><div class="info-label">Contract</div><div class="info-value hash" onclick="showAddressDetail(\'' + escapeHtml(tokenAddress) + '\')" style="cursor: pointer;">' + escapeHtml(tokenAddress) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Name</div><div class="info-value">' + escapeHtml(name) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Symbol</div><div class="info-value">' + escapeHtml(symbol) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Decimals</div><div class="info-value">' + decimals + '</div></div>';
html += '<div class="info-row"><div class="info-label">Total Supply</div><div class="info-value">' + supplyNum.toLocaleString(undefined, { maximumFractionDigits: 6 }) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Holders</div><div class="info-value">' + (holders !== '-' ? formatNumber(holders) : '-') + '</div></div>';
html += '<div class="card" style="margin-top: 1rem;"><h3>Recent Transfers</h3>';
if (transfers.length === 0) {
html += '<p style="color: var(--text-light);">No transfers</p>';
} else {
html += '<table class="table"><thead><tr><th>From</th><th>To</th><th>Value</th><th>Tx</th></tr></thead><tbody>';
transfers.forEach(function(tr) {
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 += '<tr><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(from) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(from)) + '</td><td class="hash" onclick="showAddressDetail(\'' + escapeHtml(to) + '\')" style="cursor: pointer;">' + escapeHtml(shortenHash(to)) + '</td><td>' + escapeHtml(v.toLocaleString(undefined, { maximumFractionDigits: 6 })) + '</td><td class="hash" onclick="showTransactionDetail(\'' + escapeHtml(txHash) + '\')" style="cursor: pointer;">' + (txHash ? escapeHtml(shortenHash(txHash)) : '-') + '</td></tr>';
});
html += '</tbody></table>';
}
html += '</div>';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '<div class="error">Failed to load token: ' + escapeHtml(err.message || 'Unknown') + '</div>';
}
}
window.showTokenDetail = showTokenDetail;
async function showNftDetail(contractAddress, tokenId) {
if (!/^0x[a-fA-F0-9]{40}$/.test(contractAddress)) return;
currentDetailKey = 'nft:' + contractAddress.toLowerCase() + ':' + tokenId;
showView('nftDetail');
try { window.history.replaceState(null, '', '#' + '/nft/' + contractAddress + '/' + tokenId); } catch (e) {}
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 = '<div class="info-row"><div class="info-label">Contract</div><div class="info-value hash" onclick="showAddressDetail(\'' + escapeHtml(contractAddress) + '\')" style="cursor: pointer;">' + escapeHtml(contractAddress) + '</div></div>';
html += '<div class="info-row"><div class="info-label">Token ID</div><div class="info-value">' + escapeHtml(String(tokenId)) + '</div></div>';
if (data) {
if (data.metadata && data.metadata.image) {
html += '<div class="info-row"><div class="info-label">Image</div><div class="info-value"><img src="' + escapeHtml(data.metadata.image) + '" alt="NFT" style="max-width: 200px; border-radius: 8px;" onerror="this.style.display=\'none\'"></div></div>';
}
if (data.name) html += '<div class="info-row"><div class="info-label">Name</div><div class="info-value">' + escapeHtml(data.name) + '</div></div>';
if (data.description) html += '<div class="info-row"><div class="info-label">Description</div><div class="info-value">' + escapeHtml(data.description) + '</div></div>';
if (data.owner) { var ownerAddr = (data.owner.hash || data.owner); html += '<div class="info-row"><div class="info-label">Owner</div><div class="info-value hash" onclick="showAddressDetail(\'' + escapeHtml(ownerAddr) + '\')" style="cursor: pointer;">' + escapeHtml(ownerAddr) + '</div></div>'; }
}
html += '<p style="margin-top: 1rem;"><a href="https://explorer.d-bis.org/token/' + encodeURIComponent(contractAddress) + '/instance/' + encodeURIComponent(tokenId) + '" target="_blank" rel="noopener" style="color: var(--primary);">View on Blockscout</a></p>';
container.innerHTML = html;
} catch (err) {
container.innerHTML = '<div class="error">Failed to load NFT: ' + escapeHtml(err.message || 'Unknown') + '</div>';
}
}
window.showNftDetail = showNftDetail;
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) {
var item = searchResults.items[0];
var type = item.type || item.address_type || '';
if (item.address_hash || item.hash) {
var addr = item.address_hash || item.hash;
await showAddressDetail(addr);
return;
}
if (item.tx_hash || item.hash) {
var txHash = item.tx_hash || item.hash;
if (txHash.length === 66) {
await showTransactionDetail(txHash);
return;
}
}
if (item.block_number != null) {
await showBlockDetail(String(item.block_number));
return;
}
}
if (/^0x[a-f0-9]{8,63}$/i.test(normalizedQuery)) {
try {
var txResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions/' + normalizedQuery);
if (txResp && (txResp.hash || txResp.tx_hash)) {
await showTransactionDetail(txResp.hash || txResp.tx_hash);
return;
}
} catch (e) {}
showToast('No unique result for partial hash. Enter full transaction hash (0x + 64 hex).', 'info');
return;
}
}
showToast('Invalid search. Try address (0x...40 hex), transaction hash (0x...64 hex), block number, or search by 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();
});
}
});
</script>
</body>
</html>