';
}
}
/**
* Parse amount from WETH wrap/unwrap field. Label is "Amount (ETH)".
* - If input looks like raw wei (integer string, 18+ digits, no decimal), use as wei.
* - Otherwise treat as ETH and convert with parseEther (e.g. "100" -> 100 ETH).
* So pasting 100000000000000000000 wraps 100 ETH; typing 100 also wraps 100 ETH.
*/
function parseWETHAmount(inputStr) {
var s = (inputStr && String(inputStr).trim()) || '';
if (!s) return null;
try {
if (/^\d+$/.test(s) && s.length >= 18) {
return ethers.BigNumber.from(s);
}
return ethers.utils.parseEther(s);
} catch (e) {
return null;
}
}
// WETH ABI (Standard ERC-20 + WETH functions)
const WETH_ABI = [
"function deposit() payable",
"function withdraw(uint256 wad)",
"function balanceOf(address account) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
"function totalSupply() view returns (uint256)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"event Deposit(address indexed dst, uint256 wad)",
"event Withdrawal(address indexed src, uint256 wad)"
];
// Helper function to check if ethers is loaded
function ensureEthers() {
// Check immediately - ethers might already be loaded
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
return Promise.resolve(true);
}
// Wait for ethers to load if it's still loading
return new Promise((resolve, reject) => {
let resolved = false;
// Check immediately first (double check)
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
resolve(true);
return;
}
// Wait for ethersReady event
const timeout = setTimeout(() => {
if (!resolved) {
resolved = true;
// Final check before rejecting
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
resolve(true);
} else {
console.error('Ethers library failed to load after 20 seconds');
reject(new Error('Ethers library failed to load. Please refresh the page.'));
}
}
}, 20000); // 20 second timeout
const checkInterval = setInterval(() => {
if (typeof ethers !== 'undefined' && !resolved) {
resolved = true;
clearInterval(checkInterval);
clearTimeout(timeout);
window.ethersReady = true;
console.log('✅ Ethers detected via polling');
resolve(true);
}
}, 100);
// Listen for ethersReady event
const onReady = function() {
if (!resolved) {
resolved = true;
clearInterval(checkInterval);
clearTimeout(timeout);
window.removeEventListener('ethersReady', onReady);
if (typeof ethers !== 'undefined') {
window.ethersReady = true;
console.log('✅ Ethers ready via event');
resolve(true);
} else {
console.error('ethersReady event fired but ethers is still undefined');
reject(new Error('Ethers library is not loaded. Please refresh the page.'));
}
}
};
window.addEventListener('ethersReady', onReady, { once: true });
});
}
// Helper function to get API URL based on chain ID
function getAPIUrl(endpoint) {
// For ChainID 138, use Blockscout API
if (CHAIN_ID === 138) {
return `${BLOCKSCOUT_API}${endpoint}`;
}
// For other networks, use v2 Etherscan/Blockscan APIs
return `${API_BASE}/v2${endpoint}`;
}
// Initialize - only run once
let initialized = false;
document.addEventListener('DOMContentLoaded', async () => {
if (initialized) {
console.warn('Initialization already completed, skipping...');
return;
}
initialized = true;
applyStoredTheme();
var localeSel = document.getElementById('localeSelect'); if (localeSel) localeSel.value = currentLocale;
if (typeof applyI18n === 'function') applyI18n();
var initialRoute = (window.location.pathname || '/').replace(/^\//, '').replace(/\/$/, '').replace(/^index\.html$/i, '');
applyHashRoute();
window.addEventListener('popstate', function() { applyHashRoute(); });
window.addEventListener('hashchange', function() { applyHashRoute(); });
window.addEventListener('load', function() { applyHashRoute(); });
var shouldLoadHomeData = !initialRoute || initialRoute === 'home';
if (shouldLoadHomeData) {
console.log('Loading stats, blocks, and transactions...');
loadStats();
loadLatestBlocks();
loadLatestTransactions();
startTransactionUpdates();
} else {
setTimeout(function() { applyHashRoute(); }, 0);
}
// 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 (_) {
// User has not authorized the site yet; skip auto-connect silently
}
}
} finally {
checkingMetaMask = false;
}
}
var ERC20_META_ABI = ['function symbol() view returns (string)', 'function name() view returns (string)', 'function decimals() view returns (uint8)'];
var SYMBOL_SELECTOR = '0x95d89b41';
var NAME_SELECTOR = '0x06fdde03';
var DECIMALS_SELECTOR = '0x313ce567';
function decodeBytes32OrString(hex) {
if (!hex || typeof hex !== 'string' || hex.length < 66) return '';
var data = hex.slice(2);
if (data.length >= 64) {
var offset = parseInt(data.slice(0, 64), 16);
var len = parseInt(data.slice(64, 128), 16);
if (offset === 32 && len > 0 && data.length >= 128 + len * 2) {
return ethers.utils.toUtf8String('0x' + data.slice(128, 128 + len * 2)).replace(/\0+$/g, '');
}
var fixed = '0x' + data.slice(0, 64);
try {
return ethers.utils.parseBytes32String(fixed).replace(/\0+$/g, '');
} catch (_) {
return ethers.utils.toUtf8String(fixed).replace(/\0+$/g, '');
}
}
return '';
}
async function fetchTokenMetadataFromChain(address) {
if (typeof window.ethereum === 'undefined' || typeof ethers === 'undefined') return null;
var prov = new ethers.providers.Web3Provider(window.ethereum);
try {
var contract = new ethers.Contract(address, ERC20_META_ABI, prov);
var sym = await contract.symbol();
var nam = await contract.name();
var dec = await contract.decimals();
var decimalsNum = (typeof dec === 'number') ? dec : (dec && dec.toNumber ? dec.toNumber() : 18);
var symbolStr = (sym != null && sym !== undefined) ? String(sym) : '';
var nameStr = (nam != null && nam !== undefined) ? String(nam) : '';
return { symbol: symbolStr, name: nameStr, decimals: decimalsNum };
} catch (e) {
try {
var outSymbol = await window.ethereum.request({ method: 'eth_call', params: [{ to: address, data: SYMBOL_SELECTOR }] });
var outName = await window.ethereum.request({ method: 'eth_call', params: [{ to: address, data: NAME_SELECTOR }] });
var outDecimals = await window.ethereum.request({ method: 'eth_call', params: [{ to: address, data: DECIMALS_SELECTOR }] });
if (outSymbol && outSymbol !== '0x') {
var symbolStr = decodeBytes32OrString(outSymbol);
var nameStr = outName && outName !== '0x' ? decodeBytes32OrString(outName) : '';
var decimalsNum = 18;
if (outDecimals && outDecimals.length >= 66) {
decimalsNum = parseInt(outDecimals.slice(2, 66), 16);
if (isNaN(decimalsNum)) decimalsNum = 18;
}
return { symbol: symbolStr, name: nameStr, decimals: decimalsNum };
}
} catch (_) {}
return null;
}
}
async function addTokenToWallet(address, symbol, decimals, name) {
if (!address || !/^0x[a-fA-F0-9]{40}$/i.test(address)) {
if (typeof showToast === 'function') showToast('Invalid token address', 'error');
return;
}
if (typeof window.ethereum === 'undefined') {
if (typeof showToast === 'function') showToast('No wallet detected. Install MetaMask or another Web3 wallet.', 'error');
return;
}
try {
var meta = await fetchTokenMetadataFromChain(address);
if (!meta) {
if (typeof showToast === 'function') showToast('Could not read token from chain. Switch to the correct network and try again.', 'error');
return;
}
var useSymbol = (meta.symbol !== undefined && meta.symbol !== null) ? meta.symbol : 'TOKEN';
var useName = (meta.name !== undefined && meta.name !== null) ? meta.name : (name || '');
var useDecimals = (typeof meta.decimals === 'number') ? meta.decimals : (typeof decimals === 'number' ? decimals : 18);
if (useSymbol === '' || (typeof useSymbol === 'string' && useSymbol.trim() === '')) {
if (typeof showToast === 'function') showToast('This token has no symbol on-chain. Add it manually in MetaMask: use this contract address and set symbol to WETH.', 'info');
return;
}
var added = await window.ethereum.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20',
options: {
address: address,
symbol: (useSymbol !== undefined && useSymbol !== null) ? useSymbol : 'TOKEN',
decimals: useDecimals,
name: useName || undefined,
image: undefined
}
}
});
if (typeof showToast === 'function') {
var displaySym = useSymbol || symbol || 'Token';
showToast(added ? (displaySym ? displaySym + ' added to wallet' : 'Token added to wallet') : 'Add token was cancelled', added ? 'success' : 'info');
}
} catch (e) {
var msg = (e && e.message) ? String(e.message) : '';
var friendly = (e && e.code === 4001) || /not been authorized|rejected|denied/i.test(msg)
? 'Please approve the MetaMask popup to add the token.'
: (msg || 'Could not add token to wallet.');
if (typeof showToast === 'function') showToast(friendly, 'error');
}
}
window.addTokenToWallet = addTokenToWallet;
let connectingMetaMask = false;
async function connectMetaMask() {
// Prevent multiple simultaneous connections
if (connectingMetaMask) {
console.log('connectMetaMask already in progress, skipping...');
return;
}
connectingMetaMask = true;
try {
if (typeof window.ethereum === 'undefined') {
alert('MetaMask is not installed! Please install MetaMask to use WETH utilities.');
return;
}
// Wait for ethers to be loaded
if (typeof ethers === 'undefined') {
try {
await ensureEthers();
// Wait a bit more to ensure ethers is fully initialized
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
alert('Ethers library is not loaded. Please refresh the page and try again.');
console.error('ethers loading error:', error);
return;
}
}
// Double-check ethers is available
if (typeof ethers === 'undefined') {
alert('Ethers library is not loaded. Please refresh the page and try again.');
console.error('Ethers still not available after ensureEthers()');
return;
}
try {
// Request account access
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
userAddress = accounts[0];
// Connect to Chain 138
await switchToChain138();
// Setup provider and signer
provider = new ethers.providers.Web3Provider(window.ethereum);
signer = provider.getSigner();
// Update UI
const statusEl = document.getElementById('metamaskStatus');
statusEl.className = 'metamask-status connected';
statusEl.innerHTML = `
Connected: ${escapeHtml(shortenHash(userAddress))}
`;
// Enable buttons
document.getElementById('weth9WrapBtn').disabled = false;
document.getElementById('weth9UnwrapBtn').disabled = false;
document.getElementById('weth10WrapBtn').disabled = false;
document.getElementById('weth10UnwrapBtn').disabled = false;
// Load balances
await refreshWETHBalances();
// Listen for account changes
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
disconnectMetaMask();
} else {
connectMetaMask();
}
});
// Listen for chain changes
window.ethereum.on('chainChanged', () => {
switchToChain138();
});
} catch (error) {
var errMsg = (error && error.message) ? String(error.message) : '';
var friendly = (error && error.code === 4001) || /not been authorized|rejected|denied/i.test(errMsg)
? 'Connection was rejected. Click Connect Wallet and approve access when MetaMask asks.'
: (errMsg.includes('ethers is not defined') || typeof ethers === 'undefined')
? 'Ethers library failed to load. Please refresh the page.'
: ('Failed to connect MetaMask: ' + (errMsg || 'Unknown error'));
alert(friendly);
}
} 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://blockscout.defi-oracle.io']
}],
});
} catch (addError) {
throw addError;
}
} else {
throw switchError;
}
}
}
function disconnectMetaMask() {
provider = null;
signer = null;
userAddress = null;
const statusEl = document.getElementById('metamaskStatus');
statusEl.className = 'metamask-status disconnected';
statusEl.innerHTML = `
MetaMask not connected
`;
document.getElementById('weth9WrapBtn').disabled = true;
document.getElementById('weth9UnwrapBtn').disabled = true;
document.getElementById('weth10WrapBtn').disabled = true;
document.getElementById('weth10UnwrapBtn').disabled = true;
}
async function refreshWETHBalances() {
if (!userAddress) return;
try {
await ensureEthers();
// Checksum addresses when ethers is available
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
// Convert to lowercase first, then checksum
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
// Fallback to lowercase version
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
// Fallback to lowercase if ethers not available
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
// Get ETH balance
const ethBalance = await provider.getBalance(userAddress);
const ethBalanceFormatted = formatEther(ethBalance);
// Get WETH9 balance
const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, provider);
const weth9Balance = await weth9Contract.balanceOf(userAddress);
const weth9BalanceFormatted = formatEther(weth9Balance);
// Get WETH10 balance - use checksummed address
const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, provider);
const weth10Balance = await weth10Contract.balanceOf(userAddress);
const weth10BalanceFormatted = formatEther(weth10Balance);
// Update UI
document.getElementById('weth9EthBalance').textContent = ethBalanceFormatted + ' ETH';
document.getElementById('weth9TokenBalance').textContent = weth9BalanceFormatted + ' WETH9';
document.getElementById('weth10EthBalance').textContent = ethBalanceFormatted + ' ETH';
document.getElementById('weth10TokenBalance').textContent = weth10BalanceFormatted + ' WETH10';
} catch (error) {
console.error('Error refreshing balances:', error);
}
}
function wrapUnwrapErrorMessage(op, error) {
if (error && (error.code === 4001 || error.code === 'ACTION_REJECTED' || (error.message && /user rejected|user denied/i.test(error.message)))) return 'Transaction cancelled.';
if (error && error.reason) return error.reason;
return (error && error.message) ? error.message : 'Unknown error';
}
function setMaxWETH9(type) {
if (type === 'wrap') {
const ethBalance = document.getElementById('weth9EthBalance').textContent.replace(' ETH', '');
document.getElementById('weth9WrapAmount').value = parseFloat(ethBalance).toFixed(6);
} else {
const wethBalance = document.getElementById('weth9TokenBalance').textContent.replace(' WETH9', '');
document.getElementById('weth9UnwrapAmount').value = parseFloat(wethBalance).toFixed(6);
}
}
function setMaxWETH10(type) {
if (type === 'wrap') {
const ethBalance = document.getElementById('weth10EthBalance').textContent.replace(' ETH', '');
document.getElementById('weth10WrapAmount').value = parseFloat(ethBalance).toFixed(6);
} else {
const wethBalance = document.getElementById('weth10TokenBalance').textContent.replace(' WETH10', '');
document.getElementById('weth10UnwrapAmount').value = parseFloat(wethBalance).toFixed(6);
}
}
async function wrapWETH9() {
const amount = document.getElementById('weth9WrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const ethBalance = await provider.getBalance(userAddress);
if (ethBalance.lt(amountWei)) {
alert('Insufficient ETH balance. You have ' + formatEther(ethBalance) + ' ETH.');
return;
}
const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, signer);
try {
await weth9Contract.callStatic.deposit({ value: amountWei });
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth9WrapBtn');
btn.disabled = true;
btn.innerHTML = ' Processing...';
const tx = await weth9Contract.deposit({ value: amountWei });
const receipt = await tx.wait();
btn.innerHTML = ' Success!';
document.getElementById('weth9WrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = ' Wrap ETH to WETH9';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error wrapping WETH9:', error);
alert('Wrap WETH9: ' + wrapUnwrapErrorMessage('wrap', error));
document.getElementById('weth9WrapBtn').innerHTML = ' Wrap ETH to WETH9';
document.getElementById('weth9WrapBtn').disabled = false;
}
}
async function unwrapWETH9() {
const amount = document.getElementById('weth9UnwrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const weth9Contract = new ethers.Contract(WETH9_ADDRESS, WETH_ABI, signer);
const wethBalance = await weth9Contract.balanceOf(userAddress);
if (wethBalance.lt(amountWei)) {
alert('Insufficient WETH9 balance. You have ' + formatEther(wethBalance) + ' WETH9.');
return;
}
try {
await weth9Contract.callStatic.withdraw(amountWei);
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth9UnwrapBtn');
btn.disabled = true;
btn.innerHTML = ' Processing...';
const tx = await weth9Contract.withdraw(amountWei);
const receipt = await tx.wait();
btn.innerHTML = ' Success!';
document.getElementById('weth9UnwrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = ' Unwrap WETH9 to ETH';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error unwrapping WETH9:', error);
alert('Unwrap WETH9: ' + wrapUnwrapErrorMessage('unwrap', error));
document.getElementById('weth9UnwrapBtn').innerHTML = ' Unwrap WETH9 to ETH';
document.getElementById('weth9UnwrapBtn').disabled = false;
}
}
async function wrapWETH10() {
const amount = document.getElementById('weth10WrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
// Ensure address is checksummed
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const ethBalance = await provider.getBalance(userAddress);
if (ethBalance.lt(amountWei)) {
alert('Insufficient ETH balance. You have ' + formatEther(ethBalance) + ' ETH.');
return;
}
const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, signer);
try {
await weth10Contract.callStatic.deposit({ value: amountWei });
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth10WrapBtn');
btn.disabled = true;
btn.innerHTML = ' Processing...';
const tx = await weth10Contract.deposit({ value: amountWei });
const receipt = await tx.wait();
btn.innerHTML = ' Success!';
document.getElementById('weth10WrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = ' Wrap ETH to WETH10';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error wrapping WETH10:', error);
alert('Wrap WETH10: ' + wrapUnwrapErrorMessage('wrap', error));
document.getElementById('weth10WrapBtn').innerHTML = ' Wrap ETH to WETH10';
document.getElementById('weth10WrapBtn').disabled = false;
}
}
async function unwrapWETH10() {
const amount = document.getElementById('weth10UnwrapAmount').value;
if (!amount || parseFloat(amount) <= 0) {
alert('Please enter a valid amount');
return;
}
if (!signer) {
alert('Please connect MetaMask first');
return;
}
try {
await ensureEthers();
// Ensure address is checksummed
if (typeof ethers !== 'undefined' && ethers.utils) {
try {
const lowerAddress = WETH10_ADDRESS_RAW.toLowerCase();
WETH10_ADDRESS = ethers.utils.getAddress(lowerAddress);
} catch (e) {
console.warn('Could not checksum WETH10 address:', e);
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
} else {
WETH10_ADDRESS = WETH10_ADDRESS_RAW.toLowerCase();
}
const amountWei = parseWETHAmount(amount);
if (!amountWei || amountWei.isZero()) {
alert('Please enter a valid amount in ETH or wei (e.g. 100 or 100000000000000000000).');
return;
}
const weth10Contract = new ethers.Contract(WETH10_ADDRESS, WETH_ABI, signer);
const wethBalance = await weth10Contract.balanceOf(userAddress);
if (wethBalance.lt(amountWei)) {
alert('Insufficient WETH10 balance. You have ' + formatEther(wethBalance) + ' WETH10.');
return;
}
try {
await weth10Contract.callStatic.withdraw(amountWei);
} catch (e) {
alert('Simulation failed: ' + (e.reason || e.message || 'Unknown error'));
return;
}
const btn = document.getElementById('weth10UnwrapBtn');
btn.disabled = true;
btn.innerHTML = ' Processing...';
const tx = await weth10Contract.withdraw(amountWei);
const receipt = await tx.wait();
btn.innerHTML = ' Success!';
document.getElementById('weth10UnwrapAmount').value = '';
await refreshWETHBalances();
setTimeout(() => {
btn.innerHTML = ' Unwrap WETH10 to ETH';
btn.disabled = false;
}, 3000);
} catch (error) {
console.error('Error unwrapping WETH10:', error);
alert('Unwrap WETH10: ' + wrapUnwrapErrorMessage('unwrap', error));
document.getElementById('weth10UnwrapBtn').innerHTML = ' Unwrap WETH10 to ETH';
document.getElementById('weth10UnwrapBtn').disabled = false;
}
}
function showWETHTab(tab, clickedElement) {
document.querySelectorAll('.weth-tab-content').forEach(el => el.style.display = 'none');
document.querySelectorAll('.weth-tab').forEach(el => el.classList.remove('active'));
const tabElement = document.getElementById(`${tab}Tab`);
if (tabElement) {
tabElement.style.display = 'block';
}
// Update active tab - use clickedElement if provided, otherwise find by tab name
if (clickedElement) {
clickedElement.classList.add('active');
} else {
// Find the button that corresponds to this tab
const tabButtons = document.querySelectorAll('.weth-tab');
tabButtons.forEach(btn => {
if (btn.getAttribute('onclick')?.includes(`'${tab}'`)) {
btn.classList.add('active');
}
});
}
}
window.showWETHTab = showWETHTab;
async function renderWETHUtilitiesView() {
showView('weth');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'weth') updatePath('/weth');
if (userAddress) {
await refreshWETHBalances();
}
}
window._showWETHUtilities = renderWETHUtilitiesView;
async function renderBridgeView() {
showView('bridge');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'bridge') updatePath('/bridge');
await refreshBridgeData();
}
window._showBridgeMonitoring = renderBridgeView;
async function runMissionControlBridgeTrace() {
var input = document.getElementById('missionControlBridgeTraceInput');
var result = document.getElementById('missionControlBridgeTraceResult');
if (!input || !result) return;
var tx = String(input.value || '').trim();
if (!/^0x[a-fA-F0-9]{64}$/.test(tx)) {
result.innerHTML = '
Enter a valid 32-byte transaction hash.
';
return;
}
result.innerHTML = '
Resolving trace...
';
try {
var res = await fetch(EXPLORER_API_V1_BASE + '/mission-control/bridge/trace?tx=' + encodeURIComponent(tx));
var body = await res.json();
if (!res.ok) {
throw new Error((body && body.error && body.error.message) || ('HTTP ' + res.status));
}
var data = body && body.data ? body.data : {};
var html = '';
html += '
';
html += '
Mission-control trace resolved the tx through Blockscout and labeled any matching Chain 138 contracts from smart-contracts-master.json.
';
}
}
window.runMissionControlBridgeTrace = runMissionControlBridgeTrace;
async function renderHomeView() {
showView('home');
if ((window.location.pathname || '').replace(/\/$/, '') !== '') updatePath('/');
await loadStats();
await loadLatestBlocks();
await loadLatestTransactions();
// Start real-time transaction updates
startTransactionUpdates();
}
window._showHome = renderHomeView;
async function renderBlocksView() {
showView('blocks');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'blocks') updatePath('/blocks');
await loadAllBlocks();
}
window._showBlocks = renderBlocksView;
async function renderTransactionsView() {
showView('transactions');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'transactions') updatePath('/transactions');
await loadAllTransactions();
}
window._showTransactions = renderTransactionsView;
async function renderAddressesView() {
showView('addresses');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'addresses') updatePath('/addresses');
await loadAllAddresses();
}
window._showAddresses = renderAddressesView;
function buildAnalyticsViewHtml() {
var html = '';
html += '
';
html += '
';
html += '
';
html += '
';
html += '
Live Network Analytics
';
html += '
Analytics surfaces are consolidated into the live explorer dashboards instead of a separate unfinished panel. Use this page as a hub to the active gas, block, bridge, and route monitoring views.
';
return html;
}
// Analytics view
function renderAnalyticsView() {
showView('analytics');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'analytics') updatePath('/analytics');
var container = document.getElementById('analyticsContent');
if (!container) return;
container.innerHTML = buildAnalyticsViewHtml();
}
window._showAnalytics = renderAnalyticsView;
function buildOperatorViewHtml() {
var html = '';
html += '
';
html += '
Track 4 APIs require authenticated wallet + IP allowlisting on the Go backend. This browser UI never holds deployer keys. Do not paste secrets into Explorer AI; PMM/MCP execution is off unless the server enables it explicitly.
';
html += '
';
html += '
';
html += '
';
html += '
Operator Access Hub
';
html += '
The explorer does not expose raw privileged controls here. Instead, this page collects the live operator-facing observability and execution surfaces that are safe to browse from the public UI.
GET /explorer-api/v1/mission-control/stream GET /explorer-api/v1/mission-control/bridge/trace?tx=0x... GET /explorer-api/v1/mission-control/liquidity/token/{address}/pools
';
html += '
Track 4 script API
POST /explorer-api/v1/track4/operator/run-script Requires authenticated wallet, IP allowlisting, and backend allowlist configuration.
';
html += '
';
html += '
';
return html;
}
// Operator view
function renderOperatorView() {
showView('operator');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'operator') updatePath('/operator');
var container = document.getElementById('operatorContent');
if (!container) return;
container.innerHTML = buildOperatorViewHtml();
}
window._showOperator = renderOperatorView;
function renderTopologyCytoscape(graph) {
var c = document.getElementById('systemTopologyContent');
if (!c) return;
var elements = (graph && graph.elements) ? graph.elements : [];
var liqNote = '';
var ls = graph && graph.liquiditySample;
if (ls && typeof ls.poolCount === 'number') {
liqNote = '
Token-aggregation sample at generation: ' + ls.poolCount + ' pools for sample token (set TOKEN_AGGREGATION_BASE_URL when running the generator).
';
}
c.innerHTML = '' +
liqNote +
'
Regenerate: bash explorer-monorepo/scripts/generate-topology-graph.sh from repo root, then deploy /var/www/html/config/topology-graph.json. Click a contract node to open its address page when href is present.
'; };
document.head.appendChild(s);
}
function renderSystemTopologyView() {
var c = document.getElementById('systemTopologyContent');
if (!c) return;
c.innerHTML = '
';
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);
const blockFilter = getExplorerPageFilter('homeBlocks');
const filteredBlocks = blockFilter ? limitedBlocks.filter(function(block) {
var d = normalizeBlockDisplay(block);
return matchesExplorerFilter([d.blockNum, d.hash, d.txCount, d.timestampFormatted, d.timeAgo].join(' '), blockFilter);
}) : limitedBlocks;
const filterBar = renderPageFilterBar('homeBlocks', 'Filter blocks by number, hash, tx count, or age...', 'Filters the live block cards below.', 'loadLatestBlocks()');
if (limitedBlocks.length === 0) {
if (container) container.innerHTML = filterBar + '
No blocks found.
';
} else if (filteredBlocks.length === 0) {
if (container) container.innerHTML = filterBar + '
No blocks match the current filter.
';
} else {
// Create HTML with duplicated blocks for seamless infinite loop
let html = filterBar + '
';
html += '
';
// First set of blocks (with animations for first 3)
filteredBlocks.forEach(function(block, index) {
var animationClass = index < 3 ? 'new-block' : '';
html += createBlockCardHtml(block, { animationClass: animationClass });
});
// Duplicate blocks for seamless infinite loop
filteredBlocks.forEach(function(block) {
html += createBlockCardHtml(block, {});
});
html += '
';
}
function renderRouteNode(node, depthLevel) {
var indent = Math.max(0, depthLevel || 0) * 1.05;
var status = normalizeRouteStatus(node.status);
var statusColor = status === 'live' ? '#16a34a' : status === 'partial' ? '#f59e0b' : status === 'stale' ? '#d97706' : '#dc2626';
var html = '
This sweep probes explicit local token pairs against compliant and official anchor assets on Chain 138. The priority route cards above remain the bridge-path checks; this table focuses on direct-pair coverage and quote-token metadata gaps.
';
html += renderRouteSweepSummary(sweepOkResults);
if (priorityErrors.length) {
html += '
Some priority route requests failed, but the pools table is still available.
';
}
html += '
';
html += '
';
priorityOkResults.forEach(function(entry) {
html += renderPriorityRouteCard(entry);
});
html += '
';
html += '
';
html += '
';
html += '
Missing Quote-Token Pools
';
html += '
';
html += renderMissingQuotePools(allSweepMissing);
html += '
Failed to load live route tree: ' + escapeHtml(err.message || 'Unknown error') + '
';
updatePoolsMissingQuoteBadge(0);
}
}
function buildRoutesLandingHtml() {
var html = '';
html += '
';
html += '
';
html += '
';
html += '
Live Route Decision Tree
';
html += '
This dedicated view follows the Chain 138 routing graph end-to-end. It keeps the live coverage sweep, direct-pair diagnostics, and bridge-path branches together in one place so route investigations do not get buried inside the pools inventory.
';
html += '
';
html += '
';
html += '';
html += '';
html += '';
html += '
';
html += '
';
html += '
';
html += '
Best for
Route debugging and operator review
Use this page when a user route, destination branch, or quote-token path looks wrong.
';
html += '
Includes
Coverage sweep + priority route cards
The pools page now links here instead of embedding the full route tree inline.
';
html += '
Data source
Live token-aggregation route tree API
Every refresh re-reads current Chain 138 PMM and bridge state.
';
html += '
';
html += '
';
html += '
Loading live route tree...
';
return html;
}
function renderRoutesView() {
showView('routes');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'routes') updatePath('/routes');
updateBreadcrumb('routes');
var container = document.getElementById('routesContent');
if (!container) return;
container.innerHTML = buildRoutesLandingHtml();
setTimeout(function() {
loadLiveRouteTrees('routesRouteTreeContent');
}, 0);
_poolsRouteTreeRefreshTimer = setInterval(function() {
if (currentView === 'routes') {
loadLiveRouteTrees('routesRouteTreeContent');
}
}, ROUTE_TREE_REFRESH_MS);
}
window.renderRoutesView = renderRoutesView;
function summarizePoolRows(rows) {
var summary = {
liveLocal: 0,
externalMainnet: 0,
notYetCreated: 0,
missingCode: 0,
partial: 0,
};
(rows || []).forEach(function(row) {
var status = String((row && row.status) || '').toLowerCase();
if (status.indexOf('funded (live)') !== -1 || status.indexOf('deployed (live)') !== -1) {
summary.liveLocal += 1;
return;
}
if (status.indexOf('external / mainnet') !== -1 || status.indexOf('external / not on chain 138') !== -1) {
summary.externalMainnet += 1;
return;
}
if (status.indexOf('not created') !== -1) {
summary.notYetCreated += 1;
return;
}
if (status.indexOf('missing code') !== -1) {
summary.missingCode += 1;
return;
}
if (status.indexOf('partially funded') !== -1 || status.indexOf('created (unfunded)') !== -1) {
summary.partial += 1;
}
});
return summary;
}
function toCsv(rows) {
return rows.map(function(row) {
return row.map(function(cell) {
return '"' + String(cell == null ? '' : cell).replace(/"/g, '""') + '"';
}).join(',');
}).join('\n');
}
function exportPoolsCSV() {
if (!latestPoolsSnapshot || !Array.isArray(latestPoolsSnapshot.rows)) {
showToast('Pools data is not ready yet', 'error');
return;
}
var summary = latestPoolsSnapshot.summary || {};
var rows = latestPoolsSnapshot.rows || [];
var csvRows = [
['Section', 'Metric', 'Value'],
['Summary', 'Generated At', latestPoolsSnapshot.generatedAt || ''],
['Summary', 'Live local pools', summary.liveLocal || 0],
['Summary', 'External Mainnet-side', summary.externalMainnet || 0],
['Summary', 'Not yet created', summary.notYetCreated || 0],
['Summary', 'Needs attention', (summary.missingCode || 0) + (summary.partial || 0)],
[],
['Category', 'Pool Pair', 'System', 'Address', 'Status', 'Notes']
];
rows.forEach(function(row) {
csvRows.push([
row.category || '',
row.poolPair || '',
row.poolType || '',
row.address || '',
row.status || '',
row.notes || ''
]);
});
var blob = new Blob([toCsv(csvRows)], { type: 'text/csv' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'pools-status.csv';
a.click();
URL.revokeObjectURL(url);
showToast('CSV downloaded', 'success');
}
function exportPoolsJSON() {
if (!latestPoolsSnapshot) {
showToast('Pools data is not ready yet', 'error');
return;
}
var blob = new Blob([JSON.stringify(latestPoolsSnapshot, null, 2)], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'pools-status.json';
a.click();
URL.revokeObjectURL(url);
showToast('JSON downloaded', 'success');
}
window.exportPoolsCSV = exportPoolsCSV;
window.exportPoolsJSON = exportPoolsJSON;
async function renderPoolsView() {
showView('pools');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'pools') updatePath('/pools');
var container = document.getElementById('poolsContent');
if (_poolsRouteTreeRefreshTimer) {
clearInterval(_poolsRouteTreeRefreshTimer);
_poolsRouteTreeRefreshTimer = null;
}
if (!container) return;
try {
container.innerHTML = '
Loading pools...
';
var live = await getLivePoolRows();
var filter = getExplorerPageFilter('poolsList');
var filterBar = renderPageFilterBar('poolsList', 'Filter by category, pair, type, status, address, or notes...', 'Tracks live Chain 138 pool, reserve, and bridge-linked contract state.', 'openPoolsView()');
var summary = summarizePoolRows(live.rows);
latestPoolsSnapshot = {
generatedAt: new Date().toISOString(),
summary: summary,
rows: live.rows
};
var rows = live.rows.map(function(row) {
return { row: row, searchText: [row.category, row.poolPair, row.poolType, row.address, row.status, row.notes].join(' ') };
});
var filtered = filter ? rows.filter(function(entry) { return matchesExplorerFilter(entry.searchText, filter); }) : rows;
var html = filterBar + '
This table is derived from live Chain 138 contract state. Pool addresses, funding status, quote-token readiness, and private-registry registrations are refreshed from the chain each time the page renders. External or mainnet-only systems are labeled explicitly.
The full route sweep and priority route cards now live on their own dedicated page so investigations can open directly into the routing graph.
';
html += '
';
html += '
';
html += '';
html += '';
html += '
';
html += '
';
html += '
';
html += '
Use Routes for the live route coverage sweep, bridge-path diagnostics, and missing quote-token review. The pools table above stays focused on pool inventory and funding state.
';
}
}
window.renderPoolsView = renderPoolsView;
function renderLiquidityAccessView() {
showView('liquidity');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'liquidity') updatePath('/liquidity');
var container = document.getElementById('liquidityContent');
if (!container) return;
var publicApiBase = TOKEN_AGGREGATION_API_BASE + '/v1';
var livePools = [
{
pair: 'cUSDT / cUSDC',
poolAddress: '0xff8d3b8fDF7B112759F076B69f4271D4209C0849',
reserves: '10,000,000 / 10,000,000'
},
{
pair: 'cUSDT / USDT',
poolAddress: '0x6fc60DEDc92a2047062294488539992710b99D71',
reserves: '10,000,000 / 10,000,000'
},
{
pair: 'cUSDC / USDC',
poolAddress: '0x0309178ae30302D83c76d6Dd402a684eF3160eec',
reserves: '10,000,000 / 10,000,000'
},
{
pair: 'cUSDT / cXAUC',
poolAddress: '0x1AA55E2001E5651349AfF5A63FD7A7Ae44f0F1b0',
reserves: '2,666,965 / 519.477000'
},
{
pair: 'cUSDC / cXAUC',
poolAddress: '0xEA9Ac6357CaCB42a83b9082B870610363B177cBa',
reserves: '1,000,000 / 194.782554'
},
{
pair: 'cEURT / cXAUC',
poolAddress: '0xbA99bc1eAAC164569d5AcA96C806934DDaF970Cf',
reserves: '1,000,000 / 225.577676'
}
];
var endpointCards = [
{
title: 'Mission-control cached token pools',
method: 'GET',
href: EXPLORER_API_V1_BASE + '/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools',
notes: '30-second cached proxy to token-aggregation pools for the configured chain. Useful for fast operator checks and UI panels.'
},
{
title: 'Canonical route matrix',
method: 'GET',
href: publicApiBase + '/routes/matrix',
notes: 'Full live-route inventory with optional blocked and planned route visibility.'
},
{
title: 'Live ingestion export',
method: 'GET',
href: publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap',
notes: 'Flat export for adapter discovery and route ingestion.'
},
{
title: 'Partner payload templates',
method: 'GET',
href: publicApiBase + '/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true',
notes: 'Builds request templates for 1inch, 0x, and LiFi from live routes.'
},
{
title: 'Resolve supported partner payloads',
method: 'POST',
href: publicApiBase + '/routes/partner-payloads/resolve',
notes: 'Accepts partner, amount, and addresses and returns supported payloads by default.'
},
{
title: 'Dispatch supported partner payload',
method: 'POST',
href: publicApiBase + '/routes/partner-payloads/dispatch',
notes: 'Dispatches one supported partner payload when the chain is publicly supported.'
},
{
title: 'Internal Chain 138 execution plan',
method: 'POST',
href: publicApiBase + '/routes/internal-execution-plan',
notes: 'Returns the DODO PMM fallback execution plan when public partner support is unavailable.'
}
];
var requestExamples = [
'GET ' + EXPLORER_API_V1_BASE + '/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools',
'GET ' + publicApiBase + '/routes/matrix?includeNonLive=true',
'GET ' + publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap',
'GET ' + publicApiBase + '/routes/partner-payloads?partner=LiFi&amount=1000000&includeUnsupported=true',
'POST ' + publicApiBase + '/routes/partner-payloads/resolve',
'POST ' + publicApiBase + '/routes/internal-execution-plan'
];
var html = '';
html += '
';
html += '
Live public pools
6
Verified DODO PMM pools on Chain 138.
';
html += '
Public access path
/token-aggregation/api/v1
Explorer-hosted proxy for route and execution APIs.
';
html += '
Partner status
Fallback Ready
Templates exist for 1inch, 0x, and LiFi, but Chain 138 execution still falls back internally.
';
html += '
';
html += '
';
html += '
Live Pool Snapshot
';
html += '
';
livePools.forEach(function(pool) {
html += '
';
html += '
';
html += '
' + escapeHtml(pool.pair) + '
Pool: ' + escapeHtml(pool.poolAddress) + '
';
html += '
Reserves: ' + escapeHtml(pool.reserves) + '
';
html += '
';
});
html += '
';
html += '
';
html += '
Route and Execution Notes
';
html += '
';
html += '
Direct live routes today: cUSDT ↔ cUSDC, cUSDT ↔ USDT, cUSDC ↔ USDC, cUSDT ↔ cXAUC, cUSDC ↔ cXAUC, and cEURT ↔ cXAUC.
';
html += '
Multi-hop public paths exist through cXAUC for cEURT ↔ cUSDT, cEURT ↔ cUSDC, and an alternate cUSDT ↔ cUSDC path.
';
html += '
Mainnet bridge discovery is live for cUSDT → USDT and cUSDC → USDC through the configured UniversalCCIPBridge lane.
';
html += '
1inch, 0x, and LiFi request templates are available through the explorer API, but those partners do not publicly support Chain 138 execution today.
';
html += '
When public partner execution is unavailable, the internal DODO PMM execution plan endpoint returns the Chain 138 fallback route instead of a dead end.
';
html += '
';
html += '
';
html += '
Public Explorer Access Points
';
html += '
';
endpointCards.forEach(function(card) {
html += '';
html += '
';
requestExamples.forEach(function(example) {
html += '
' + escapeHtml(example) + '
';
});
html += '
';
html += '
Related Explorer Tools
';
html += '
Use Wallet for network onboarding and the explorer token list URL, then open Routes for live route-tree diagnostics and Pools for contract-state inventory checks.
';
html += '
';
html += '';
html += '';
html += '';
html += ' Explorer docs';
html += '
';
html += '
';
container.innerHTML = html;
}
window.renderLiquidityAccessView = renderLiquidityAccessView;
function renderMoreView() {
showView('more');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'operations') updatePath('/operations');
var container = document.getElementById('moreContent');
if (!container) return;
var groups = [
{
key: 'tools',
title: 'Tools',
items: [
{ title: 'Input Data Decoder', icon: 'fa-file-code', status: 'Live', badgeClass: 'badge-info', desc: 'Open transaction detail pages to decode calldata, logs, and contract interactions already exposed by the explorer.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Unit Converter', icon: 'fa-scale-balanced', status: 'Live', badgeClass: 'badge-success', desc: 'Convert wei, gwei, ether, and common Chain 138 stablecoin units with a quick in-page helper.', action: 'showUnitConverterModal();', href: '/operations' },
{ title: 'CSV Export', icon: 'fa-file-csv', status: 'Live', badgeClass: 'badge-success', desc: 'Export pool state and route inventory snapshots for operator review and downstream ingestion.', action: 'showPools(); updatePath(\'/pools\'); setTimeout(function(){ if (typeof exportPoolsCSV === \"function\") exportPoolsCSV(); }, 200);', href: '/pools' },
{ title: 'Account Balance Checker', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Jump into indexed addresses to inspect balances, token inventory, internal transfers, and recent activity.', action: 'showAddresses();', href: '/addresses' }
]
},
{
key: 'explore',
title: 'Explore',
items: [
{ title: 'Gas Tracker', icon: 'fa-gas-pump', status: 'Live', badgeClass: 'badge-success', desc: 'Review live gas, block time, TPS, and chain health from the home network dashboard.', action: 'showHome();', href: '/' },
{ title: 'Visual Command Center', icon: 'fa-satellite-dish', status: 'Live', badgeClass: 'badge-info', desc: 'Interactive Mermaid topology: Chain 138 hub, CCIP, Alltra, stack, flows, cross-chain, cW Mainnet, and off-chain integrations (from SMOM_DBIS_138_FULL_DEPLOYMENT_FLOW_MAP).', action: 'window.location.href=\'/chain138-command-center.html\';', href: '/chain138-command-center.html' },
{ title: 'DEX Tracker', icon: 'fa-chart-line', status: 'Live', badgeClass: 'badge-success', desc: 'Open liquidity discovery, PMM pool status, live route trees, and partner payload access points.', action: 'showRoutes();', href: '/routes' },
{ title: 'Node Tracker', icon: 'fa-server', status: 'Live', badgeClass: 'badge-success', desc: 'Inspect bridge balances, destination configuration, and operator-facing chain references from the live bridge monitoring panel.', action: 'showBridgeMonitoring();', href: '/bridge' },
{ title: 'Label Cloud', icon: 'fa-tags', status: 'Live', badgeClass: 'badge-success', desc: 'Browse labeled addresses, contracts, and address activity through the explorer address index.', action: 'showAddresses();', href: '/addresses' },
{ title: 'Domain Name Lookup', icon: 'fa-magnifying-glass', status: 'Live', badgeClass: 'badge-success', desc: 'Use the smart search launcher to resolve ENS-style names, domains, addresses, hashes, and token symbols.', action: 'openSmartSearchModal(\'\');', href: '/operations' }
]
},
{
key: 'services',
title: 'Services',
items: [
{ title: 'Token Approvals', icon: 'fa-shield-halved', status: 'External', badgeClass: 'badge-warning', desc: 'Jump to revoke.cash for wallet approval review. Address detail pages also expose approval shortcuts directly.', action: 'openExternalMoreLink(\'https://revoke.cash/\');', href: '#' },
{ title: 'Verified Signature', icon: 'fa-signature', status: 'Live', badgeClass: 'badge-success', desc: 'Use wallet sign-in and verified address flows already built into the explorer authentication surfaces.', action: 'showWalletModal();', href: '/operations' },
{ title: 'Input Data Messages', icon: 'fa-message', status: 'Live', badgeClass: 'badge-info', desc: 'Transaction detail pages already surface decoded input data, event logs, and contract interaction context.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Advanced Filter', icon: 'fa-filter', status: 'Live', badgeClass: 'badge-success', desc: 'Block, transaction, address, token, pool, bridge, and watchlist screens all support focused page-level filtering.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'MetaMask Snap', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Open the Chain 138 MetaMask Snap companion for network setup, token list access, and wallet integration guidance.', action: 'window.location.href=\'/snap/\';', href: '/snap/' }
]
}
];
var html = '
';
html += '
';
html += '
Operations Hub
';
html += '
Discover SolaceScan operational explorer tools in one place, grouped the way users expect from a polished specialist explorer.
';
html += '
';
html += '
Now live
Route matrix, ingestion APIs, smart search, pool exports, and live Mainnet stable bridge discovery.
';
html += '
Good entry points
';
html += '';
html += '';
html += '';
html += '';
html += '
';
html += '
';
groups.forEach(function(group) {
html += '
';
html += '
' + escapeHtml(group.title) + '
';
html += '
';
group.items.forEach(function(item) {
var disabled = !!item.disabled;
var disabledTitle = String(item.title) + ' is not exposed in the explorer yet.';
var onclick = disabled
? ('event.preventDefault(); showToast(' + JSON.stringify(disabledTitle) + ', "info");')
: (item.href === '#'
? ('event.preventDefault(); ' + item.action + ' closeNavMenu();')
: ('event.preventDefault(); ' + item.action + ' updatePath(' + JSON.stringify(item.href) + '); closeNavMenu();'));
var href = disabled ? '/operations' : item.href;
html += '';
html += '
Cross-chain interoperability powered by Chainlink CCIP
Arbitrum remains route-blocked on the current Mainnet hub leg. The latest Mainnet -> Arbitrum WETH9 send reverted before any bridge event was emitted, so treat Arbitrum as unavailable until that hub path is repaired.
Paste a transaction hash to resolve the from and to addresses through the mission-control bridge-trace API. The response is labeled with Chain 138 contract names from the smart-contracts registry when available.
', decodedValue || '', 'Copy decoded log'));
html += '
';
return html;
}
function renderInputWordTable(inputHex) {
if (!inputHex || !/^0x[0-9a-f]+$/i.test(String(inputHex))) return '';
var normalized = String(inputHex).slice(2);
if (normalized.length <= 8) return '';
var payload = normalized.slice(8);
if (!payload) return '';
var words = [];
for (var i = 0; i < payload.length; i += 64) {
words.push(payload.slice(i, i + 64));
}
if (!words.length) return '';
var rows = words.map(function(word, index) {
var offsetBytes = 4 + (index * 32);
var hexWord = '0x' + word;
var decimalPreview = '';
var parsed = toBigIntSafe(hexWord);
if (parsed != null) {
decimalPreview = summarizeInspectorValue(parsed.toString(), 18, 10);
}
return '
Failed to load block: ' + escapeHtml(error.message) + '
';
}
}
window._showBlockDetail = renderBlockDetail;
// Keep wrapper (do not overwrite) so all calls go through setTimeout and avoid stack overflow
async function renderTransactionDetail(txHash) {
const th = safeTxHash(txHash);
if (!th) { showToast('Invalid transaction hash', 'error'); return; }
txHash = th;
currentDetailKey = 'tx:' + txHash.toLowerCase();
showView('transactionDetail');
updatePath('/transactions/' + txHash);
const container = document.getElementById('transactionDetail');
updateBreadcrumb('transaction', txHash);
container.innerHTML = createSkeletonLoader('detail');
try {
let t;
let rawTx = null;
if (CHAIN_ID === 138) {
try {
var detailResult = await fetchChain138TransactionDetail(txHash);
rawTx = detailResult.rawTransaction;
t = detailResult.transaction;
if (!t) {
var diagnostics = rawTx && rawTx.diagnostics ? rawTx.diagnostics : null;
if (diagnostics && diagnostics.rpc_transaction_found === false) {
throw new Error('Transaction not found in Blockscout or the Chain 138 public RPC. It may belong to a different network, have been replaced, or never broadcast successfully' + (diagnostics.latest_block_number ? ' (latest block #' + diagnostics.latest_block_number + ')' : ''));
}
throw new Error('Transaction not found');
}
} catch (error) {
container.innerHTML = '
Normalized + raw transaction payload${escapeHtml(rawSourceLabel)} / ${escapeHtml(summarizeInspectorValue(t.hash, 12, 10))}
This includes the normalized transaction fields used by the explorer plus the underlying Blockscout or RPC fallback payload so nothing important stays hidden behind the UI.
RPC fallback mode. Indexed address metadata is temporarily unavailable or responding too slowly, so this view is using live RPC for balance, nonce-derived transaction count, and contract detection. Transactions, token balances, internal txns, and NFTs remain best-effort while the explorer API recovers.
';
}
}
window.showNftDetail = showNftDetail;
function showSearchResultsList(items, query) {
showView('searchResults');
var container = document.getElementById('searchResultsContent');
if (!container) return;
var html = '
Found ' + items.length + ' result(s) for "' + escapeHtml(query) + '". Click a row to open.
';
html += '
Type
Value
';
items.forEach(function(item) {
var type = (item.type || item.address_type || '').toLowerCase();
var label = item.name || item.symbol || item.address_hash || item.hash || item.tx_hash || (item.block_number != null ? 'Block #' + item.block_number : '') || '-';
var addr, txHash, blockNum, tokenAddr;
if (item.token_address || item.token_contract_address_hash) {
tokenAddr = item.token_address || item.token_contract_address_hash;
if (/^0x[a-f0-9]{40}$/i.test(tokenAddr)) {
html += '
';
container.innerHTML = html;
}
window.showSearchResultsList = showSearchResultsList;
async function handleSearch(query) {
query = query.trim();
if (!query) {
showToast('Please enter a search query', 'info');
return;
}
saveSmartSearchHistory(query);
closeSmartSearchModal();
const normalizedQuery = query.toLowerCase().replace(/\s/g, '');
try {
if (/^0x[a-f0-9]{40}$/i.test(normalizedQuery)) {
await showAddressDetail(normalizedQuery);
return;
}
if (/^0x[a-f0-9]{64}$/i.test(normalizedQuery)) {
await showTransactionDetail(normalizedQuery);
return;
}
if (/^\d+$/.test(query)) {
await showBlockDetail(query);
return;
}
if (/^0x[a-f0-9]+$/i.test(normalizedQuery)) {
const blockNum = parseInt(normalizedQuery, 16);
if (!isNaN(blockNum)) {
await showBlockDetail(blockNum.toString());
return;
}
}
if (CHAIN_ID === 138) {
var searchResults = null;
try {
searchResults = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/search?q=' + encodeURIComponent(query));
} catch (e) {}
if (searchResults && searchResults.items && searchResults.items.length > 0) {
showSearchResultsList(searchResults.items, query);
return;
}
if (/^0x[a-f0-9]{8,64}$/i.test(normalizedQuery)) {
try {
var txResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/transactions/' + normalizedQuery);
if (txResp && (txResp.hash || txResp.tx_hash)) {
var fullHash = txResp.hash || txResp.tx_hash;
await showTransactionDetail(fullHash);
return;
}
} catch (e) {}
showToast('No unique result for partial hash. Use at least 0x + 8 hex characters, or full tx hash (0x + 64 hex).', 'info');
return;
}
}
showToast('Invalid search. Try address (0x...40 hex), tx hash (0x...64 hex or 0x+8 hex), block number, or token/contract name.', 'error');
} catch (error) {
console.error('Search error:', error);
showToast('Failed to load search results: ' + (error.message || 'Unknown error'), 'error');
}
}
window.handleSearch = handleSearch;
function getTimeAgo(date) {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return 'N/A';
}
const now = new Date();
const diffMs = now - date;
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) {
return `${diffSecs}s ago`;
} else if (diffMins < 60) {
return `${diffMins}m ago`;
} else if (diffHours < 24) {
return `${diffHours}h ago`;
} else if (diffDays < 7) {
return `${diffDays}d ago`;
} else {
return date.toLocaleDateString();
}
}
function formatNumber(num) {
return parseInt(num || 0).toLocaleString();
}
function shortenHash(hash, length = 10) {
if (!hash) return 'N/A';
// Convert to string if it's not already
const hashStr = String(hash);
if (hashStr.length <= length * 2 + 2) return hashStr;
return hashStr.substring(0, length + 2) + '...' + hashStr.substring(hashStr.length - length);
}
function toBigIntValue(value) {
if (typeof value === 'bigint') return value;
if (typeof value === 'number') {
if (!isFinite(value)) return 0n;
return BigInt(Math.trunc(value));
}
var stringValue = String(value == null ? '0' : value).trim();
if (!stringValue) return 0n;
try {
if (/^0x/i.test(stringValue) || /^-?\d+$/.test(stringValue)) {
return BigInt(stringValue);
}
} catch (e) {}
var parsed = Number(stringValue);
if (!isFinite(parsed)) return 0n;
return BigInt(Math.trunc(parsed));
}
function formatUnits(value, decimals, fractionDigits) {
try {
var bigValue = toBigIntValue(value);
var precision = Math.max(0, Math.min(decimals, fractionDigits == null ? 6 : fractionDigits));
var divisor = 10n ** BigInt(decimals);
var whole = bigValue / divisor;
var fraction = bigValue % divisor;
if (fraction < 0) fraction = -fraction;
if (precision === 0) return whole.toString();
var scale = 10n ** BigInt(decimals - precision);
var truncatedFraction = (fraction / scale).toString().padStart(precision, '0').replace(/0+$/, '');
if (!truncatedFraction) return whole.toString();
return whole.toString() + '.' + truncatedFraction;
} catch (e) {
return '0';
}
}
function formatDecimalStringWithGrouping(value) {
var stringValue = String(value == null ? '0' : value).trim();
if (!stringValue) return '0';
var negative = stringValue.charAt(0) === '-';
if (negative) stringValue = stringValue.slice(1);
var parts = stringValue.split('.');
var whole = (parts[0] || '0').replace(/\B(?=(\d{3})+(?!\d))/g, ',');
var fraction = parts.length > 1 && parts[1] ? '.' + parts[1] : '';
return (negative ? '-' : '') + whole + fraction;
}
function formatUnitsLocalized(value, decimals, fractionDigits) {
return formatDecimalStringWithGrouping(formatUnits(value, decimals, fractionDigits));
}
function parseDecimalToUnits(value, decimals) {
var stringValue = String(value == null ? '' : value).trim();
if (!stringValue || !/^\d+(\.\d+)?$/.test(stringValue)) return null;
var parts = stringValue.split('.');
var whole = BigInt(parts[0] || '0');
var fraction = parts[1] || '';
if (fraction.length > decimals) return null;
var paddedFraction = decimals > 0 ? fraction.padEnd(decimals, '0') : '';
return whole * (10n ** BigInt(decimals)) + BigInt(paddedFraction || '0');
}
function formatEther(wei, unit = 'ether') {
return formatUnits(wei, unit === 'gwei' ? 9 : 18, 6);
}
function getExplorerAIPageContext() {
return {
path: (window.location && window.location.pathname) ? window.location.pathname : '/',
view: currentView || 'home'
};
}
function renderExplorerAIMessages() {
var list = document.getElementById('explorerAIMessageList');
var status = document.getElementById('explorerAIStatus');
if (!list) return;
list.innerHTML = _explorerAIState.messages.map(function(message) {
var isAssistant = message.role === 'assistant';
var bubbleStyle = isAssistant
? 'background: rgba(37,99,235,0.10); border:1px solid rgba(37,99,235,0.18);'
: 'background: rgba(15,23,42,0.06); border:1px solid rgba(148,163,184,0.25);';
return '
' +
'
' +
'
' + (isAssistant ? 'Explorer AI' : 'You') + '
' +
'
' + escapeHtml(message.content || '') + '
' +
'
' +
'
';
}).join('');
if (_explorerAIState.loading) {
list.innerHTML += '
Thinking through indexed data, live routes, and docs...
';
}
list.scrollTop = list.scrollHeight;
if (status) {
status.textContent = _explorerAIState.loading
? 'Querying explorer data and the model...'
: 'Read-only assistant using indexed explorer data, route APIs, and curated docs.';
}
}
function setExplorerAIOpen(open) {
_explorerAIState.open = !!open;
var panel = document.getElementById('explorerAIPanel');
var button = document.getElementById('explorerAIFab');
if (panel) panel.style.display = open ? 'flex' : 'none';
if (button) button.setAttribute('aria-expanded', open ? 'true' : 'false');
if (open) {
renderExplorerAIMessages();
var input = document.getElementById('explorerAIInput');
if (input) setTimeout(function() { input.focus(); }, 30);
}
}
function toggleExplorerAIPanel(forceOpen) {
if (typeof forceOpen === 'boolean') {
setExplorerAIOpen(forceOpen);
return;
}
setExplorerAIOpen(!_explorerAIState.open);
}
window.toggleExplorerAIPanel = toggleExplorerAIPanel;
function buildExplorerAISourceSummary(context) {
if (!context || !Array.isArray(context.sources) || !context.sources.length) return '';
return context.sources.map(function(source) {
return source.label || source.type || 'source';
}).filter(Boolean).join(' | ');
}
async function submitExplorerAIMessage(prefill) {
var input = document.getElementById('explorerAIInput');
var raw = typeof prefill === 'string' ? prefill : (input ? input.value : '');
var question = String(raw || '').trim();
if (!question || _explorerAIState.loading) return;
_explorerAIState.messages.push({ role: 'user', content: question });
if (input) input.value = '';
_explorerAIState.loading = true;
renderExplorerAIMessages();
try {
var payload = {
messages: _explorerAIState.messages.slice(-8),
pageContext: getExplorerAIPageContext()
};
var response = await postJSON(EXPLORER_AI_API_BASE + '/chat', payload);
var reply = (response && response.reply) ? String(response.reply) : 'No reply returned.';
var sourceSummary = buildExplorerAISourceSummary(response && response.context);
if (sourceSummary) {
reply += '\n\nSources: ' + sourceSummary;
}
if (response && Array.isArray(response.warnings) && response.warnings.length) {
reply += '\n\nWarnings: ' + response.warnings.join(' | ');
}
_explorerAIState.messages.push({ role: 'assistant', content: reply });
} catch (error) {
_explorerAIState.messages.push({
role: 'assistant',
content: 'Explorer AI could not complete that request.\n\n' + (error && error.message ? error.message : 'Unknown error') + '\n\nIf this is production, confirm the backend has OPENAI_API_KEY and TOKEN_AGGREGATION_API_BASE configured.'
});
} finally {
_explorerAIState.loading = false;
renderExplorerAIMessages();
}
}
window.submitExplorerAIMessage = submitExplorerAIMessage;
function initExplorerAIPanel() {
if (document.getElementById('explorerAIPanel') || !document.body) return;
var style = document.createElement('style');
style.textContent = `
#explorerAIFab {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 20010;
border: 0;
border-radius: 999px;
padding: 0.9rem 1rem;
background: linear-gradient(135deg, #0f172a, #2563eb);
color: #fff;
box-shadow: 0 16px 36px rgba(15,23,42,0.28);
cursor: pointer;
font-weight: 700;
letter-spacing: 0.02em;
}
#explorerAIPanel {
position: fixed;
right: 20px;
bottom: 84px;
width: min(420px, calc(100vw - 24px));
height: min(72vh, 680px);
display: none;
flex-direction: column;
z-index: 20010;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 22px;
box-shadow: 0 24px 60px rgba(15,23,42,0.25);
overflow: hidden;
}
#explorerAIPanel textarea {
width: 100%;
min-height: 88px;
resize: vertical;
border-radius: 14px;
border: 1px solid var(--border);
background: var(--light);
color: var(--text);
padding: 0.85rem 0.9rem;
font: inherit;
}
@media (max-width: 680px) {
#explorerAIPanel {
right: 12px;
left: 12px;
bottom: 76px;
width: auto;
height: min(74vh, 720px);
}
#explorerAIFab {
right: 12px;
bottom: 12px;
}
}
`;
document.head.appendChild(style);
var button = document.createElement('button');
button.id = 'explorerAIFab';
button.type = 'button';
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-controls', 'explorerAIPanel');
button.innerHTML = 'Explorer AI';
button.addEventListener('click', function() { toggleExplorerAIPanel(); });
var panel = document.createElement('section');
panel.id = 'explorerAIPanel';
panel.setAttribute('aria-label', 'Explorer AI');
panel.innerHTML = '' +
'
' +
'
' +
'
' +
'
Explorer AI
' +
'
Read-only assistant using indexed explorer data, route APIs, and curated docs.
' +
'
' +
'' +
'
' +
'
' +
'' +
'' +
'' +
'
' +
'
' +
'' +
'
' +
'
Read-only: indexed explorer + route APIs + docs. No private keys. Server-side operator or PMM/MCP tools stay disabled unless EXPLORER_AI_OPERATOR_TOOLS_ENABLED=1 on the backend.
' +
'' +
'
' +
'
Shift+Enter for a new line. Enter to send.
' +
'' +
'
' +
'
';
document.body.appendChild(button);
document.body.appendChild(panel);
var input = document.getElementById('explorerAIInput');
var sendButton = document.getElementById('explorerAISendBtn');
if (sendButton) {
sendButton.addEventListener('click', function() {
submitExplorerAIMessage();
});
}
if (input) {
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
submitExplorerAIMessage();
}
});
}
renderExplorerAIMessages();
}
// 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) {
Promise.resolve(CHAIN_ID === 138 ? fetchChain138TransactionDetail(txHash) : fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}`).then(function(response) {
return { transaction: normalizeTransaction(response), rawTransaction: response };
}))
.then(result => {
const payload = {
normalized: result && result.transaction ? result.transaction : null,
raw: result && result.rawTransaction ? result.rawTransaction : null
};
const dataStr = safeJsonStringify(payload);
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 launcher, modal handlers, and mobile nav close on link click
document.addEventListener('DOMContentLoaded', () => {
initExplorerAIPanel();
const launchBtn = document.getElementById('searchLauncherBtn');
const modal = document.getElementById('smartSearchModal');
const backdrop = document.getElementById('smartSearchBackdrop');
const closeBtn = document.getElementById('smartSearchCloseBtn');
const input = document.getElementById('smartSearchInput');
const submitBtn = document.getElementById('smartSearchSubmitBtn');
if (launchBtn) {
launchBtn.addEventListener('click', function(e) {
e.preventDefault();
openSmartSearchModal('');
});
}
if (closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
closeSmartSearchModal();
});
}
if (backdrop) {
backdrop.addEventListener('click', closeSmartSearchModal);
}
if (input) {
input.addEventListener('input', function(e) {
updateSmartSearchPreview(e.target.value);
});
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
e.preventDefault();
closeSmartSearchModal();
} else if (e.key === 'Enter') {
e.preventDefault();
handleSearch(e.target.value);
}
});
}
if (submitBtn && input) {
submitBtn.addEventListener('click', function(e) {
e.preventDefault();
handleSearch(input.value);
});
}
window.addEventListener('keydown', function(e) {
var target = e.target;
var tag = target && target.tagName ? target.tagName.toLowerCase() : '';
var isEditable = !!(target && (target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select'));
if (e.key === 'Escape' && modal && modal.style.display === 'block') {
e.preventDefault();
closeSmartSearchModal();
return;
}
if ((e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k')) && !isEditable) {
e.preventDefault();
openSmartSearchModal('');
}
});
var navLinks = document.getElementById('navLinks');
if (navLinks) {
navLinks.addEventListener('click', function(e) {
if (e.target.closest('a')) closeNavMenu();
});
initNavDropdowns();
}
ensureMissionControlHealthStrip();
startMissionControlEventSource();
refreshMissionControlHealthStrip();
setInterval(refreshMissionControlHealthStrip, 120000);
});