refactor: rename SolaceScanScout to Solace and update related configurations

- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
defiQUG
2026-04-10 12:52:17 -07:00
parent 6eef6b07f6
commit 0972178cc5
160 changed files with 13274 additions and 1061 deletions

View File

@@ -73,13 +73,13 @@
if (j.error) throw new Error(j.error.message || 'RPC error');
return j.result;
}
const BLOCKSCOUT_API_ORIGIN = 'https://explorer.d-bis.org/api'; // fallback when not on explorer host
const BLOCKSCOUT_API_ORIGIN = 'https://blockscout.defi-oracle.io/api'; // fallback when not on explorer host
// Use relative /api when on explorer host so API always hits same host (avoids CORS/origin mismatch with www, port, or proxy)
const EXPLORER_HOSTS = ['explorer.d-bis.org', '192.168.11.140'];
const EXPLORER_HOSTS = ['explorer.d-bis.org', 'blockscout.defi-oracle.io', '192.168.11.140'];
const isOnExplorerHost = (typeof window !== 'undefined' && window.location && window.location.hostname && EXPLORER_HOSTS.indexOf(window.location.hostname) !== -1);
const BLOCKSCOUT_API = isOnExplorerHost ? '/api' : BLOCKSCOUT_API_ORIGIN;
const EXPLORER_ORIGINS = ['https://explorer.d-bis.org', 'http://explorer.d-bis.org', 'http://192.168.11.140', 'https://192.168.11.140'];
const EXPLORER_ORIGIN = (typeof window !== 'undefined' && window.location && EXPLORER_ORIGINS.includes(window.location.origin)) ? window.location.origin : 'https://explorer.d-bis.org';
const EXPLORER_ORIGINS = ['https://explorer.d-bis.org', 'http://explorer.d-bis.org', 'https://blockscout.defi-oracle.io', 'http://blockscout.defi-oracle.io', 'http://192.168.11.140', 'https://192.168.11.140'];
const EXPLORER_ORIGIN = (typeof window !== 'undefined' && window.location && EXPLORER_ORIGINS.includes(window.location.origin)) ? window.location.origin : 'https://blockscout.defi-oracle.io';
var I18N = {
en: { home: 'Home', blocks: 'Blocks', transactions: 'Transactions', addresses: 'Addresses', bridge: 'Bridge', weth: 'WETH', tokens: 'Tokens', pools: 'Pools', more: 'More', analytics: 'Analytics', operator: 'Operator', watchlist: 'Watchlist', searchPlaceholder: 'Address, tx hash, block number, or token/contract name...', connectWallet: 'Connect Wallet', darkMode: 'Dark mode', lightMode: 'Light mode', back: 'Back', exportCsv: 'Export CSV', tokenBalances: 'Token Balances', internalTxns: 'Internal Txns', readContract: 'Read contract', writeContract: 'Write contract', addToWatchlist: 'Add to watchlist', removeFromWatchlist: 'Remove from watchlist', checkApprovals: 'Check token approvals', copied: 'Copied' },
de: { home: 'Start', blocks: 'Blöcke', transactions: 'Transaktionen', addresses: 'Adressen', bridge: 'Brücke', weth: 'WETH', tokens: 'Token', pools: 'Pools', more: 'Mehr', analytics: 'Analysen', operator: 'Operator', watchlist: 'Beobachtungsliste', searchPlaceholder: 'Adresse, Tx-Hash, Blocknummer oder Token/Vertrag…', connectWallet: 'Wallet verbinden', darkMode: 'Dunkelmodus', lightMode: 'Hellmodus', back: 'Zurück', exportCsv: 'CSV exportieren', tokenBalances: 'Token-Bestände', internalTxns: 'Interne Transaktionen', readContract: 'Vertrag lesen', writeContract: 'Vertrag schreiben', addToWatchlist: 'Zur Beobachtungsliste', removeFromWatchlist: 'Aus Beobachtungsliste entfernen', checkApprovals: 'Token-Freigaben prüfen', copied: 'Kopiert' },
@@ -1124,7 +1124,7 @@
}
// Sign message
const message = `Sign this message to authenticate with SolaceScanScout Explorer.\n\nNonce: ${nonceData.nonce}`;
const message = `Sign this message to authenticate with SolaceScan.\n\nNonce: ${nonceData.nonce}`;
const signer = provider.getSigner();
const signature = await signer.signMessage(message);
@@ -1312,6 +1312,18 @@
};
}
function mergeAddressTabsCounters(addressDetail, counters) {
if (!addressDetail || !counters || typeof counters !== 'object') return addressDetail;
var merged = Object.assign({}, addressDetail);
if (counters.transactions_count != null) {
merged.transaction_count = Number(counters.transactions_count) || 0;
}
if (counters.token_balances_count != null) {
merged.token_count = Number(counters.token_balances_count) || 0;
}
return merged;
}
function hexToDecimalString(value) {
if (value == null || value === '') return '0';
var stringValue = String(value);
@@ -1494,7 +1506,24 @@
}
var rpcTx = await rpcCall('eth_getTransactionByHash', [txHash]);
if (!rpcTx) {
return { transaction: null, rawTransaction: null };
var latestBlock = null;
try {
latestBlock = await rpcCall('eth_blockNumber', []);
} catch (error) {
latestBlock = null;
}
return {
transaction: null,
rawTransaction: {
source: 'unavailable',
diagnostics: {
blockscout_indexed: false,
rpc_transaction_found: false,
rpc_receipt_found: false,
latest_block_number: latestBlock ? parseInt(latestBlock, 16) : null
}
}
};
}
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;
@@ -1517,6 +1546,16 @@
var response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/addresses/${normalizedAddress}`, 1, RETRY_DELAY_MS, ADDRESS_DETAIL_BLOCKSCOUT_TIMEOUT_MS);
var raw = response && (response.data !== undefined ? response.data : response.address !== undefined ? response.address : response.items && response.items[0] !== undefined ? response.items[0] : response);
var normalized = normalizeAddress(raw);
var needsCounters = raw && raw.hash && raw.transactions_count == null && raw.transaction_count == null && raw.tx_count == null;
needsCounters = needsCounters || (raw && raw.hash && raw.token_count == null);
if (normalized && normalized.hash && needsCounters) {
try {
var counters = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/addresses/${normalizedAddress}/tabs-counters`, 1, RETRY_DELAY_MS, ADDRESS_DETAIL_BLOCKSCOUT_TIMEOUT_MS);
normalized = mergeAddressTabsCounters(normalized, counters);
} catch (counterError) {
console.warn('Address counters unavailable:', counterError.message || counterError);
}
}
if (normalized && normalized.hash) {
return {
address: normalized,
@@ -2032,7 +2071,7 @@
decimals: 18
},
rpcUrls: RPC_URLS.length > 0 ? RPC_URLS : [RPC_URL],
blockExplorerUrls: [window.location.origin || 'https://explorer.d-bis.org']
blockExplorerUrls: [window.location.origin || 'https://blockscout.defi-oracle.io']
}],
});
} catch (addError) {
@@ -2786,7 +2825,7 @@
var decode = function(s) { try { return decodeURIComponent(s); } catch (e) { return s; } };
if (parts[0] === 'block' && parts[1]) { var p1 = decode(parts[1]); var key = 'block:' + p1; if (currentDetailKey === key) return; currentDetailKey = key; setTimeout(function() { showBlockDetail(p1); }, 0); return; }
if (parts[0] === 'tx' && parts[1]) { var p1 = decode(parts[1]); var txKey = 'tx:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)); if (currentDetailKey === txKey) return; currentDetailKey = txKey; setTimeout(function() { showTransactionDetail(p1); }, 0); return; }
if (parts[0] === 'address' && parts[1]) { var p1 = decode(parts[1]); var addrKey = 'address:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)); if (currentDetailKey === addrKey) return; currentDetailKey = addrKey; setTimeout(function() { showAddressDetail(p1); }, 0); return; }
if ((parts[0] === 'address' || parts[0] === 'addresses') && parts[1]) { var p1 = decode(parts[1]); var addrKey = 'address:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)); if (currentDetailKey === addrKey) return; currentDetailKey = addrKey; setTimeout(function() { showAddressDetail(p1); }, 0); return; }
if (parts[0] === 'token' && parts[1]) { var p1 = decode(parts[1]); var tokKey = 'token:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)); if (currentDetailKey === tokKey) return; currentDetailKey = tokKey; setTimeout(function() { showTokenDetail(p1); }, 0); return; }
if (parts[0] === 'nft' && parts[1] && parts[2]) { var p1 = decode(parts[1]), p2 = decode(parts[2]); var nftKey = 'nft:' + (p1 && typeof p1 === 'string' ? p1.toLowerCase() : String(p1)) + ':' + p2; if (currentDetailKey === nftKey) return; currentDetailKey = nftKey; setTimeout(function() { showNftDetail(p1, p2); }, 0); return; }
if (parts[0] === 'home') { if (currentView !== 'home') showHome(); return; }
@@ -2922,7 +2961,7 @@
case 'nft':
breadcrumbContainer = document.getElementById('nftDetailBreadcrumb');
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<a href="/address/' + encodeURIComponent(identifier) + '">' + escapeHtml(shortenHash(identifier)) + '</a>';
breadcrumbHTML += '<a href="/addresses/' + encodeURIComponent(identifier) + '">' + escapeHtml(shortenHash(identifier)) + '</a>';
breadcrumbHTML += '<span class="breadcrumb-separator">/</span>';
breadcrumbHTML += '<span class="breadcrumb-current">Token ID ' + (identifierExtra != null ? escapeHtml(String(identifierExtra)) : '') + '</span>';
break;
@@ -3614,7 +3653,7 @@
filteredBlocks.forEach(function(block) {
var d = normalizeBlockDisplay(block);
var blockNumber = escapeHtml(String(d.blockNum));
var blockHref = '/block/' + encodeURIComponent(String(d.blockNum));
var blockHref = '/blocks/' + encodeURIComponent(String(d.blockNum));
var blockLink = '<a href="' + blockHref + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + blockNumber + '\')" style="color: inherit; text-decoration: none; font-weight: 600;">' + blockNumber + '</a>';
var hashLink = safeBlockNumber(d.blockNum) ? '<a class="hash" href="' + blockHref + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + blockNumber + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(shortenHash(d.hash)) + '</a>' : '<span class="hash">' + escapeHtml(shortenHash(d.hash)) + '</span>';
html += '<tr onclick="showBlockDetail(\'' + blockNumber + '\')" style="cursor: pointer;"><td>' + blockLink + '</td><td>' + hashLink + '</td><td>' + escapeHtml(String(d.txCount)) + '</td><td>' + escapeHtml(d.timestampFormatted) + '</td></tr>';
@@ -3705,11 +3744,11 @@
const blockNumber = tx.block_number || 'N/A';
const valueFormatted = formatEther(value);
var safeHash = escapeHtml(hash);
var txHref = '/tx/' + encodeURIComponent(hash);
var txHref = '/transactions/' + encodeURIComponent(hash);
var hashLink = '<a class="hash" href="' + txHref + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + safeHash + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(shortenHash(hash)) + '</a>';
var fromLink = safeAddress(from) ? '<a class="hash" href="/address/' + encodeURIComponent(from) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(from) + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel(from) + '</a>' : formatAddressWithLabel(from);
var toLink = safeAddress(to) ? '<a class="hash" href="/address/' + encodeURIComponent(to) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(to || '') + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel(to) + '</a>' : (to ? formatAddressWithLabel(to) : '-');
var blockLink = safeBlockNumber(blockNumber) ? '<a href="/block/' + encodeURIComponent(String(blockNumber)) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeHtml(String(blockNumber)) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(String(blockNumber)) + '</a>' : escapeHtml(String(blockNumber));
var fromLink = safeAddress(from) ? '<a class="hash" href="/addresses/' + encodeURIComponent(from) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(from) + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel(from) + '</a>' : formatAddressWithLabel(from);
var toLink = safeAddress(to) ? '<a class="hash" href="/addresses/' + encodeURIComponent(to) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(to || '') + '\')" style="color: inherit; text-decoration: none;">' + formatAddressWithLabel(to) + '</a>' : (to ? formatAddressWithLabel(to) : '-');
var blockLink = safeBlockNumber(blockNumber) ? '<a href="/blocks/' + encodeURIComponent(String(blockNumber)) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeHtml(String(blockNumber)) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(String(blockNumber)) + '</a>' : escapeHtml(String(blockNumber));
html += '<tr onclick="showTransactionDetail(\'' + safeHash + '\')" style="cursor: pointer;"><td>' + hashLink + '</td><td>' + fromLink + '</td><td>' + toLink + '</td><td>' + escapeHtml(valueFormatted) + ' ETH</td><td>' + blockLink + '</td></tr>';
});
}
@@ -3789,7 +3828,7 @@
var tokenCount = Number(item.token_count || 0);
var lastSeen = String(item.last_seen_at || '—');
html += '<tr style="cursor: pointer;" onclick="showAddressDetail(\'' + escapeHtml(addr) + '\')">';
html += '<td><a class="hash" href="/address/' + encodeURIComponent(addr) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(addr) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(shortenHash(addr)) + '</a></td>';
html += '<td><a class="hash" href="/addresses/' + encodeURIComponent(addr) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeHtml(addr) + '\')" style="color: inherit; text-decoration: none;">' + escapeHtml(shortenHash(addr)) + '</a></td>';
html += '<td>' + escapeHtml(label || '—') + '</td>';
html += '<td>' + escapeHtml(type) + '</td>';
html += '<td>' + escapeHtml(String(txSent)) + '</td>';
@@ -4460,7 +4499,7 @@
html += '<button type="button" class="btn btn-primary" onclick="showRoutes(); updatePath(\'/routes\')" aria-label="Open routes view"><i class="fas fa-diagram-project"></i> Routes view</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showPools(); updatePath(\'/pools\')" aria-label="Open pools view"><i class="fas fa-water"></i> Pools view</button>';
html += '<button type="button" class="btn btn-secondary" onclick="showWETHUtilities(); updatePath(\'/weth\')" aria-label="Open WETH tools"><i class="fas fa-coins"></i> WETH tools</button>';
html += '<a class="btn btn-secondary" href="/docs.html" style="text-decoration:none;"><i class="fas fa-book"></i> Explorer docs</a>';
html += '<a class="btn btn-secondary" href="/docs" style="text-decoration:none;"><i class="fas fa-book"></i> Explorer docs</a>';
html += '</div></div></div>';
html += '</div>';
@@ -4470,7 +4509,7 @@
function renderMoreView() {
showView('more');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'more') updatePath('/more');
if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'operations') updatePath('/operations');
var container = document.getElementById('moreContent');
if (!container) return;
var groups = [
@@ -4479,7 +4518,7 @@
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: 'Unit Converter', icon: 'fa-scale-balanced', status: 'Live', badgeClass: 'badge-success', desc: 'Convert wei, gwei, ether, and common Chain 138 stablecoin units with a quick in-page helper.', action: 'showUnitConverterModal();', href: '/operations' },
{ title: 'CSV Export', icon: 'fa-file-csv', status: 'Live', badgeClass: 'badge-success', desc: 'Export pool state and route inventory snapshots for operator review and downstream ingestion.', action: 'showPools(); updatePath(\'/pools\'); setTimeout(function(){ if (typeof exportPoolsCSV === \"function\") exportPoolsCSV(); }, 200);', href: '/pools' },
{ title: 'Account Balance Checker', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Jump into indexed addresses to inspect balances, token inventory, internal transfers, and recent activity.', action: 'showAddresses();', href: '/addresses' }
]
@@ -4493,7 +4532,7 @@
{ 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' }
{ title: 'Domain Name Lookup', icon: 'fa-magnifying-glass', status: 'Live', badgeClass: 'badge-success', desc: 'Use the smart search launcher to resolve ENS-style names, domains, addresses, hashes, and token symbols.', action: 'openSmartSearchModal(\'\');', href: '/operations' }
]
},
{
@@ -4501,7 +4540,7 @@
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: 'Verified Signature', icon: 'fa-signature', status: 'Live', badgeClass: 'badge-success', desc: 'Use wallet sign-in and verified address flows already built into the explorer authentication surfaces.', action: 'showWalletModal();', href: '/operations' },
{ title: 'Input Data Messages', icon: 'fa-message', status: 'Live', badgeClass: 'badge-info', desc: 'Transaction detail pages already surface decoded input data, event logs, and contract interaction context.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'Advanced Filter', icon: 'fa-filter', status: 'Live', badgeClass: 'badge-success', desc: 'Block, transaction, address, token, pool, bridge, and watchlist screens all support focused page-level filtering.', action: 'showTransactionsList();', href: '/transactions' },
{ title: 'MetaMask Snap', icon: 'fa-wallet', status: 'Live', badgeClass: 'badge-success', desc: 'Open the Chain 138 MetaMask Snap companion for network setup, token list access, and wallet integration guidance.', action: 'window.location.href=\'/snap/\';', href: '/snap/' }
@@ -4511,8 +4550,8 @@
var html = '<div style="display:grid; grid-template-columns:minmax(240px, 0.9fr) repeat(3, minmax(220px, 1fr)); gap:1rem; align-items:start;">';
html += '<div style="border:1px solid var(--border); border-radius:18px; padding:1.25rem; background:linear-gradient(180deg, rgba(59,130,246,0.08), rgba(15,23,42,0.02)); min-height:100%;">';
html += '<div style="font-size:1.25rem; font-weight:800; margin-bottom:0.75rem;">Tools &amp; Services</div>';
html += '<div style="color:var(--text-light); line-height:1.7; margin-bottom:1rem;">Discover more of SolaceScanScout&apos;s explorer tools in one place, grouped the way users expect from Etherscan-style explorers.</div>';
html += '<div style="font-size:1.25rem; font-weight:800; margin-bottom:0.75rem;">Operations Hub</div>';
html += '<div style="color:var(--text-light); line-height:1.7; margin-bottom:1rem;">Discover SolaceScan operational explorer tools in one place, grouped the way users expect from a polished specialist explorer.</div>';
html += '<div style="display:grid; gap:0.75rem;">';
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Now live</div><div style="font-weight:700;">Route matrix, ingestion APIs, smart search, pool exports, and live Mainnet stable bridge discovery.</div></div>';
html += '<div style="padding:0.85rem; border:1px solid var(--border); border-radius:14px; background:var(--muted-surface);"><div style="font-size:0.82rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.35rem;">Good entry points</div><div style="display:flex; flex-wrap:wrap; gap:0.5rem;">';
@@ -4535,7 +4574,7 @@
: (item.href === '#'
? ('event.preventDefault(); ' + item.action + ' closeNavMenu();')
: ('event.preventDefault(); ' + item.action + ' updatePath(' + JSON.stringify(item.href) + '); closeNavMenu();'));
var href = disabled ? '/more' : item.href;
var href = disabled ? '/operations' : item.href;
html += '<a href="' + escapeAttr(href) + '" onclick="' + onclick + '" style="display:block; text-decoration:none; color:inherit; border:1px solid var(--border); border-radius:14px; padding:0.9rem; background:' + (disabled ? 'rgba(148,163,184,0.08)' : 'var(--muted-surface)') + '; opacity:' + (disabled ? '0.78' : '1') + ';">';
html += '<div style="display:flex; justify-content:space-between; gap:0.75rem; align-items:flex-start; margin-bottom:0.45rem;">';
html += '<div style="display:flex; align-items:center; gap:0.65rem; min-width:0;">';
@@ -4907,17 +4946,17 @@
function explorerAddressLink(address, content, style) {
var safe = safeAddress(address);
if (!safe) return content || 'N/A';
return '<a class="hash" href="/address/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(shortenHash(safe))) + '</a>';
return '<a class="hash" href="/addresses/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showAddressDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(shortenHash(safe))) + '</a>';
}
function explorerTransactionLink(txHash, content, style) {
var safe = safeTxHash(txHash);
if (!safe) return content || 'N/A';
return '<a class="hash" href="/tx/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(shortenHash(safe))) + '</a>';
return '<a class="hash" href="/transactions/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showTransactionDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(shortenHash(safe))) + '</a>';
}
function explorerBlockLink(blockNumber, content, style) {
var safe = safeBlockNumber(blockNumber);
if (!safe) return content || 'N/A';
return '<a href="/block/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(String(safe))) + '</a>';
return '<a href="/blocks/' + encodeURIComponent(safe) + '" onclick="event.preventDefault(); event.stopPropagation(); showBlockDetail(\'' + escapeJsSingleQuoted(safe) + '\')" style="' + escapeAttr(style || 'color: inherit; text-decoration: none;') + '">' + (content || escapeHtml(String(safe))) + '</a>';
}
function toBigIntSafe(value) {
if (value == null || value === '') return null;
@@ -4980,16 +5019,19 @@
if (value == null || value === '') return '';
return ' <button type="button" class="btn-copy" onclick="event.stopPropagation(); copyToClipboard(\'' + escapeJsSingleQuoted(String(value)) + '\', \'Copied\');" aria-label="' + escapeAttr(ariaLabel || 'Copy value') + '"><i class="fas fa-copy"></i></button>';
}
function renderInspectorCopyRow(valueHtml, copyValue, ariaLabel) {
return '<div class="tx-inspector-copy-row"><div class="tx-inspector-scroll">' + valueHtml + '</div>' + (copyValue != null && copyValue !== '' ? '<div class="tx-inspector-copy-action">' + renderCopyButtonHtml(copyValue, ariaLabel) + '</div>' : '') + '</div>';
}
function renderInspectorHtmlLine(label, valueHtml) {
return '<div class="tx-inspector-line"><div class="tx-inspector-label">' + escapeHtml(label) + '</div><div class="tx-inspector-content">' + (valueHtml || '<span class="tx-empty">N/A</span>') + '</div></div>';
}
function renderInspectorTextLine(label, value, copyValue) {
if (value == null || value === '') return renderInspectorHtmlLine(label, '<span class="tx-empty">N/A</span>');
return renderInspectorHtmlLine(label, '<span>' + escapeHtml(String(value)) + '</span>' + (copyValue != null ? renderCopyButtonHtml(copyValue, 'Copy ' + label) : ''));
return renderInspectorHtmlLine(label, renderInspectorCopyRow('<span>' + escapeHtml(String(value)) + '</span>', copyValue, 'Copy ' + label));
}
function renderInspectorCodeLine(label, value, copyValue) {
if (value == null || value === '') return renderInspectorHtmlLine(label, '<span class="tx-empty">N/A</span>');
return renderInspectorHtmlLine(label, '<div class="tx-inspector-scroll"><code class="tx-inspector-mono">' + escapeHtml(String(value)) + '</code>' + renderCopyButtonHtml(copyValue != null ? copyValue : value, 'Copy ' + label) + '</div>');
return renderInspectorHtmlLine(label, renderInspectorCopyRow('<code class="tx-inspector-mono">' + escapeHtml(String(value)) + '</code>', copyValue != null ? copyValue : value, 'Copy ' + label));
}
function renderNumericInspectorEntry(label, value, note, openByDefault) {
var repr = buildNumericRepresentations(value);
@@ -5027,6 +5069,155 @@
if (value == null || value === '') return '';
return typeof value === 'string' ? value : safeJsonStringify(value);
}
var KNOWN_LOG_SIGNATURES = {
transfer: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
approval: '0x8c5be1e5ebec7d5bd14f714f7e582d5c3b27c1d03c7d98cfc9b7c6f7d3a5b5d',
approvalForAll: '0x17307eab39ab6107e8899845ad3d59bd9653f200f220920489ca2b5937696c31',
transferSingle: '0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62'
};
function formatTopicSignaturePreview(topic) {
if (!topic) return '';
var value = String(topic);
return value.length > 20 ? value.slice(0, 12) + '…' + value.slice(-6) : value;
}
function normalizeHexWord(value) {
if (!value) return '';
var normalized = String(value).toLowerCase();
if (!normalized.startsWith('0x')) normalized = '0x' + normalized;
return normalized;
}
function splitHexDataWords(dataValue) {
var normalized = normalizeHexWord(dataValue);
if (!/^0x[0-9a-f]*$/i.test(normalized)) return [];
var payload = normalized.slice(2);
var words = [];
for (var i = 0; i < payload.length; i += 64) {
var word = payload.slice(i, i + 64);
if (word.length === 64) words.push('0x' + word);
}
return words;
}
function extractAddressFromTopic(topicValue) {
var normalized = normalizeHexWord(topicValue);
if (!/^0x[0-9a-f]{64}$/i.test(normalized)) return '';
return safeAddress('0x' + normalized.slice(-40)) || '';
}
function extractBoolFromWord(wordValue) {
var parsed = toBigIntSafe(wordValue);
if (parsed == null) return '';
return parsed === 0n ? 'false' : 'true';
}
function formatUintWord(wordValue) {
var parsed = toBigIntSafe(wordValue);
if (parsed == null) return '';
return formatGroupedDigits(parsed.toString(), 3, ',');
}
function detectKnownLogEvent(topics, dataValue) {
var topic0 = topics && topics[0] ? normalizeHexWord(topics[0]) : '';
var words = splitHexDataWords(dataValue);
if (!topic0) return null;
if (topic0 === KNOWN_LOG_SIGNATURES.transfer) {
if (topics.length >= 4) {
return {
name: 'Transfer',
standard: 'ERC-721',
fields: [
{ label: 'From', type: 'address', value: extractAddressFromTopic(topics[1]) },
{ label: 'To', type: 'address', value: extractAddressFromTopic(topics[2]) },
{ label: 'Token ID', type: 'uint', value: normalizeHexWord(topics[3]) }
]
};
}
if (topics.length >= 3 && words.length >= 1) {
return {
name: 'Transfer',
standard: 'ERC-20',
fields: [
{ label: 'From', type: 'address', value: extractAddressFromTopic(topics[1]) },
{ label: 'To', type: 'address', value: extractAddressFromTopic(topics[2]) },
{ label: 'Value', type: 'uint', value: words[0] }
]
};
}
}
if (topic0 === KNOWN_LOG_SIGNATURES.approval) {
if (topics.length >= 4) {
return {
name: 'Approval',
standard: 'ERC-721',
fields: [
{ label: 'Owner', type: 'address', value: extractAddressFromTopic(topics[1]) },
{ label: 'Approved', type: 'address', value: extractAddressFromTopic(topics[2]) },
{ label: 'Token ID', type: 'uint', value: normalizeHexWord(topics[3]) }
]
};
}
if (topics.length >= 3 && words.length >= 1) {
return {
name: 'Approval',
standard: 'ERC-20',
fields: [
{ label: 'Owner', type: 'address', value: extractAddressFromTopic(topics[1]) },
{ label: 'Spender', type: 'address', value: extractAddressFromTopic(topics[2]) },
{ label: 'Value', type: 'uint', value: words[0] }
]
};
}
}
if (topic0 === KNOWN_LOG_SIGNATURES.approvalForAll && topics.length >= 3 && words.length >= 1) {
return {
name: 'ApprovalForAll',
standard: 'ERC-721 / ERC-1155',
fields: [
{ label: 'Owner', type: 'address', value: extractAddressFromTopic(topics[1]) },
{ label: 'Operator', type: 'address', value: extractAddressFromTopic(topics[2]) },
{ label: 'Approved', type: 'bool', value: words[0] }
]
};
}
if (topic0 === KNOWN_LOG_SIGNATURES.transferSingle && topics.length >= 4 && words.length >= 2) {
return {
name: 'TransferSingle',
standard: 'ERC-1155',
fields: [
{ label: 'Operator', type: 'address', value: extractAddressFromTopic(topics[1]) },
{ label: 'From', type: 'address', value: extractAddressFromTopic(topics[2]) },
{ label: 'To', type: 'address', value: extractAddressFromTopic(topics[3]) },
{ label: 'Token ID', type: 'uint', value: words[0] },
{ label: 'Value', type: 'uint', value: words[1] }
]
};
}
return null;
}
function renderStructuredLogFields(eventInfo) {
if (!eventInfo || !Array.isArray(eventInfo.fields) || eventInfo.fields.length === 0) return '';
var lines = eventInfo.fields.map(function(field) {
if (!field || !field.value) return '';
if (field.type === 'address') {
return renderInspectorHtmlLine(field.label, renderInspectorCopyRow(explorerAddressLink(field.value, escapeHtml(field.value), 'color: inherit; text-decoration: none;'), field.value, 'Copy ' + field.label));
}
if (field.type === 'bool') {
var boolValue = extractBoolFromWord(field.value);
return renderInspectorTextLine(field.label, boolValue || 'N/A', boolValue || '');
}
if (field.type === 'uint') {
var numeric = formatUintWord(field.value);
return renderInspectorTextLine(field.label, numeric || 'N/A', field.value);
}
return renderInspectorTextLine(field.label, field.value, field.value);
}).filter(function(line) { return line; }).join('');
if (!lines) return '';
return '<div class="tx-inspector-structured">' +
'<div class="tx-inspector-structured-title">Structured Fields</div>' +
lines +
'</div>';
}
function renderTransactionLogEntry(log, idx) {
var addressValue = (log.address && (log.address.hash || log.address)) || log.address || '';
var topics = Array.isArray(log.topics) ? log.topics.filter(function(topic) { return topic != null; }) : [];
@@ -5035,28 +5226,33 @@
var dataBytes = /^0x[0-9a-f]*$/i.test(String(dataValue || '')) ? Math.max(0, (String(dataValue).length - 2) / 2) : 0;
var blockNumber = log.block_number != null ? String(log.block_number) : '';
var txHash = log.transaction_hash || log.transactionHash || '';
var knownEvent = detectKnownLogEvent(topics, dataValue);
var topicRows = topics.length ? '<div class="tx-inspector-topic-list">' + topics.map(function(topic, topicIndex) {
return '<div class="tx-inspector-topic-row"><div class="tx-inspector-topic-index">Topic ' + topicIndex + '</div><div class="tx-inspector-scroll"><code class="tx-inspector-mono">' + escapeHtml(String(topic)) + '</code>' + renderCopyButtonHtml(String(topic), 'Copy topic ' + topicIndex) + '</div></div>';
return '<div class="tx-inspector-topic-row"><div class="tx-inspector-topic-index">Topic ' + topicIndex + '</div>' + renderInspectorCopyRow('<code class="tx-inspector-mono">' + escapeHtml(String(topic)) + '</code>', String(topic), 'Copy topic ' + topicIndex) + '</div>';
}).join('') + '</div>' : '<span class="tx-empty">No topics</span>';
var metaChips = '<div class="tx-chip-row">' +
'<span class="tx-chip"><span class="tx-chip-label">Index</span><span>' + escapeHtml(String(log.index != null ? log.index : idx)) + '</span></span>' +
'<span class="tx-chip"><span class="tx-chip-label">Topics</span><span>' + escapeHtml(String(topics.length)) + '</span></span>' +
'<span class="tx-chip"><span class="tx-chip-label">Data</span><span>' + escapeHtml(String(dataBytes)) + ' bytes</span></span>' +
'<span class="tx-chip tx-chip-emphasis" id="txLogEventChip' + idx + '"><span class="tx-chip-label">Event</span><span>' + escapeHtml(knownEvent && knownEvent.name ? knownEvent.name : (decodedValue ? 'decoded' : (topics[0] ? formatTopicSignaturePreview(topics[0]) : 'raw log'))) + '</span></span>' +
(knownEvent && knownEvent.standard ? '<span class="tx-chip"><span class="tx-chip-label">Standard</span><span>' + escapeHtml(knownEvent.standard) + '</span></span>' : '') +
'</div>';
var summaryTitle = 'Log #' + escapeHtml(String(log.index != null ? log.index : idx)) + ' • ' + escapeHtml(knownEvent && knownEvent.name ? knownEvent.name : (addressValue ? shortenHash(addressValue) : 'Unknown address'));
var html = '<details class="tx-inspector-entry"' + (idx === 0 ? ' open' : '') + '>';
html += '<summary><span>Log #' + escapeHtml(String(log.index != null ? log.index : idx)) + '' + escapeHtml(addressValue ? shortenHash(addressValue) : 'Unknown address') + '</span><span class="tx-inspector-summary-value">' + escapeHtml(String(topics.length)) + ' topics / ' + escapeHtml(String(dataBytes)) + ' bytes</span></summary>';
html += '<summary><span id="txLogSummaryTitle' + idx + '">' + summaryTitle + '</span><span class="tx-inspector-summary-value">' + escapeHtml(String(topics.length)) + ' topics / ' + escapeHtml(String(dataBytes)) + ' bytes</span></summary>';
html += '<div class="tx-inspector-entry-body">';
html += metaChips;
html += renderInspectorHtmlLine('Address', addressValue ? explorerAddressLink(addressValue, escapeHtml(addressValue), 'color: inherit; text-decoration: none;') + renderCopyButtonHtml(addressValue, 'Copy log address') : '<span class="tx-empty">N/A</span>');
html += renderInspectorHtmlLine('Address', addressValue ? renderInspectorCopyRow(explorerAddressLink(addressValue, escapeHtml(addressValue), 'color: inherit; text-decoration: none;'), addressValue, 'Copy log address') : '<span class="tx-empty">N/A</span>');
if (blockNumber) {
html += renderInspectorHtmlLine('Block', explorerBlockLink(blockNumber, escapeHtml(blockNumber), 'color: inherit; text-decoration: none;'));
html += renderInspectorHtmlLine('Block', renderInspectorCopyRow(explorerBlockLink(blockNumber, escapeHtml(blockNumber), 'color: inherit; text-decoration: none;'), blockNumber, 'Copy block number'));
}
if (txHash) {
html += renderInspectorHtmlLine('Tx Hash', explorerTransactionLink(txHash, escapeHtml(txHash), 'color: inherit; text-decoration: none;'));
html += renderInspectorHtmlLine('Tx Hash', renderInspectorCopyRow(explorerTransactionLink(txHash, escapeHtml(txHash), 'color: inherit; text-decoration: none;'), txHash, 'Copy tx hash'));
}
html += renderStructuredLogFields(knownEvent);
html += renderInspectorHtmlLine('Topics', topicRows);
html += renderInspectorCodeLine('Data', dataValue, dataValue);
html += renderInspectorHtmlLine('Decoded', '<div id="txLogDecoded' + idx + '" class="tx-inspector-mono">' + (decodedValue ? escapeHtml(decodedValue) : '—') + '</div>' + (decodedValue ? renderCopyButtonHtml(decodedValue, 'Copy decoded log') : ''));
html += renderInspectorHtmlLine('Decoded', renderInspectorCopyRow('<div id="txLogDecoded' + idx + '" class="tx-inspector-mono">' + (decodedValue ? escapeHtml(decodedValue) : '—') + '</div>', decodedValue || '', 'Copy decoded log'));
html += '</div></details>';
return html;
}
@@ -5089,7 +5285,7 @@
blockNumber = bn;
currentDetailKey = 'block:' + blockNumber;
showView('blockDetail');
updatePath('/block/' + blockNumber);
updatePath('/blocks/' + blockNumber);
const container = document.getElementById('blockDetail');
updateBreadcrumb('block', blockNumber);
container.innerHTML = createSkeletonLoader('detail');
@@ -5207,7 +5403,7 @@
txHash = th;
currentDetailKey = 'tx:' + txHash.toLowerCase();
showView('transactionDetail');
updatePath('/tx/' + txHash);
updatePath('/transactions/' + txHash);
const container = document.getElementById('transactionDetail');
updateBreadcrumb('transaction', txHash);
container.innerHTML = createSkeletonLoader('detail');
@@ -5221,7 +5417,13 @@
var detailResult = await fetchChain138TransactionDetail(txHash);
rawTx = detailResult.rawTransaction;
t = detailResult.transaction;
if (!t) throw new Error('Transaction not found');
if (!t) {
var diagnostics = rawTx && rawTx.diagnostics ? rawTx.diagnostics : null;
if (diagnostics && diagnostics.rpc_transaction_found === false) {
throw new Error('Transaction not found in Blockscout or the Chain 138 public RPC. It may belong to a different network, have been replaced, or never broadcast successfully' + (diagnostics.latest_block_number ? ' (latest block #' + diagnostics.latest_block_number + ')' : ''));
}
throw new Error('Transaction not found');
}
} catch (error) {
container.innerHTML = '<div class="error">Failed to load transaction: ' + escapeHtml(error.message || 'Unknown error') + '. <button onclick="showTransactionDetail(\'' + escapeHtml(String(txHash)) + '\')" class="btn btn-primary" style="margin-top: 1rem;">Retry</button></div>';
return;
@@ -5579,6 +5781,15 @@
var args = parsed.args && parsed.args.length ? parsed.args.map(function(a) { return String(a); }).join(', ') : '';
decodedEl.textContent = parsed.name + '(' + args + ')';
decodedEl.title = parsed.signature || '';
var summaryTitleEl = document.getElementById('txLogSummaryTitle' + idx);
if (summaryTitleEl) {
summaryTitleEl.textContent = 'Log #' + String(log.index != null ? log.index : idx) + ' • ' + parsed.name;
}
var eventChipEl = document.getElementById('txLogEventChip' + idx);
if (eventChipEl) {
eventChipEl.innerHTML = '<span class="tx-chip-label">Event</span><span>' + escapeHtml(parsed.name) + '</span>';
eventChipEl.title = parsed.signature || '';
}
}
} catch (e) {}
});
@@ -5723,7 +5934,7 @@
address = addr;
currentDetailKey = 'address:' + address.toLowerCase();
showView('addressDetail');
updatePath('/address/' + address);
updatePath('/addresses/' + address);
const container = document.getElementById('addressDetail');
updateBreadcrumb('address', address);
container.innerHTML = createSkeletonLoader('detail');
@@ -5908,7 +6119,7 @@
const decimals = token.decimals != null ? token.decimals : 18;
const displayBalance = formatUnitsLocalized(balance, decimals, 6);
const type = token.type || b.token_type || 'ERC-20';
tbl += '<tr><td><a href="/token/' + encodeURIComponent(contract) + '">' + escapeHtml(symbol) + '</a></td><td>' + explorerAddressLink(contract, escapeHtml(shortenHash(contract)), 'color: inherit; text-decoration: none;') + '</td><td>' + escapeHtml(displayBalance) + '</td><td>' + escapeHtml(type) + '</td></tr>';
tbl += '<tr><td><a href="/tokens/' + encodeURIComponent(contract) + '">' + escapeHtml(symbol) + '</a></td><td>' + explorerAddressLink(contract, escapeHtml(shortenHash(contract)), 'color: inherit; text-decoration: none;') + '</td><td>' + escapeHtml(displayBalance) + '</td><td>' + escapeHtml(type) + '</td></tr>';
});
if (filteredItems.length === 0) {
tbl += '<tr><td colspan="4" style="text-align:center; padding: 1rem;">No token balances match the current filter.</td></tr>';
@@ -6236,7 +6447,7 @@
if (!/^0x[a-fA-F0-9]{40}$/.test(tokenAddress)) return;
currentDetailKey = 'token:' + tokenAddress.toLowerCase();
showView('tokenDetail');
updatePath('/token/' + tokenAddress);
updatePath('/tokens/' + tokenAddress);
var container = document.getElementById('tokenDetail');
updateBreadcrumb('token', tokenAddress);
container.innerHTML = createSkeletonLoader('detail');
@@ -6251,7 +6462,7 @@
} catch (e) {}
}
if (!data) {
container.innerHTML = '<p class="error">Token not found or not indexed.</p><p><a href="/address/' + encodeURIComponent(tokenAddress) + '">View as address</a></p>';
container.innerHTML = '<p class="error">Token not found or not indexed.</p><p><a href="/addresses/' + encodeURIComponent(tokenAddress) + '">View as address</a></p>';
return;
}
var knownTokenDetail = {