';
container.innerHTML = html;
};
var KNOWN_ADDRESS_LABELS = { '0x89dd12025bfcd38a168455a44b400e913ed33be2': 'CCIP WETH9 Bridge', '0xe0e93247376aa097db308b92e6ba36ba015535d0': 'CCIP WETH10 Bridge', '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2': 'WETH9', '0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f': 'WETH10', '0x8078a09637e47fa5ed34f626046ea2094a5cde5e': 'CCIP Router', '0x105f8a15b819948a89153505762444ee9f324684': 'CCIP Sender' };
const CHAIN_138_PMM_INTEGRATION_ADDRESS = '0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d';
const CHAIN_138_PRIVATE_POOL_REGISTRY = '0xb27057B27db09e8Df353AF722c299f200519882A';
const CHAIN_138_CUSDT_ADDRESS = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22';
const CHAIN_138_CUSDC_ADDRESS = '0xf22258f57794CC8E06237084b353Ab30fFfa640b';
const CHAIN_138_CEURT_ADDRESS = '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72';
const CHAIN_138_CXAUC_ADDRESS = '0x290E52a8819A4fbD0714E517225429aA2B70EC6b';
const CHAIN_138_CXAUT_ADDRESS = '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E';
const MAINNET_USDT_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
const MAINNET_USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';
const RESERVE_SYSTEM_ADDRESS = '0x607e97cD626f209facfE48c1464815DDE15B5093';
const RESERVE_TOKEN_INTEGRATION_ADDRESS = '0x34B73e6EDFd9f85a7c25EeD31dcB13aB6E969b96';
const BRIDGE_VAULT_ADDRESS = '0x31884f84555210FFB36a19D2471b8eBc7372d0A8';
const CHAIN_138_POOL_BALANCE_DECIMALS = 6;
const SELECTOR_POOLS = '0x901754d7';
const SELECTOR_GET_PRIVATE_POOL = '0xc427540d';
const SELECTOR_BALANCE_OF = '0x70a08231';
const SELECTOR_SYMBOL = '0x95d89b41';
const SELECTOR_OFFICIAL_USDT = '0xe015a3b8';
const SELECTOR_OFFICIAL_USDC = '0xc82ab874';
const SELECTOR_COMPLIANT_USDT = '0x15fdffdf';
const SELECTOR_COMPLIANT_USDC = '0x59916868';
const ROUTE_TREE_REFRESH_MS = 120000;
function buildRoutePriorityQueries(ctx) {
var officialUsdt = (ctx && ctx.officialUSDT) || '';
var officialUsdc = (ctx && ctx.officialUSDC) || '';
var queries = [
{
key: 'local-cusdt-cusdc',
title: 'Local direct: cUSDT / cUSDC',
tokenIn: CHAIN_138_CUSDT_ADDRESS,
tokenOut: CHAIN_138_CUSDC_ADDRESS,
destinationChainId: 138,
amountIn: '1000000',
},
{
key: 'bridge-cusdt-usdt',
title: 'Mainnet bridge path: cUSDT -> USDT',
tokenIn: CHAIN_138_CUSDT_ADDRESS,
tokenOut: MAINNET_USDT_ADDRESS,
destinationChainId: 1,
amountIn: '1000000',
},
{
key: 'bridge-cusdc-usdc',
title: 'Mainnet bridge path: cUSDC -> USDC',
tokenIn: CHAIN_138_CUSDC_ADDRESS,
tokenOut: MAINNET_USDC_ADDRESS,
destinationChainId: 1,
amountIn: '1000000',
}
];
if (safeAddress(officialUsdt)) {
queries.unshift({
key: 'local-cusdt-usdt',
title: 'Local direct: cUSDT / USDT (official mirror, Chain 138)',
tokenIn: CHAIN_138_CUSDT_ADDRESS,
tokenOut: officialUsdt,
destinationChainId: 138,
amountIn: '1000000',
});
}
if (safeAddress(officialUsdc)) {
queries.splice(2, 0, {
key: 'local-cusdc-usdc',
title: 'Local direct: cUSDC / USDC (official mirror, Chain 138)',
tokenIn: CHAIN_138_CUSDC_ADDRESS,
tokenOut: officialUsdc,
destinationChainId: 138,
amountIn: '1000000',
});
}
return queries;
}
const CHAIN_138_ROUTE_SWEEP_TOKENS = [
{ symbol: 'cUSDT', address: '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22' },
{ symbol: 'cUSDC', address: '0xf22258f57794CC8E06237084b353Ab30fFfa640b' },
{ symbol: 'cEURC', address: '0x8085961F9cF02b4d800A3c6d386D31da4B34266a' },
{ symbol: 'cEURT', address: '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72' },
{ symbol: 'cGBPC', address: '0x003960f16D9d34F2e98d62723B6721Fb92074aD2' },
{ symbol: 'cGBPT', address: '0x350f54e4D23795f86A9c03988c7135357CCaD97c' },
{ symbol: 'cAUDC', address: '0xD51482e567c03899eecE3CAe8a058161FD56069D' },
{ symbol: 'cJPYC', address: '0xEe269e1226a334182aace90056EE4ee5Cc8A6770' },
{ symbol: 'cCHFC', address: '0x873990849DDa5117d7C644f0aF24370797C03885' },
{ symbol: 'cCADC', address: '0x54dBd40cF05e15906A2C21f600937e96787f5679' },
{ symbol: 'cXAUC', address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' },
{ symbol: 'cXAUT', address: '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' }
];
function buildRouteSweepQueries(ctx) {
var officialUsdt = (ctx && ctx.officialUSDT) || '';
var officialUsdc = (ctx && ctx.officialUSDC) || '';
var queries = [];
CHAIN_138_ROUTE_SWEEP_TOKENS.forEach(function(token) {
var anchors = [];
if (token.symbol === 'cUSDT') {
anchors.push({ symbol: 'cUSDC', address: CHAIN_138_CUSDC_ADDRESS });
if (safeAddress(officialUsdt)) anchors.push({ symbol: 'USDT', address: officialUsdt });
} else if (token.symbol === 'cUSDC') {
anchors.push({ symbol: 'cUSDT', address: CHAIN_138_CUSDT_ADDRESS });
if (safeAddress(officialUsdc)) anchors.push({ symbol: 'USDC', address: officialUsdc });
} else {
anchors.push({ symbol: 'cUSDT', address: CHAIN_138_CUSDT_ADDRESS });
anchors.push({ symbol: 'cUSDC', address: CHAIN_138_CUSDC_ADDRESS });
}
anchors.forEach(function(anchor) {
if (!safeAddress(anchor.address)) return;
if (String(anchor.address).toLowerCase() === String(token.address).toLowerCase()) return;
queries.push({
key: token.symbol.toLowerCase() + '-' + anchor.symbol.toLowerCase(),
title: token.symbol + ' / ' + anchor.symbol + ' coverage probe',
symbol: token.symbol,
pairLabel: token.symbol + ' / ' + anchor.symbol,
tokenIn: token.address,
tokenOut: anchor.address,
destinationChainId: 138,
amountIn: '1000000'
});
});
});
return queries;
}
function stripHexPrefix(value) {
return String(value || '').replace(/^0x/i, '');
}
function encodeAddressWord(address) {
return stripHexPrefix(address).toLowerCase().padStart(64, '0');
}
function decodeAddressWord(data) {
var hex = stripHexPrefix(data);
if (!hex || hex.length < 64) return '';
return '0x' + hex.slice(hex.length - 40);
}
function decodeUint256Word(data, offsetWords) {
var hex = stripHexPrefix(data);
var start = (offsetWords || 0) * 64;
var word = hex.slice(start, start + 64);
if (!word) return 0n;
try { return BigInt('0x' + word); } catch (e) { return 0n; }
}
async function rpcEthCall(to, data) {
return rpcCall('eth_call', [{ to: to, data: data }, 'latest']);
}
async function rpcCodeExists(address) {
if (!safeAddress(address)) return false;
try {
var code = await rpcCall('eth_getCode', [address, 'latest']);
return !!code && code !== '0x';
} catch (e) {
return false;
}
}
async function rpcReadAddress(to, selector) {
if (!safeAddress(to)) return '';
try {
var out = await rpcEthCall(to, selector);
var addr = decodeAddressWord(out);
return safeAddress(addr) ? addr : '';
} catch (e) {
return '';
}
}
async function rpcReadMappedPool(integration, tokenA, tokenB) {
if (!safeAddress(integration) || !safeAddress(tokenA) || !safeAddress(tokenB)) return '';
try {
var out = await rpcEthCall(integration, SELECTOR_POOLS + encodeAddressWord(tokenA) + encodeAddressWord(tokenB));
var addr = decodeAddressWord(out);
return safeAddress(addr) ? addr : '';
} catch (e) {
return '';
}
}
async function rpcReadPrivatePool(registry, tokenA, tokenB) {
if (!safeAddress(registry) || !safeAddress(tokenA) || !safeAddress(tokenB)) return '';
try {
var out = await rpcEthCall(registry, SELECTOR_GET_PRIVATE_POOL + encodeAddressWord(tokenA) + encodeAddressWord(tokenB));
var addr = decodeAddressWord(out);
return safeAddress(addr) ? addr : '';
} catch (e) {
return '';
}
}
async function rpcReadBalanceOf(token, account) {
if (!safeAddress(token) || !safeAddress(account)) return 0n;
try {
var out = await rpcEthCall(token, SELECTOR_BALANCE_OF + encodeAddressWord(account));
return decodeUint256Word(out, 0);
} catch (e) {
return 0n;
}
}
async function rpcReadSymbol(token) {
if (!safeAddress(token)) return '';
try {
var out = await rpcEthCall(token, SELECTOR_SYMBOL);
var hex = stripHexPrefix(out);
if (!hex || hex.length < 128) return '';
var len = Number(BigInt('0x' + hex.slice(64, 128)));
if (!len || !Number.isFinite(len)) return '';
var dataHex = hex.slice(128, 128 + (len * 2));
var bytes = [];
for (var i = 0; i < dataHex.length; i += 2) {
bytes.push(parseInt(dataHex.slice(i, i + 2), 16));
}
return new TextDecoder().decode(new Uint8Array(bytes)).replace(/\0+$/, '');
} catch (e) {
return '';
}
}
function formatTokenUnits(value, decimals, precision) {
var amount = typeof value === 'bigint' ? value : BigInt(value || 0);
var scale = BigInt(Math.pow(10, Number(decimals || 0)));
if (scale === 0n) return '0';
var whole = amount / scale;
var fraction = amount % scale;
if (fraction === 0n) return whole.toString();
var fracStr = fraction.toString().padStart(Number(decimals || 0), '0');
var trimmed = fracStr.slice(0, Math.max(0, Number(precision == null ? 3 : precision))).replace(/0+$/, '');
return trimmed ? (whole.toString() + '.' + trimmed) : whole.toString();
}
async function fetchCurrentPmmContext() {
var integration = CHAIN_138_PMM_INTEGRATION_ADDRESS;
var officialUSDT = await rpcReadAddress(integration, SELECTOR_OFFICIAL_USDT);
var officialUSDC = await rpcReadAddress(integration, SELECTOR_OFFICIAL_USDC);
var compliantUSDT = await rpcReadAddress(integration, SELECTOR_COMPLIANT_USDT);
var compliantUSDC = await rpcReadAddress(integration, SELECTOR_COMPLIANT_USDC);
return {
integration: integration,
privateRegistry: CHAIN_138_PRIVATE_POOL_REGISTRY,
officialUSDT: officialUSDT,
officialUSDC: officialUSDC,
compliantUSDT: compliantUSDT || CHAIN_138_CUSDT_ADDRESS,
compliantUSDC: compliantUSDC || CHAIN_138_CUSDC_ADDRESS
};
}
function inferKnownTokenSymbol(address, ctx) {
var lower = String(address || '').toLowerCase();
if (lower === String((ctx && ctx.compliantUSDT) || CHAIN_138_CUSDT_ADDRESS).toLowerCase()) return 'cUSDT';
if (lower === String((ctx && ctx.compliantUSDC) || CHAIN_138_CUSDC_ADDRESS).toLowerCase()) return 'cUSDC';
if (lower === String((ctx && ctx.officialUSDT) || '').toLowerCase()) return 'USDT';
if (lower === String((ctx && ctx.officialUSDC) || '').toLowerCase()) return 'USDC';
var matched = CHAIN_138_ROUTE_SWEEP_TOKENS.find(function(token) { return String(token.address).toLowerCase() === lower; });
return matched ? matched.symbol : '';
}
async function buildLiveDirectRouteFallback(query, ctx) {
if (!query || Number(query.destinationChainId || query.chainId || 138) !== 138 || !safeAddress(query.tokenOut)) return null;
var poolAddress = await rpcReadMappedPool((ctx && ctx.integration) || CHAIN_138_PMM_INTEGRATION_ADDRESS, query.tokenIn, query.tokenOut);
if (!safeAddress(poolAddress) || !(await rpcCodeExists(poolAddress))) return null;
var reserveIn = await rpcReadBalanceOf(query.tokenIn, poolAddress);
var reserveOut = await rpcReadBalanceOf(query.tokenOut, poolAddress);
var funded = reserveIn > 0n && reserveOut > 0n;
var partial = (reserveIn > 0n || reserveOut > 0n) && !funded;
var status = funded ? 'live' : (partial ? 'partial' : 'unavailable');
var tokenInSymbol = inferKnownTokenSymbol(query.tokenIn, ctx) || await rpcReadSymbol(query.tokenIn) || shortenHash(query.tokenIn);
var tokenOutSymbol = inferKnownTokenSymbol(query.tokenOut, ctx) || await rpcReadSymbol(query.tokenOut) || shortenHash(query.tokenOut);
var reserveInFormatted = formatTokenUnits(reserveIn, CHAIN_138_POOL_BALANCE_DECIMALS, 3);
var reserveOutFormatted = formatTokenUnits(reserveOut, CHAIN_138_POOL_BALANCE_DECIMALS, 3);
return {
generatedAt: new Date().toISOString(),
source: {
chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet',
tokenIn: {
address: query.tokenIn,
symbol: tokenInSymbol,
name: tokenInSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
}
},
destination: {
chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet'
},
decision: 'direct-pool',
tree: [{
id: 'live-direct:' + String(poolAddress).toLowerCase(),
kind: 'direct-pool',
label: tokenInSymbol + '/' + tokenOutSymbol,
chainId: 138,
chainName: 'DeFi Oracle Meta Mainnet',
status: status,
depth: {
tvlUsd: 0,
reserve0: reserveInFormatted,
reserve1: reserveOutFormatted,
estimatedTradeCapacityUsd: 0,
freshnessSeconds: 0,
status: funded ? 'live' : (partial ? 'stale' : 'unavailable')
},
tokenIn: {
address: query.tokenIn,
symbol: tokenInSymbol,
name: tokenInSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
tokenOut: {
address: query.tokenOut,
symbol: tokenOutSymbol,
name: tokenOutSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
poolAddress: poolAddress,
dexType: 'DODO PMM',
path: [query.tokenIn, query.tokenOut],
notes: [
'Live fallback from on-chain DODOPMMIntegration mapping',
'Base reserve ' + reserveInFormatted,
'Quote reserve ' + reserveOutFormatted
]
}],
pools: [{
poolAddress: poolAddress,
dexType: 'DODO PMM',
token0: {
address: query.tokenIn,
symbol: tokenInSymbol,
name: tokenInSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
token1: {
address: query.tokenOut,
symbol: tokenOutSymbol,
name: tokenOutSymbol,
decimals: CHAIN_138_POOL_BALANCE_DECIMALS,
source: 'live-rpc'
},
depth: {
tvlUsd: 0,
reserve0: reserveInFormatted,
reserve1: reserveOutFormatted,
estimatedTradeCapacityUsd: 0,
freshnessSeconds: 0,
status: funded ? 'live' : (partial ? 'stale' : 'unavailable')
}
}],
missingQuoteTokenPools: []
};
}
async function discoverPublicXauPool(baseToken, anchors, integration) {
for (var i = 0; i < anchors.length; i++) {
var anchor = anchors[i];
var pool = await rpcReadMappedPool(integration, baseToken, anchor.address);
if (safeAddress(pool)) {
return { pool: pool, anchor: anchor };
}
}
return { pool: '', anchor: anchors[0] };
}
async function discoverPrivateXauPool(baseToken, anchors, registry) {
for (var i = 0; i < anchors.length; i++) {
var anchor = anchors[i];
var pool = await rpcReadPrivatePool(registry, baseToken, anchor.address);
if (safeAddress(pool)) {
return { pool: pool, anchor: anchor };
}
}
return { pool: '', anchor: anchors[0] };
}
async function getLivePoolRows() {
var ctx = await fetchCurrentPmmContext();
var xauAnchors = [
{ symbol: 'cXAUC', address: CHAIN_138_CXAUC_ADDRESS },
{ symbol: 'cXAUT', address: CHAIN_138_CXAUT_ADDRESS }
];
var rows = [];
async function buildLivePoolRow(category, poolPair, poolType, poolAddress, baseToken, quoteToken, notesPrefix) {
var codeExists = await rpcCodeExists(poolAddress);
var baseBal = codeExists ? await rpcReadBalanceOf(baseToken, poolAddress) : 0n;
var quoteBal = codeExists ? await rpcReadBalanceOf(quoteToken, poolAddress) : 0n;
var funded = baseBal > 0n && quoteBal > 0n;
var partial = (baseBal > 0n || quoteBal > 0n) && !funded;
var status = !safeAddress(poolAddress) ? 'Not created' : (funded ? 'Funded (live)' : (partial ? 'Partially funded' : (codeExists ? 'Created (unfunded)' : 'Missing code')));
var notes = [];
if (notesPrefix) notes.push(notesPrefix);
if (safeAddress(poolAddress) && codeExists) {
notes.push('Base reserve: ' + formatTokenUnits(baseBal, CHAIN_138_POOL_BALANCE_DECIMALS, 3));
notes.push('Quote reserve: ' + formatTokenUnits(quoteBal, CHAIN_138_POOL_BALANCE_DECIMALS, 3));
}
return { category: category, poolPair: poolPair, poolType: poolType, address: poolAddress || '', status: status, notes: notes.join(' | ') };
}
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDT / cUSDC', 'DODO PMM',
await rpcReadMappedPool(ctx.integration, ctx.compliantUSDT, ctx.compliantUSDC),
ctx.compliantUSDT, ctx.compliantUSDC, 'Derived from live DODOPMMIntegration state'));
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDT / USDT (official mirror)', 'DODO PMM',
await rpcReadMappedPool(ctx.integration, ctx.compliantUSDT, ctx.officialUSDT),
ctx.compliantUSDT, ctx.officialUSDT, (await rpcCodeExists(ctx.officialUSDT)) ? ('Live quote token ' + shortenHash(ctx.officialUSDT)) : 'Quote-side USDT contract missing on Chain 138'));
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDC / USDC (official mirror)', 'DODO PMM',
await rpcReadMappedPool(ctx.integration, ctx.compliantUSDC, ctx.officialUSDC),
ctx.compliantUSDC, ctx.officialUSDC, (await rpcCodeExists(ctx.officialUSDC)) ? ('Live quote token ' + shortenHash(ctx.officialUSDC)) : 'Quote-side USDC contract missing on Chain 138'));
var publicCusdtXau = await discoverPublicXauPool(ctx.compliantUSDT, xauAnchors, ctx.integration);
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDT / XAU (' + publicCusdtXau.anchor.symbol + ')', 'DODO PMM',
publicCusdtXau.pool, ctx.compliantUSDT, publicCusdtXau.anchor.address,
safeAddress(publicCusdtXau.pool) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered'));
var publicCusdcXau = await discoverPublicXauPool(ctx.compliantUSDC, xauAnchors, ctx.integration);
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cUSDC / XAU (' + publicCusdcXau.anchor.symbol + ')', 'DODO PMM',
publicCusdcXau.pool, ctx.compliantUSDC, publicCusdcXau.anchor.address,
safeAddress(publicCusdcXau.pool) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered'));
var publicCeurtXau = await discoverPublicXauPool(CHAIN_138_CEURT_ADDRESS, xauAnchors, ctx.integration);
rows.push(await buildLivePoolRow('Public Liquidity Pools', 'cEURT / XAU (' + publicCeurtXau.anchor.symbol + ')', 'DODO PMM',
publicCeurtXau.pool, CHAIN_138_CEURT_ADDRESS, publicCeurtXau.anchor.address,
safeAddress(publicCeurtXau.pool) ? 'Resolved via live integration pool mapping' : 'No live XAU public pool currently registered'));
var privateCusdtXau = await discoverPrivateXauPool(ctx.compliantUSDT, xauAnchors, ctx.privateRegistry);
rows.push(await buildLivePoolRow('Private Stabilization Pools', 'cUSDT ↔ XAU (' + privateCusdtXau.anchor.symbol + ')', 'PrivatePoolRegistry',
privateCusdtXau.pool, ctx.compliantUSDT, privateCusdtXau.anchor.address,
safeAddress(privateCusdtXau.pool) ? ('Registered in live PrivatePoolRegistry ' + shortenHash(ctx.privateRegistry)) : 'No live private XAU pool currently registered'));
var privateCusdcXau = await discoverPrivateXauPool(ctx.compliantUSDC, xauAnchors, ctx.privateRegistry);
rows.push(await buildLivePoolRow('Private Stabilization Pools', 'cUSDC ↔ XAU (' + privateCusdcXau.anchor.symbol + ')', 'PrivatePoolRegistry',
privateCusdcXau.pool, ctx.compliantUSDC, privateCusdcXau.anchor.address,
safeAddress(privateCusdcXau.pool) ? ('Registered in live PrivatePoolRegistry ' + shortenHash(ctx.privateRegistry)) : 'No live private XAU pool currently registered'));
var privateCeurtXau = await discoverPrivateXauPool(CHAIN_138_CEURT_ADDRESS, xauAnchors, ctx.privateRegistry);
rows.push(await buildLivePoolRow('Private Stabilization Pools', 'cEURT ↔ XAU (' + privateCeurtXau.anchor.symbol + ')', 'PrivatePoolRegistry',
privateCeurtXau.pool, CHAIN_138_CEURT_ADDRESS, privateCeurtXau.anchor.address,
safeAddress(privateCeurtXau.pool) ? ('Registered in live PrivatePoolRegistry ' + shortenHash(ctx.privateRegistry)) : 'No live private XAU pool currently registered'));
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'ReserveSystem',
poolType: 'Reserve',
address: RESERVE_SYSTEM_ADDRESS,
status: (await rpcCodeExists(RESERVE_SYSTEM_ADDRESS)) ? 'Deployed (live)' : 'Missing code',
notes: 'Live code check against Chain 138'
});
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'ReserveTokenIntegration',
poolType: 'Reserve',
address: RESERVE_TOKEN_INTEGRATION_ADDRESS,
status: (await rpcCodeExists(RESERVE_TOKEN_INTEGRATION_ADDRESS)) ? 'Deployed (live)' : 'Missing code',
notes: 'Live code check against Chain 138'
});
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'StablecoinReserveVault',
poolType: 'Reserve',
address: '',
status: 'External / Mainnet-side',
notes: 'Reserve vault lives on Mainnet by design; no local Chain 138 contract is expected here'
});
rows.push({
category: 'Reserve Pools / Vault Backing',
poolPair: 'Bridge_Vault',
poolType: 'Vault',
address: BRIDGE_VAULT_ADDRESS,
status: (await rpcCodeExists(BRIDGE_VAULT_ADDRESS)) ? 'Deployed (live)' : 'Missing code',
notes: 'Live code check against Chain 138'
});
rows.push({
category: 'Bridge Liquidity Pool',
poolPair: 'LiquidityPoolETH',
poolType: 'Bridge LP',
address: '',
status: 'External / Mainnet bridge LP',
notes: 'Trustless bridge liquidity pool is a Mainnet-side contract, not a local Chain 138 pool'
});
return { rows: rows, context: ctx };
}
function getActiveBridgeContractCount() {
return new Set([
'0x971cD9D156f193df8051E48043C476e53ECd4693',
'0xe0E93247376aa097dB308B92e6Ba36bA015535D0',
'0x2A0840e5117683b11682ac46f5CF5621E67269E3',
'0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03',
'0x8078a09637e47fa5ed34f626046ea2094a5cde5e',
'0xa780ef19a041745d353c9432f2a7f5a241335ffe',
'0x105f8a15b819948a89153505762444ee9f324684',
'0xdab0591e5e89295ffad75a71dcfc30c5625c4fa2'
].map(function(addr) { return String(addr || '').toLowerCase(); })).size;
}
function getAddressLabel(addr) { if (!addr) return ''; var lower = addr.toLowerCase(); if (KNOWN_ADDRESS_LABELS[lower]) return KNOWN_ADDRESS_LABELS[lower]; try { var j = localStorage.getItem('explorerAddressLabels'); if (!j) return ''; var m = JSON.parse(j); return m[lower] || ''; } catch(e){ return ''; } }
function formatAddressWithLabel(addr) { if (!addr) return ''; var label = getAddressLabel(addr); return label ? escapeHtml(label) + ' (' + escapeHtml(shortenHash(addr)) + ')' : escapeHtml(shortenHash(addr)); }
function copyToClipboard(val, msg) { if (!val) return; try { navigator.clipboard.writeText(String(val)); showToast(msg || 'Copied', 'success'); } catch(e) { showToast('Copy failed', 'error'); } }
function setAddressLabel(addr, label) { try { var j = localStorage.getItem('explorerAddressLabels') || '{}'; var m = JSON.parse(j); m[addr.toLowerCase()] = (label || '').trim(); localStorage.setItem('explorerAddressLabels', JSON.stringify(m)); return true; } catch(e){ return false; } }
function getWatchlist() { try { var j = localStorage.getItem('explorerWatchlist'); if (!j) return []; var a = JSON.parse(j); return Array.isArray(a) ? a : []; } catch(e){ return []; } }
function addToWatchlist(addr) { if (!addr || !/^0x[a-fA-F0-9]{40}$/i.test(addr)) return false; var a = getWatchlist(); var lower = addr.toLowerCase(); if (a.indexOf(lower) === -1) { a.push(lower); try { localStorage.setItem('explorerWatchlist', JSON.stringify(a)); return true; } catch(e){} } return false; }
function removeFromWatchlist(addr) { var a = getWatchlist().filter(function(x){ return x !== addr.toLowerCase(); }); try { localStorage.setItem('explorerWatchlist', JSON.stringify(a)); return true; } catch(e){ return false; } }
function isInWatchlist(addr) { return getWatchlist().indexOf((addr || '').toLowerCase()) !== -1; }
let currentView = 'home';
let _poolsRouteTreeRefreshTimer = null;
let currentDetailKey = '';
let latestPoolsSnapshot = null;
var _inNavHandler = false; // re-entrancy guard: prevents hashchange -> applyHashRoute -> stub from recursing
let provider = null;
let signer = null;
let userAddress = null;
// Tiered Architecture: Track and Authentication
let userTrack = 1; // Default to Track 1 (public)
let authToken = null;
// View switch helper: works even if rest of script fails. Do NOT set location.hash here (we use path-based URLs).
function switchToView(viewName) {
if (viewName !== 'pools' && viewName !== 'routes' && _poolsRouteTreeRefreshTimer) {
clearInterval(_poolsRouteTreeRefreshTimer);
_poolsRouteTreeRefreshTimer = null;
}
if (viewName !== 'blocks' && _blocksScrollAnimationId != null) {
cancelAnimationFrame(_blocksScrollAnimationId);
_blocksScrollAnimationId = null;
}
currentView = viewName;
var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','addresses','pools','routes','liquidity','more'];
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');
}
// Compatibility wrappers for nav buttons; re-entrancy guard prevents hashchange recursion.
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.showAddresses = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('addresses'); if (window._showAddresses) window._showAddresses(); } finally { _inNavHandler = false; } };
window.showBridgeMonitoring = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('bridge'); if (window._showBridgeMonitoring) window._showBridgeMonitoring(); } finally { _inNavHandler = false; } };
window.showWETHUtilities = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('weth'); if (window._showWETHUtilities) window._showWETHUtilities(); } finally { _inNavHandler = false; } };
window.showWETHTab = function() {};
window.showWatchlist = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('watchlist'); if (window._renderWatchlist) window._renderWatchlist(); } finally { _inNavHandler = false; } };
function openPoolsView() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('pools'); if (typeof renderPoolsView === 'function') renderPoolsView(); } finally { _inNavHandler = false; } }
window.openPoolsView = openPoolsView;
// Back-compat alias for older menu wiring; prefer openPoolsView() and renderPoolsView().
window.showPools = openPoolsView;
window.showRoutes = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('routes'); if (typeof renderRoutesView === 'function') renderRoutesView(); } finally { _inNavHandler = false; } };
window.showLiquidityAccess = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('liquidity'); if (typeof renderLiquidityAccessView === 'function') renderLiquidityAccessView(); } finally { _inNavHandler = false; } };
window.showMore = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('more'); if (window._showMore) window._showMore(); } finally { _inNavHandler = false; } };
window.showTokensList = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('tokens'); if (window._loadTokensList) window._loadTokensList(); } finally { _inNavHandler = false; } };
window.showAnalytics = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('analytics'); if (window._showAnalytics) window._showAnalytics(); } finally { _inNavHandler = false; } };
window.showOperator = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('operator'); if (window._showOperator) window._showOperator(); } finally { _inNavHandler = false; } };
// Compatibility wrappers for detail views; defer to the next tick to avoid synchronous recursion.
window.showBlockDetail = function(n) { if (window._showBlockDetail) setTimeout(function() { window._showBlockDetail(n); }, 0); };
window.showTransactionDetail = function(h) { if (window._showTransactionDetail) setTimeout(function() { window._showTransactionDetail(h); }, 0); };
window.showAddressDetail = function(a) { if (window._showAddressDetail) setTimeout(function() { window._showAddressDetail(a); }, 0); };
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(EXPLORER_API_V1_BASE + '/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(EXPLORER_API_V1_BASE + '/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(EXPLORER_API_V1_BASE + '/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) {
var msg = (error && error.message) ? String(error.message) : '';
var friendly = (error && error.code === 4001) || /not been authorized|rejected|denied/i.test(msg)
? 'Connection was rejected. Please approve the MetaMask popup to connect.'
: ('Failed to connect wallet: ' + (msg || 'Unknown error'));
alert(friendly);
}
}
// Check for stored auth token on load
window.addEventListener('DOMContentLoaded', () => {
const storedToken = localStorage.getItem('authToken');
const storedAddress = localStorage.getItem('userAddress');
if (storedToken && storedAddress) {
authToken = storedToken;
userAddress = storedAddress;
const walletBtn = document.getElementById('walletConnectBtn');
const walletStatus = document.getElementById('walletStatus');
const walletAddress = document.getElementById('walletAddress');
if (walletBtn) walletBtn.style.display = 'none';
if (walletStatus) walletStatus.style.display = 'flex';
if (walletAddress) walletAddress.textContent = shortenHash(storedAddress);
loadFeatureFlags();
} else {
const walletBtn = document.getElementById('walletConnectBtn');
if (walletBtn) walletBtn.style.display = 'block';
}
});
// WETH Contract Addresses
const WETH9_ADDRESS = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2';
// WETH10 address - will be checksummed when ethers is loaded
const WETH10_ADDRESS_RAW = '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f';
let WETH10_ADDRESS = WETH10_ADDRESS_RAW; // Will be updated to checksummed version
// Data adapter functions to normalize Blockscout API responses
function normalizeBlock(blockscoutBlock) {
if (!blockscoutBlock) return null;
return {
number: blockscoutBlock.height || blockscoutBlock.number || parseInt(blockscoutBlock.block_number, 10),
hash: blockscoutBlock.hash || blockscoutBlock.block_hash,
parent_hash: blockscoutBlock.parent_hash || blockscoutBlock.parentHash,
timestamp: blockscoutBlock.timestamp,
miner: blockscoutBlock.miner?.hash || blockscoutBlock.miner || blockscoutBlock.miner_hash,
transaction_count: blockscoutBlock.transaction_count || blockscoutBlock.transactions_count || 0,
gas_used: blockscoutBlock.gas_used || '0',
gas_limit: blockscoutBlock.gas_limit || blockscoutBlock.gasLimit || '0',
size: blockscoutBlock.size || 0,
difficulty: blockscoutBlock.difficulty || '0',
base_fee_per_gas: blockscoutBlock.base_fee_per_gas,
burnt_fees: blockscoutBlock.burnt_fees || '0',
total_difficulty: blockscoutBlock.total_difficulty || '0',
nonce: blockscoutBlock.nonce || '0x0',
extra_data: blockscoutBlock.extra_data || '0x'
};
}
function normalizeTransaction(blockscoutTx) {
if (!blockscoutTx) return null;
// Map status: "ok" -> 1, "error" -> 0, others -> 0
let status = 0;
if (blockscoutTx.status === 'ok' || blockscoutTx.status === 'success') {
status = 1;
} else if (blockscoutTx.status === 1 || blockscoutTx.status === '1') {
status = 1;
}
return {
hash: blockscoutTx.hash || blockscoutTx.tx_hash,
from: blockscoutTx.from?.hash || blockscoutTx.from || blockscoutTx.from_address_hash || blockscoutTx.from_address,
to: blockscoutTx.to?.hash || blockscoutTx.to || blockscoutTx.to_address_hash || blockscoutTx.to_address,
value: blockscoutTx.value || '0',
block_number: blockscoutTx.block_number || blockscoutTx.block || null,
block_hash: blockscoutTx.block_hash || blockscoutTx.blockHash || null,
transaction_index: blockscoutTx.position || blockscoutTx.transaction_index || blockscoutTx.index || 0,
gas_price: blockscoutTx.gas_price || blockscoutTx.max_fee_per_gas || '0',
gas_used: blockscoutTx.gas_used || '0',
gas_limit: blockscoutTx.gas_limit || blockscoutTx.gas || '0',
nonce: blockscoutTx.nonce || '0',
status: status,
created_at: blockscoutTx.timestamp || blockscoutTx.created_at || blockscoutTx.block_timestamp,
input: blockscoutTx.input || blockscoutTx.raw_input || '0x',
max_fee_per_gas: blockscoutTx.max_fee_per_gas,
max_priority_fee_per_gas: blockscoutTx.max_priority_fee_per_gas,
priority_fee: blockscoutTx.priority_fee,
tx_burnt_fee: blockscoutTx.tx_burnt_fee || blockscoutTx.burnt_fees || '0',
type: blockscoutTx.type || 0,
confirmations: blockscoutTx.confirmations || 0,
contract_address: blockscoutTx.created_contract_address_hash || (blockscoutTx.contract_creation && (blockscoutTx.to?.hash || blockscoutTx.to_address_hash)) || null,
revert_reason: blockscoutTx.revert_reason || blockscoutTx.error || blockscoutTx.result || null,
decoded_input: blockscoutTx.decoded_input || null,
method_id: blockscoutTx.method_id || null
};
}
function normalizeAddress(blockscoutAddr) {
if (!blockscoutAddr || typeof blockscoutAddr !== 'object') return null;
var hash = blockscoutAddr.hash || blockscoutAddr.address || blockscoutAddr.address_hash;
if (!hash && blockscoutAddr.creator_address_hash === undefined) return null;
var creationTx = blockscoutAddr.creation_tx_hash || blockscoutAddr.creator_tx_hash || blockscoutAddr.creation_transaction_hash || null;
var firstSeen = blockscoutAddr.first_transaction_at || blockscoutAddr.first_seen_at || blockscoutAddr.first_tx_at || null;
var lastSeen = blockscoutAddr.last_transaction_at || blockscoutAddr.last_seen_at || blockscoutAddr.last_tx_at || null;
var txSent = blockscoutAddr.transactions_sent_count;
if (txSent == null) txSent = blockscoutAddr.tx_sent_count;
if (txSent == null) txSent = blockscoutAddr.transactions_count;
var txReceived = blockscoutAddr.transactions_received_count;
if (txReceived == null) txReceived = blockscoutAddr.tx_received_count;
if (txReceived == null) txReceived = 0;
return {
address: hash || null,
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,
tx_sent: txSent != null ? txSent : 0,
tx_received: txReceived != null ? txReceived : 0,
label: blockscoutAddr.name || blockscoutAddr.ens_domain_name || null,
name: blockscoutAddr.name || null,
ens_domain_name: blockscoutAddr.ens_domain_name || null,
creation_tx_hash: creationTx,
first_seen_at: firstSeen,
last_seen_at: lastSeen
};
}
function hexToDecimalString(value) {
if (value == null || value === '') return '0';
var stringValue = String(value);
if (!/^0x/i.test(stringValue)) return stringValue;
try {
return BigInt(stringValue).toString();
} catch (e) {
return '0';
}
}
function hexToNumber(value) {
if (value == null || value === '') return 0;
if (typeof value === 'number') return value;
try {
return Number(BigInt(String(value)));
} catch (e) {
return Number(value) || 0;
}
}
function rpcTimestampToIso(value) {
var timestampSeconds = hexToNumber(value);
if (!timestampSeconds) return new Date(0).toISOString();
return new Date(timestampSeconds * 1000).toISOString();
}
function normalizeRpcBlock(block) {
if (!block || !block.hash) return null;
return normalizeBlock({
number: hexToDecimalString(block.number),
hash: block.hash,
parent_hash: block.parentHash,
timestamp: rpcTimestampToIso(block.timestamp),
miner: block.miner,
transaction_count: Array.isArray(block.transactions) ? block.transactions.length : 0,
gas_used: hexToDecimalString(block.gasUsed),
gas_limit: hexToDecimalString(block.gasLimit),
size: hexToNumber(block.size),
difficulty: hexToDecimalString(block.difficulty),
base_fee_per_gas: block.baseFeePerGas ? hexToDecimalString(block.baseFeePerGas) : undefined,
nonce: block.nonce || '0x0'
});
}
function normalizeRpcTransaction(tx, receipt, block) {
if (!tx || !tx.hash) return null;
var blockTimestamp = block && block.timestamp ? rpcTimestampToIso(block.timestamp) : new Date(0).toISOString();
var txType = tx.type != null ? hexToNumber(tx.type) : 0;
var effectiveGasPrice = receipt && receipt.effectiveGasPrice ? receipt.effectiveGasPrice : tx.gasPrice;
return normalizeTransaction({
hash: tx.hash,
from: tx.from,
to: tx.to,
value: hexToDecimalString(tx.value),
block_number: tx.blockNumber ? hexToDecimalString(tx.blockNumber) : null,
block_hash: tx.blockHash || null,
transaction_index: tx.transactionIndex ? hexToNumber(tx.transactionIndex) : 0,
gas_price: effectiveGasPrice ? hexToDecimalString(effectiveGasPrice) : '0',
gas_used: receipt && receipt.gasUsed ? hexToDecimalString(receipt.gasUsed) : '0',
gas_limit: tx.gas ? hexToDecimalString(tx.gas) : '0',
nonce: tx.nonce != null ? String(hexToNumber(tx.nonce)) : '0',
status: receipt && receipt.status != null ? hexToNumber(receipt.status) : 0,
created_at: blockTimestamp,
input: tx.input || '0x',
max_fee_per_gas: tx.maxFeePerGas ? hexToDecimalString(tx.maxFeePerGas) : undefined,
max_priority_fee_per_gas: tx.maxPriorityFeePerGas ? hexToDecimalString(tx.maxPriorityFeePerGas) : undefined,
type: txType,
contract_address: receipt && receipt.contractAddress ? receipt.contractAddress : null
});
}
async function fetchChain138BlocksPage(page, pageSize) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks?page=${page}&page_size=${pageSize}`);
if (response && response.items) {
return response.items.map(normalizeBlock).filter(function(block) { return block !== null; });
}
} catch (error) {
console.warn('Falling back to RPC blocks list:', error.message || error);
}
var blocks = [];
var latestBlockHex = await rpcCall('eth_blockNumber', []);
var latestBlock = hexToNumber(latestBlockHex);
var startIndex = Math.max(0, latestBlock - ((page - 1) * pageSize));
for (var i = 0; i < pageSize && startIndex - i >= 0; i++) {
var block = await rpcCall('eth_getBlockByNumber', ['0x' + (startIndex - i).toString(16), false]).catch(function() { return null; });
var normalized = normalizeRpcBlock(block);
if (normalized) blocks.push(normalized);
}
return blocks;
}
async function fetchChain138TransactionsPage(page, pageSize) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions?page=${page}&page_size=${pageSize}`);
if (response && response.items) {
return response.items.map(normalizeTransaction).filter(function(tx) { return tx !== null; });
}
} catch (error) {
console.warn('Falling back to RPC transactions list:', error.message || error);
}
var transactions = [];
var latestBlockHex = await rpcCall('eth_blockNumber', []);
var latestBlock = hexToNumber(latestBlockHex);
var blockCursor = Math.max(0, latestBlock - ((page - 1) * 5));
while (blockCursor >= 0 && transactions.length < pageSize) {
var block = await rpcCall('eth_getBlockByNumber', ['0x' + blockCursor.toString(16), true]).catch(function() { return null; });
if (block && Array.isArray(block.transactions)) {
var blockIsoTimestamp = rpcTimestampToIso(block.timestamp);
block.transactions.forEach(function(tx) {
if (transactions.length >= pageSize) return;
var normalized = normalizeTransaction({
hash: tx.hash,
from: tx.from,
to: tx.to,
value: hexToDecimalString(tx.value),
block_number: tx.blockNumber ? hexToDecimalString(tx.blockNumber) : hexToDecimalString(block.number),
block_hash: tx.blockHash || block.hash,
gas_price: tx.gasPrice ? hexToDecimalString(tx.gasPrice) : (tx.maxFeePerGas ? hexToDecimalString(tx.maxFeePerGas) : '0'),
gas_limit: tx.gas ? hexToDecimalString(tx.gas) : '0',
nonce: tx.nonce != null ? String(hexToNumber(tx.nonce)) : '0',
created_at: blockIsoTimestamp,
input: tx.input || '0x',
max_fee_per_gas: tx.maxFeePerGas ? hexToDecimalString(tx.maxFeePerGas) : undefined,
max_priority_fee_per_gas: tx.maxPriorityFeePerGas ? hexToDecimalString(tx.maxPriorityFeePerGas) : undefined,
type: tx.type != null ? hexToNumber(tx.type) : 0
});
if (normalized) transactions.push(normalized);
});
}
blockCursor -= 1;
}
return transactions;
}
async function fetchChain138BlockDetail(blockNumber) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/blocks/${blockNumber}`);
return {
block: normalizeBlock(response),
rawBlockResponse: response
};
} catch (error) {
console.warn('Falling back to RPC block detail:', error.message || error);
}
var rpcBlock = await rpcCall('eth_getBlockByNumber', ['0x' + Number(blockNumber).toString(16), true]);
return {
block: normalizeRpcBlock(rpcBlock),
rawBlockResponse: null
};
}
async function fetchChain138TransactionDetail(txHash) {
try {
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/transactions/${txHash}`);
return {
transaction: normalizeTransaction(response),
rawTransaction: response
};
} catch (error) {
console.warn('Falling back to RPC transaction detail:', error.message || error);
}
var rpcTx = await rpcCall('eth_getTransactionByHash', [txHash]);
if (!rpcTx) {
return { transaction: null, rawTransaction: null };
}
var receipt = await rpcCall('eth_getTransactionReceipt', [txHash]).catch(function() { return null; });
var block = rpcTx.blockNumber ? await rpcCall('eth_getBlockByNumber', [rpcTx.blockNumber, false]).catch(function() { return null; }) : null;
return {
transaction: normalizeRpcTransaction(rpcTx, receipt, block),
rawTransaction: null
};
}
// Skeleton loader function
function createSkeletonLoader(type) {
switch(type) {
case 'stats':
return `
`;
case 'table':
return `
${Array(5).fill(0).map(() => `
`).join('')}
`;
case 'detail':
return `
${Array(8).fill(0).map(() => `
`).join('')}
`;
default:
return '
Loading...
';
}
}
/**
* 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://explorer.d-bis.org']
}],
});
} catch (addError) {
throw addError;
}
} else {
throw switchError;
}
}
}
function disconnectMetaMask() {
provider = null;
signer = null;
userAddress = null;
const statusEl = document.getElementById('metamaskStatus');
statusEl.className = 'metamask-status disconnected';
statusEl.innerHTML = `
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 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 += '
';
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.
';
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: 'Canonical route matrix',
method: 'GET',
href: publicApiBase + '/routes/matrix',
notes: 'Full live-route inventory with optional blocked and planned route visibility.'
},
{
title: 'Live ingestion export',
method: 'GET',
href: publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap',
notes: 'Flat export for adapter discovery and route ingestion.'
},
{
title: 'Partner payload templates',
method: 'GET',
href: publicApiBase + '/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true',
notes: 'Builds request templates for 1inch, 0x, and LiFi from live routes.'
},
{
title: 'Resolve supported partner payloads',
method: 'POST',
href: publicApiBase + '/routes/partner-payloads/resolve',
notes: 'Accepts partner, amount, and addresses and returns supported payloads by default.'
},
{
title: 'Dispatch supported partner payload',
method: 'POST',
href: publicApiBase + '/routes/partner-payloads/dispatch',
notes: 'Dispatches one supported partner payload when the chain is publicly supported.'
},
{
title: 'Internal Chain 138 execution plan',
method: 'POST',
href: publicApiBase + '/routes/internal-execution-plan',
notes: 'Returns the DODO PMM fallback execution plan when public partner support is unavailable.'
}
];
var requestExamples = [
'GET ' + publicApiBase + '/routes/matrix?includeNonLive=true',
'GET ' + publicApiBase + '/routes/ingestion?fromChainId=138&routeType=swap',
'GET ' + publicApiBase + '/routes/partner-payloads?partner=LiFi&amount=1000000&includeUnsupported=true',
'POST ' + publicApiBase + '/routes/partner-payloads/resolve',
'POST ' + publicApiBase + '/routes/internal-execution-plan'
];
var html = '';
html += '
';
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] !== 'more') updatePath('/more');
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: '/more' },
{ title: 'CSV Export', icon: 'fa-file-csv', status: 'Live', badgeClass: 'badge-success', desc: 'Export pool state and route inventory snapshots for operator review and downstream ingestion.', action: 'showPools(); updatePath(\'/pools\'); setTimeout(function(){ if (typeof exportPoolsCSV === \"function\") exportPoolsCSV(); }, 200);', href: '/pools' },
{ title: 'Account Balance Checker', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Jump into indexed addresses to inspect balances, token inventory, internal transfers, and recent activity.', action: 'showAddresses();', href: '/addresses' }
]
},
{
key: 'explore',
title: 'Explore',
items: [
{ 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: '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: '/more' }
]
},
{
key: 'services',
title: 'Services',
items: [
{ title: 'Token Approvals', icon: 'fa-shield-halved', status: 'External', badgeClass: 'badge-warning', desc: 'Jump to revoke.cash for wallet approval review. Address detail pages also expose approval shortcuts directly.', action: 'openExternalMoreLink(\'https://revoke.cash/\');', href: '#' },
{ title: 'Verified Signature', icon: 'fa-signature', status: 'Live', badgeClass: 'badge-success', desc: 'Use wallet sign-in and verified address flows already built into the explorer authentication surfaces.', action: 'showWalletModal();', href: '/more' },
{ title: 'Input Data Messages', icon: 'fa-message', status: 'Live', badgeClass: 'badge-info', desc: 'Transaction detail pages already surface decoded input data, event logs, and contract interaction context.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Advanced Filter', icon: 'fa-filter', status: 'Live', badgeClass: 'badge-success', desc: 'Block, transaction, address, token, pool, bridge, and watchlist screens all support focused page-level filtering.', action: 'showTransactionsList();', href: '/transactions' },
{ 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 += '
Tools & Services
';
html += '
Discover more of SolaceScanScout's explorer tools in one place, grouped the way users expect from Etherscan-style explorers.
';
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 ? '/more' : item.href;
html += '';
html += '
Wei, gwei, ether, and 6-decimal stablecoin units for Chain 138.
' +
'' +
'
' +
'
' +
'' +
'' +
'' +
'
' +
'
';
document.body.appendChild(modal);
function renderUnitConverterResults() {
var amountEl = document.getElementById('unitConverterAmount');
var unitEl = document.getElementById('unitConverterUnit');
var resultsEl = document.getElementById('unitConverterResults');
if (!amountEl || !unitEl || !resultsEl) return;
var amount = Number(amountEl.value || '0');
var unit = unitEl.value;
if (!isFinite(amount) || amount < 0) {
resultsEl.innerHTML = '
Enter a non-negative amount to convert.
';
return;
}
var wei = 0;
if (unit === 'ether') wei = amount * 1e18;
else if (unit === 'gwei') wei = amount * 1e9;
else if (unit === 'wei') wei = amount;
else if (unit === 'stable') wei = amount * 1e6;
var etherValue = unit === 'stable' ? 'N/A' : (wei / 1e18).toLocaleString(undefined, { maximumFractionDigits: 18 });
var gweiValue = unit === 'stable' ? 'N/A' : (wei / 1e9).toLocaleString(undefined, { maximumFractionDigits: 9 });
var stableValue = (unit === 'stable' ? amount : wei / 1e6).toLocaleString(undefined, { maximumFractionDigits: 6 });
resultsEl.innerHTML =
'
';
const transfersFilter = getExplorerPageFilter('tokenTransfers');
const transfersFilterBar = renderPageFilterBar('tokenTransfers', 'Filter by from, to, value, or tx hash...', 'Filters the recent transfers below.', 'showTokenDetail(\'' + addrEsc + '\')');
html += '
Recent Transfers
';
if (transfers.length === 0) {
html += transfersFilterBar + '
No transfers
';
} else {
const filteredTransfers = transfersFilter ? transfers.filter(function(tr) {
var from = tr.from?.hash || tr.from || '-';
var to = tr.to?.hash || tr.to || '-';
var val = tr.total?.value != null ? tr.total.value : (tr.value || '0');
var dec = tr.token?.decimals != null ? tr.token.decimals : decimals;
var v = Number(val) / Math.pow(10, parseInt(dec, 10));
var txHash = tr.transaction_hash || tr.tx_hash || '';
return matchesExplorerFilter([from, to, v.toLocaleString(undefined, { maximumFractionDigits: 6 }), txHash].join(' '), transfersFilter);
}) : transfers;
html += transfersFilterBar + '
From
To
Value
Tx
';
filteredTransfers.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 += '
';
}
}
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 += '