diff --git a/.gitignore b/.gitignore index a68d9dc..e965055 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,10 @@ backend/bin/ backend/api/rest/cmd/api-server backend/cmd +# Python +__pycache__/ +*.py[cod] + # Tooling / scratch directories out/ cache/ diff --git a/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json b/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json index e6c1198..ffa6b0e 100644 --- a/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json +++ b/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json @@ -3,10 +3,10 @@ "version": {"major": 1, "minor": 2, "patch": 0}, "defaultChainId": 138, "explorerUrl": "https://explorer.d-bis.org", - "tokenListUrl": "https://explorer.d-bis.org/api/config/token-list", + "tokenListUrl": "https://explorer.d-bis.org/api/v1/report/token-list?chainId=138", "generatedBy": "DBIS Explorer", "chains": [ - {"chainId":"0x8a","chainIdDecimal":138,"chainName":"DeFi Oracle Meta Mainnet","shortName":"dbis","rpcUrls":["https://rpc-http-pub.d-bis.org","https://rpc.d-bis.org","https://rpc2.d-bis.org","https://rpc.defi-oracle.io"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://explorer.d-bis.org","https://blockscout.defi-oracle.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"],"infoURL":"https://explorer.d-bis.org","explorerApiUrl":"https://explorer.d-bis.org/api/v2","testnet":false}, + {"chainId":"0x8a","chainIdDecimal":138,"chainName":"DeFi Oracle Meta Mainnet","shortName":"dbis","rpcUrls":["https://rpc-http-pub.d-bis.org","https://rpc.d-bis.org","https://rpc2.d-bis.org","https://rpc.defi-oracle.io"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://explorer.d-bis.org","https://blockscout.defi-oracle.io"],"iconUrls":["https://explorer.d-bis.org/token-icons/chain-138.png","https://explorer.d-bis.org/api/v1/report/logo/chain-138","https://explorer.d-bis.org/favicon.ico"],"infoURL":"https://explorer.d-bis.org","explorerApiUrl":"https://explorer.d-bis.org/api/v2","testnet":false}, {"chainId":"0x1","chainIdDecimal":1,"chainName":"Ethereum Mainnet","shortName":"eth","rpcUrls":["https://eth.llamarpc.com","https://rpc.ankr.com/eth","https://ethereum.publicnode.com","https://1rpc.io/eth"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://etherscan.io"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"],"infoURL":"https://ethereum.org","testnet":false}, {"chainId":"0x9f2c4","chainIdDecimal":651940,"chainName":"ALL Mainnet","shortName":"all","rpcUrls":["https://mainnet-rpc.alltra.global"],"nativeCurrency":{"name":"Ether","symbol":"ETH","decimals":18},"blockExplorerUrls":["https://alltra.global"],"iconUrls":["https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png"],"infoURL":"https://alltra.global","testnet":false}, {"chainId":"0x19","chainIdDecimal":25,"chainName":"Cronos Mainnet","rpcUrls":["https://evm.cronos.org","https://cronos-rpc.publicnode.com"],"nativeCurrency":{"name":"CRO","symbol":"CRO","decimals":18},"blockExplorerUrls":["https://cronos.org/explorer"],"iconUrls":["https://ipfs.io/ipfs/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong"]}, diff --git a/backend/api/rest/config/metamask/GRU_V2_PUBLIC_DEPLOYMENT_STATUS.json b/backend/api/rest/config/metamask/GRU_V2_PUBLIC_DEPLOYMENT_STATUS.json index 9eafa26..1def54f 100644 --- a/backend/api/rest/config/metamask/GRU_V2_PUBLIC_DEPLOYMENT_STATUS.json +++ b/backend/api/rest/config/metamask/GRU_V2_PUBLIC_DEPLOYMENT_STATUS.json @@ -129,7 +129,7 @@ "coveredSymbols": 10, "missingSymbols": [] }, - "note": "The public EVM cW token mesh is complete on the currently loaded 10-chain set, but Wemix remains a desired target without a cW suite in deployment-status.json." + "note": "The public EVM cW token mesh is aligned to the nine-chain promoted surface (Cronos excluded from that count); Wemix remains a desired target without a cW suite in deployment-status.json." }, "transport": { "liveTransportAssets": [ diff --git a/backend/config/metamask/DUAL_CHAIN_NETWORKS.json b/backend/config/metamask/DUAL_CHAIN_NETWORKS.json index 73ee120..f8dc9b0 100644 --- a/backend/config/metamask/DUAL_CHAIN_NETWORKS.json +++ b/backend/config/metamask/DUAL_CHAIN_NETWORKS.json @@ -25,7 +25,9 @@ "https://explorer.d-bis.org" ], "iconUrls": [ - "https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png" + "https://explorer.d-bis.org/token-icons/chain-138.png", + "https://explorer.d-bis.org/api/v1/report/logo/chain-138", + "https://explorer.d-bis.org/favicon.ico" ] }, { @@ -90,4 +92,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/frontend/FRONTEND_TASKS_AND_REVIEW.md b/frontend/FRONTEND_TASKS_AND_REVIEW.md index 2315cf1..2933c3a 100644 --- a/frontend/FRONTEND_TASKS_AND_REVIEW.md +++ b/frontend/FRONTEND_TASKS_AND_REVIEW.md @@ -97,7 +97,7 @@ - **Wallet status (1639, 1722)** – `statusEl.innerHTML` uses `shortenHash(userAddress)`. If `userAddress` were ever from an untrusted source, it should be escaped. **Action:** Use `escapeHtml(shortenHash(userAddress))` for consistency (in **H1**). - **loadGasAndNetworkStats (2509)** – `el.innerHTML` uses `gasGwei`, `blockTimeSec`, `tps`. These are from API; escaping is low risk but recommended for defense in depth. **Action:** Escape these values (in **H1** or small follow-up). - **Token list: `#/token/' + contract`** – The `contract` in `href="#/token/' + contract + '"` can break the attribute if it contains a quote. **Action:** Encode or validate; include in **H2** (safe href/attributes). -- **External link (3800)** – `'https://explorer.d-bis.org/address/' + addr + '/contract'` – `addr` should be validated or encoded so the URL cannot be malformed. **Action:** Use `encodeURIComponent(addr)` for the path segment (in **H2**). +- **External link (3800)** – `'https://explorer.d-bis.org/addresses/' + addr + '/contract'` – `addr` should be validated or encoded so the URL cannot be malformed. **Action:** Use `encodeURIComponent(addr)` for the path segment (in **H2**). ### 2.3 SPA: onclick and attribute injection diff --git a/frontend/public/explorer-spa.js b/frontend/public/explorer-spa.js index bde3f3e..e9e2e17 100644 --- a/frontend/public/explorer-spa.js +++ b/frontend/public/explorer-spa.js @@ -332,9 +332,11 @@ setTimeout(function() { input.focus(); input.select(); + if (typeof focusTrapStart === 'function') focusTrapStart(modal); }, 0); } function closeSmartSearchModal() { + if (typeof focusTrapEnd === 'function') focusTrapEnd(); var modal = document.getElementById('smartSearchModal'); if (!modal) return; modal.style.display = 'none'; @@ -964,6 +966,138 @@ 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; } + + var INSTITUTION_PREFS_KEY = 'explorer_inst_prefs_v1'; + function getInstitutionPrefs() { + try { + var j = localStorage.getItem(INSTITUTION_PREFS_KEY); + var o = j ? JSON.parse(j) : {}; + return { + dateFormat: o.dateFormat === 'local' ? 'local' : 'iso', + gasUnit: o.gasUnit === 'eth' ? 'eth' : 'gwei', + externalLinks: o.externalLinks === 'blockscout' ? 'blockscout' : 'spa', + addressRows: o.addressRows === 'compact' ? 'compact' : 'rich' + }; + } catch (e) { + return { dateFormat: 'iso', gasUnit: 'gwei', externalLinks: 'spa', addressRows: 'rich' }; + } + } + function setInstitutionPrefs(patch) { + var cur = getInstitutionPrefs(); + Object.assign(cur, patch || {}); + try { localStorage.setItem(INSTITUTION_PREFS_KEY, JSON.stringify(cur)); } catch (e) {} + applyInstitutionPrefsToDom(); + if (typeof syncContractVerifyBlockscoutLink === 'function') syncContractVerifyBlockscoutLink(); + return cur; + } + function applyInstitutionPrefsToDom() { + var p = getInstitutionPrefs(); + try { + document.body.classList.toggle('explorer-compact-table-rows', p.addressRows === 'compact'); + } catch (e) {} + } + function formatInstitutionTimestamp(isoOrMs) { + if (isoOrMs == null || isoOrMs === '') return '—'; + var d = typeof isoOrMs === 'number' ? new Date(isoOrMs) : new Date(isoOrMs); + if (!Number.isFinite(d.getTime())) return '—'; + var p = getInstitutionPrefs(); + if (p.dateFormat === 'local') return d.toLocaleString(); + return d.toISOString(); + } + var SAVED_VIEWS_KEY = 'explorer_saved_views_v1'; + function getSavedViews() { + try { var j = localStorage.getItem(SAVED_VIEWS_KEY); var a = j ? JSON.parse(j) : []; return Array.isArray(a) ? a : []; } catch (e) { return []; } + } + function saveSavedViews(arr) { try { localStorage.setItem(SAVED_VIEWS_KEY, JSON.stringify((arr || []).slice(0, 40))); } catch (e) {} } + var FILTER_PRESETS_KEY = 'explorer_filter_presets_v1'; + function getFilterPresets() { try { var j = localStorage.getItem(FILTER_PRESETS_KEY); return j ? JSON.parse(j) : {}; } catch (e) { return {}; } } + function setFilterPresets(m) { try { localStorage.setItem(FILTER_PRESETS_KEY, JSON.stringify(m || {})); } catch (e) {} } + var NOTIF_SETTINGS_KEY = 'explorer_notif_settings_v1'; + function getNotifSettings() { + try { + var j = localStorage.getItem(NOTIF_SETTINGS_KEY); + var o = j ? JSON.parse(j) : {}; + return { + enabled: !!o.enabled, + intervalMin: Math.max(5, Math.min(1440, parseInt(o.intervalMin, 10) || 60)) + }; + } catch (e) { return { enabled: false, intervalMin: 60 }; } + } + function setNotifSettings(patch) { + var cur = getNotifSettings(); + Object.assign(cur, patch || {}); + try { localStorage.setItem(NOTIF_SETTINGS_KEY, JSON.stringify(cur)); } catch (e) {} + return cur; + } + var NOTIF_SNAP_KEY = 'explorer_notif_snap_v1'; + function getNotifSnap() { try { var j = localStorage.getItem(NOTIF_SNAP_KEY); return j ? JSON.parse(j) : {}; } catch (e) { return {}; } } + function setNotifSnap(m) { try { localStorage.setItem(NOTIF_SNAP_KEY, JSON.stringify(m || {})); } catch (e) {} } + var REPORT_SCHED_KEY = 'explorer_report_schedules_v1'; + function getReportSchedules() { try { var j = localStorage.getItem(REPORT_SCHED_KEY); var a = j ? JSON.parse(j) : []; return Array.isArray(a) ? a : []; } catch (e) { return []; } } + function setReportSchedules(a) { try { localStorage.setItem(REPORT_SCHED_KEY, JSON.stringify((a || []).slice(0, 20))); } catch (e) {} } + + function decodeExplorerJwtPayload(token) { + if (!token || typeof token !== 'string') return null; + var parts = token.split('.'); + if (parts.length < 2) return null; + try { + var b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/'); + var pad = b64.length % 4 ? '='.repeat(4 - (b64.length % 4)) : ''; + var json = atob(b64 + pad); + return JSON.parse(json); + } catch (e) { return null; } + } + + var _focusTrapState = null; + function focusTrapStart(rootEl) { + focusTrapEnd(); + if (!rootEl) return; + function selectable(el) { + if (!el || el.disabled || el.getAttribute('aria-hidden') === 'true') return false; + var ti = el.getAttribute('tabindex'); + if (ti === '-1') return false; + return el.tabIndex >= 0 || el.matches('a[href],button:not([disabled]),textarea,input,select,[tabindex]:not([tabindex="-1"])'); + } + function listFocusable() { + return Array.prototype.slice.call(rootEl.querySelectorAll('a[href],button,textarea,input,select,[tabindex]')).filter(selectable); + } + var onKeydown = function(e) { + if (e.key !== 'Tab') return; + var nodes = listFocusable(); + if (!nodes.length) return; + var first = nodes[0]; + var last = nodes[nodes.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first.focus(); } + } + }; + rootEl.addEventListener('keydown', onKeydown); + _focusTrapState = { root: rootEl, onKeydown: onKeydown }; + var n = listFocusable(); + if (n.length) setTimeout(function() { n[0].focus(); }, 0); + } + function focusTrapEnd() { + if (!_focusTrapState || !_focusTrapState.root) { _focusTrapState = null; return; } + try { _focusTrapState.root.removeEventListener('keydown', _focusTrapState.onKeydown); } catch (e) {} + _focusTrapState = null; + } + + function updateNavAriaCurrentFromPath(path) { + var raw = path != null ? String(path) : (window.location.pathname || '/'); + var p = raw.split('?')[0].replace(/\/$/, '') || '/'; + var links = document.querySelectorAll('#navLinks a[href^="/"]'); + links.forEach(function(a) { + var h = (a.getAttribute('href') || '').split('?')[0].replace(/\/$/, '') || '/'; + if (h === p || (h !== '/' && (p === h || p.indexOf(h + '/') === 0))) { + a.setAttribute('aria-current', 'page'); + } else { + a.removeAttribute('aria-current'); + } + }); + } + let currentView = 'home'; let _poolsRouteTreeRefreshTimer = null; let currentDetailKey = ''; @@ -988,7 +1122,7 @@ _blocksScrollAnimationId = null; } currentView = viewName; - var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','addresses','pools','routes','liquidity','more','system']; + var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','addresses','pools','routes','liquidity','more','bridge','weth','system','institution','compare','analytics','operator']; if (detailViews.indexOf(viewName) === -1) currentDetailKey = ''; var homeEl = document.getElementById('homeView'); if (homeEl) homeEl.style.display = viewName === 'home' ? 'block' : 'none'; @@ -1067,7 +1201,65 @@ const operatorNav = document.getElementById('operatorNav'); if (analyticsNav) analyticsNav.style.display = hasAccess(3) ? 'block' : 'none'; if (operatorNav) operatorNav.style.display = hasAccess(4) ? 'block' : 'none'; + const userAccountNav = document.getElementById('userAccountNav'); + const verifyContractsRow = document.getElementById('userMenuVerifyContractsRow'); + const identityLabel = document.getElementById('userMenuIdentityLabel'); + if (userAccountNav) { + userAccountNav.style.display = authToken ? '' : 'none'; + } + if (verifyContractsRow) { + verifyContractsRow.style.display = authToken && hasAccess(4) ? '' : 'none'; + } + var instRow = document.getElementById('userMenuInstitutionRow'); + var cmpRow = document.getElementById('userMenuCompareRow'); + var kbRow = document.getElementById('userMenuKeyboardRow'); + var diagRow = document.getElementById('userMenuDiagnosticsRow'); + var incRow = document.getElementById('userMenuIncidentsRow'); + if (instRow) instRow.style.display = authToken && hasAccess(2) ? '' : 'none'; + if (cmpRow) cmpRow.style.display = authToken && hasAccess(2) ? '' : 'none'; + if (kbRow) kbRow.style.display = authToken ? '' : 'none'; + if (diagRow) diagRow.style.display = authToken && hasAccess(2) ? '' : 'none'; + if (incRow) incRow.style.display = authToken && hasAccess(4) ? '' : 'none'; + if (identityLabel && userAddress) { + identityLabel.textContent = 'Signed in: ' + shortenHash(userAddress); + } else if (identityLabel) { + identityLabel.textContent = 'Signed in'; + } } + + window.disconnectExplorerWallet = async function disconnectExplorerWallet() { + var tok = authToken; + if (tok) { + try { + await fetch(EXPLORER_API_V1_BASE + '/auth/logout', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + tok, 'Content-Type': 'application/json' } + }); + } catch (e) {} + } + try { + localStorage.removeItem('authToken'); + localStorage.removeItem('userAddress'); + } catch (e) {} + authToken = null; + userAddress = null; + userTrack = 1; + const walletBtn = document.getElementById('walletConnectBtn'); + const walletStatus = document.getElementById('walletStatus'); + const walletAddress = document.getElementById('walletAddress'); + if (walletBtn) { + walletBtn.style.display = 'block'; + walletBtn.disabled = false; + } + if (walletStatus) walletStatus.style.display = 'none'; + if (walletAddress) walletAddress.textContent = ''; + focusTrapEnd(); + updateUIForTrack(); + loadFeatureFlags(); + if (typeof showToast === 'function') { + showToast('Signed out', 'success'); + } + }; let connectingWallet = false; async function readApiErrorMessage(response, fallbackMessage) { @@ -2524,6 +2716,22 @@ } window._showAddresses = renderAddressesView; + window.loadInstitutionAnalyticsCohortSnapshot = async function() { + var el = document.getElementById('institutionCohortSnapshot'); + if (!el) return; + var out = { at: new Date().toISOString() }; + var strip = document.getElementById('missionControlHealthStrip'); + if (strip) out.missionControlStrip = strip.textContent; + try { + var r = await fetch(EXPLORER_TRACK1_BASE + '/bridge/status', { credentials: 'omit' }); + if (r.ok) out.bridgeStatus = await r.json(); + else out.bridgeHttp = r.status; + } catch (e) { + out.bridgeError = String(e.message || e); + } + el.textContent = JSON.stringify(out, null, 2); + }; + function buildAnalyticsViewHtml() { var html = ''; html += '
Cohort-style snapshot: combine live bridge status with the mission-control strip. Use for internal reporting until dedicated analytics endpoints are enabled.
'; + html += ''; + html += 'Click refresh after this page loads.
Institution console requires track 2+ wallet authentication.
'; + return; + } + var prefs = getInstitutionPrefs(); + var jwt = decodeExplorerJwtPayload(authToken); + var expText = '—'; + if (jwt && jwt.exp) { + expText = formatInstitutionTimestamp(jwt.exp * 1000); + if (jwt.jti) expText += ' · jti: ' + escapeHtml(String(jwt.jti).slice(0, 14)) + '…'; + } + var views = getSavedViews(); + var viewRows = views.length + ? views.map(function(v) { + return '' + escapeHtml(v.path) + '' + escapeHtml(p.value || '(empty)') + 'Track 3+ users: RPC API keys and usage audit live under the access console (email session), not the wallet JWT. See EXPLORER_API_ACCESS.md.
' + : 'RPC products and keys: GET ' + escapeHtml(EXPLORER_API_V1_BASE) + '/access/products (public) and GET …/access/api-keys with an email access token from POST …/auth/register / login. Wallet JWTs gate explorer tracks, not the RPC key store.
Wallet: ' + escapeHtml(userAddress || '') + ' · Track: ' + escapeHtml(String(userTrack)) + '
' + + 'JWT expiry (decoded): ' + expText + '
' + + 'Only the current browser session is shown. Revoking this token uses Sign out (server POST /auth/logout). Organization-wide “sign out everywhere” needs operator revocation of all JTIs for the address (not exposed here).
| Name | Path |
|---|
Apply filters on Blocks or Transactions pages first, then capture a preset here.
' + + ' ' + + '' + + '| Name | Value |
|---|
| Name | Value |
|---|
While this tab is open, the explorer compares cached transaction counts for watchlist addresses on your chosen interval.
' + + '' + + ' ' + + 'Download a JSON bundle for audit / ticketing. Email delivery is not wired in-browser; export and attach to your SOC workflow.
' + + ' ' + + '' + + '| Name | Frequency |
|---|
Track 2+ required.
'; + return; + } + var aa = a ? safeAddress(String(a)) : null; + var bb = b ? safeAddress(String(b)) : null; + var form = 'Enter two contract or EOA addresses to compare balances and activity counters side by side.
'; + return; + } + c.innerHTML = form + '| Metric | A | B |
|---|
' + escapeHtml(err.message || String(err)) + '
'; + }); + } + + window.closeKeyboardShortcutsModal = function() { + focusTrapEnd(); + var m = document.getElementById('keyboardShortcutsModal'); + if (m) { + m.style.display = 'none'; + m.setAttribute('aria-hidden', 'true'); + } + document.body.style.overflow = ''; + }; + window.showKeyboardShortcutsModal = function() { + var m = document.getElementById('keyboardShortcutsModal'); + if (!m) return; + if (typeof closeAllNavDropdowns === 'function') closeAllNavDropdowns(); + m.style.display = 'block'; + m.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + focusTrapStart(m); + }; + + window.closeIncidentLinksModal = function() { + focusTrapEnd(); + var m = document.getElementById('incidentLinksModal'); + if (m) { + m.style.display = 'none'; + m.setAttribute('aria-hidden', 'true'); + } + document.body.style.overflow = ''; + }; + window.showIncidentLinksModal = function() { + if (!authToken || !hasAccess(4)) { + if (typeof showToast === 'function') showToast('Incident hub is limited to operator-track wallets.', 'warning'); + return; + } + var m = document.getElementById('incidentLinksModal'); + if (!m) return; + if (typeof closeAllNavDropdowns === 'function') closeAllNavDropdowns(); + m.style.display = 'block'; + m.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + focusTrapStart(m); + }; + + window.copyExplorerDiagnosticsBundle = async function() { + if (!authToken) { + if (typeof showToast === 'function') showToast('Sign in to copy diagnostics.', 'warning'); + return; + } + var pl = decodeExplorerJwtPayload(authToken); + var bundle = { + exportedAt: new Date().toISOString(), + pathname: window.location.pathname, + userTrack: userTrack, + wallet: userAddress, + jwtExp: pl && pl.exp, + jwtJtiPrefix: pl && pl.jti ? String(pl.jti).slice(0, 12) : null, + chainId: typeof CHAIN_ID !== 'undefined' ? CHAIN_ID : null, + userAgent: navigator.userAgent, + explorerApi: EXPLORER_API_V1_BASE + }; + var strip = document.getElementById('missionControlHealthStrip'); + if (strip) bundle.missionControlStrip = strip.textContent; + var txt = JSON.stringify(bundle, null, 2); + try { + await navigator.clipboard.writeText(txt); + if (typeof showToast === 'function') showToast('Diagnostics bundle copied', 'success'); + } catch (e) { + window.prompt('Copy diagnostics:', txt); + } + }; + + window.downloadInstitutionReportSnapshot = async function() { + if (!hasAccess(3)) { + if (typeof showToast === 'function') showToast('Track 3+ required for report snapshot.', 'warning'); + return; + } + var snap = { + generatedAt: new Date().toISOString(), + userTrack: userTrack, + pathname: window.location.pathname, + schedules: getReportSchedules() + }; + var strip = document.getElementById('missionControlHealthStrip'); + if (strip) snap.missionControlStrip = strip.textContent; + var g = document.getElementById('gasCurrentValue'); + if (g) snap.gasHomeLabel = g.textContent; + try { + var r = await fetch(EXPLORER_TRACK1_BASE + '/bridge/status', { credentials: 'omit' }); + if (r.ok) snap.bridgeStatus = await r.json(); + } catch (e) {} + var blob = new Blob([JSON.stringify(snap, null, 2)], { type: 'application/json' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'explorer-institution-snapshot-' + snap.generatedAt.slice(0, 19).replace(/[:]/g, '-') + '.json'; + a.click(); + URL.revokeObjectURL(url); + var list = getReportSchedules(); + for (var i = 0; i < list.length; i++) { + list[i].lastDownloadAt = snap.generatedAt; + } + setReportSchedules(list); + if (typeof showToast === 'function') showToast('Snapshot downloaded', 'success'); + renderInstitutionView(); + }; + + var _watchlistNotifTimer = null; + function restartWatchlistInstitutionNotifications() { + if (_watchlistNotifTimer) { + clearInterval(_watchlistNotifTimer); + _watchlistNotifTimer = null; + } + _watchlistNotifTimer = setInterval(tickWatchlistInstitutionNotifications, 60000); + } + window.restartWatchlistInstitutionNotifications = restartWatchlistInstitutionNotifications; + + async function tickWatchlistInstitutionNotifications() { + var s = getNotifSettings(); + if (!authToken || !s.enabled) return; + var wl = getWatchlist(); + if (!wl.length) return; + var last = 0; + try { last = parseInt(sessionStorage.getItem('explorer_notif_last_run') || '0', 10); } catch (e) {} + var now = Date.now(); + if (now - last < s.intervalMin * 60000) return; + try { sessionStorage.setItem('explorer_notif_last_run', String(now)); } catch (e) {} + var snap = getNotifSnap(); + var slice = wl.slice(0, 8); + for (var i = 0; i < slice.length; i++) { + var addr = slice[i]; + try { + var res = await fetchChain138AddressDetail(addr); + var ad = res && res.address; + var tc = ad && ad.transaction_count != null ? ad.transaction_count : 0; + var prev = snap[addr.toLowerCase()]; + if (prev != null && tc > prev && typeof showToast === 'function') { + showToast('Watchlist activity: ' + shortenHash(addr) + ' tx count ' + prev + ' → ' + tc, 'info', 8000); + } + snap[addr.toLowerCase()] = tc; + } catch (e) {} + } + setNotifSnap(snap); + } + function applyHashRoute() { + try { var route = ''; var fromPath = (window.location.pathname || '/').replace(/^\//, '').replace(/\/$/, '').replace(/^index\.html$/i, ''); var fromHash = (window.location.hash || '').replace(/^#/, ''); @@ -2840,9 +3471,28 @@ if (parts[0] === 'liquidity') { if (currentView !== 'liquidity') showLiquidityAccess(); return; } if (parts[0] === 'more') { if (currentView !== 'more') showMore(); return; } if (parts[0] === 'tokens') { if (typeof showTokensList === 'function') showTokensList(); else focusSearchWithHint('token'); return; } + if (parts[0] === 'institution') { + if (!authToken || !hasAccess(2)) { if (currentView !== 'home') showHome(); return; } + if (currentView !== 'institution') switchToView('institution'); + renderInstitutionView(); + return; + } + if (parts[0] === 'compare') { + if (!authToken || !hasAccess(2)) { if (currentView !== 'home') showHome(); return; } + var c1 = parts[1] ? decode(parts[1]) : ''; + var c2 = parts[2] ? decode(parts[2]) : ''; + if (currentView !== 'compare') switchToView('compare'); + renderCompareView(c1, c2); + return; + } if (parts[0] === 'analytics') { if (currentView !== 'analytics') showAnalytics(); return; } if (parts[0] === 'operator') { if (currentView !== 'operator') showOperator(); return; } if (parts[0] === 'system') { if (currentView !== 'system') showSystemTopology(); return; } + } finally { + if (typeof updateNavAriaCurrentFromPath === 'function') { + updateNavAriaCurrentFromPath(window.location.pathname); + } + } } window.applyHashRoute = applyHashRoute; var hasRouteOnReady = window.location.hash || ((window.location.pathname || '').replace(/^\//, '').replace(/\/$/, '')); @@ -4940,6 +5590,161 @@ if (/^0x0{40}$/i.test(s)) return null; return s; } + + function guessContractAddressFromPath() { + var path = (typeof window !== 'undefined' && window.location && window.location.pathname) ? window.location.pathname : ''; + var m = path.match(/^\/addresses\/(0x[a-fA-F0-9]{40})\/?/i); + if (m) return m[1]; + m = path.match(/^\/address\/(0x[a-fA-F0-9]{40})\/?/i); + return m ? m[1] : ''; + } + + function institutionExternalLinksBlockscout() { + return (getInstitutionPrefs().externalLinks || 'spa') === 'blockscout'; + } + function clearExplorerPendingAddressInitialTabFor(addr) { + var p = window.__explorerPendingAddressInitialTab; + var low = addr && String(addr).toLowerCase(); + if (p && low && p.address === low) { + window.__explorerPendingAddressInitialTab = null; + } + } + function institutionOpenContractInExplorer(addr) { + var a = safeAddress(addr); + if (!a) return; + try { + window.__explorerPendingAddressInitialTab = { tab: 'contract', address: a.toLowerCase() }; + if (typeof showAddressDetail === 'function') showAddressDetail(a); + if (typeof updatePath === 'function') updatePath('/addresses/' + a + '/contract'); + } catch (e) {} + } + window.institutionOpenContractInExplorer = institutionOpenContractInExplorer; + + function institutionContractExploreAnchor(addr, extLabel, spaLabel, inlineStyle) { + var a = safeAddress(addr); + if (!a) return ''; + var sty = inlineStyle ? ' style="' + escapeAttr(inlineStyle) + '"' : ''; + if (institutionExternalLinksBlockscout()) { + return '' + escapeHtml(extLabel) + ''; + } + var addressForJs = a.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + return '' + escapeHtml(spaLabel) + ''; + } + function institutionAddressContractLinkHtml(addr) { + return institutionContractExploreAnchor(addr, 'View contract on Blockscout', 'Open contract tab', 'color: var(--primary); font-size: 0.875rem;'); + } + + function syncContractVerifyBlockscoutLink() { + var input = document.getElementById('contractVerifyAddressInput'); + var link = document.getElementById('contractVerifyBlockscoutLink'); + if (!link) return; + var raw = input && input.value ? String(input.value).trim() : ''; + var addr = safeAddress(raw) || safeAddress(guessContractAddressFromPath()); + link.onclick = null; + if (addr) { + if (institutionExternalLinksBlockscout()) { + link.href = EXPLORER_ORIGIN + '/address/' + encodeURIComponent(addr) + '/contract'; + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer'); + } else { + link.href = '#'; + link.removeAttribute('target'); + link.removeAttribute('rel'); + (function (contractAddr) { + link.onclick = function (ev) { + ev.preventDefault(); + institutionOpenContractInExplorer(contractAddr); + if (typeof closeContractVerifyHelpModal === 'function') closeContractVerifyHelpModal(); + return false; + }; + })(addr); + } + link.setAttribute('aria-disabled', 'false'); + link.style.pointerEvents = ''; + link.style.opacity = ''; + } else { + link.href = '#'; + link.removeAttribute('target'); + link.removeAttribute('rel'); + link.onclick = function (ev) { ev.preventDefault(); return false; }; + link.setAttribute('aria-disabled', 'true'); + link.style.pointerEvents = 'none'; + link.style.opacity = '0.55'; + } + } + + function closeContractVerifyHelpModal() { + if (typeof focusTrapEnd === 'function') focusTrapEnd(); + var modal = document.getElementById('contractVerifyHelpModal'); + if (!modal) return; + modal.style.display = 'none'; + modal.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + } + + window.showContractVerificationHelp = function showContractVerificationHelp(prefillAddress) { + if (!authToken || !hasAccess(4)) { + if (typeof showToast === 'function') { + showToast('Contract verification tools are available to operator-approved accounts.', 'warning'); + } else { + alert('Contract verification tools are available to operator-approved accounts.'); + } + return; + } + var modal = document.getElementById('contractVerifyHelpModal'); + if (!modal) return; + if (typeof closeAllNavDropdowns === 'function') { + closeAllNavDropdowns(); + } + var input = document.getElementById('contractVerifyAddressInput'); + var fromArg = prefillAddress && safeAddress(prefillAddress); + var fromPath = guessContractAddressFromPath(); + var initial = fromArg || fromPath || (input && input.value ? safeAddress(input.value) : null); + if (input) { + input.value = initial || ''; + } + syncContractVerifyBlockscoutLink(); + modal.style.display = 'block'; + modal.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + if (typeof focusTrapStart === 'function') focusTrapStart(modal); + }; + + function initContractVerifyHelpModal() { + var modal = document.getElementById('contractVerifyHelpModal'); + var backdrop = document.getElementById('contractVerifyHelpBackdrop'); + var closeBtn = document.getElementById('contractVerifyHelpCloseBtn'); + var input = document.getElementById('contractVerifyAddressInput'); + var copyBtn = document.getElementById('contractVerifyCopyCommandBtn'); + var cmdEl = document.getElementById('contractVerifyScriptCommand'); + if (backdrop) { + backdrop.addEventListener('click', closeContractVerifyHelpModal); + } + if (closeBtn) { + closeBtn.addEventListener('click', function (e) { + e.preventDefault(); + closeContractVerifyHelpModal(); + }); + } + if (input) { + input.addEventListener('input', syncContractVerifyBlockscoutLink); + } + if (copyBtn && cmdEl) { + copyBtn.addEventListener('click', function (e) { + e.preventDefault(); + var text = cmdEl.textContent || './scripts/verify/run-contract-verification-with-proxy.sh'; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { + if (typeof showToast === 'function') showToast('Command copied', 'success'); + }).catch(function () { + window.prompt('Copy:', text); + }); + } else { + window.prompt('Copy:', text); + } + }); + } + } function escapeJsSingleQuoted(value) { return String(value == null ? '' : value).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } @@ -5942,6 +6747,7 @@ try { // Validate address format if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { + clearExplorerPendingAddressInitialTabFor(address); container.innerHTML = 'Contract source not indexed. Verify on Blockscout
'; + el.innerHTML = 'Contract source not indexed. ' + institutionContractExploreAnchor(addr, 'Verify on Blockscout', 'Open contract tab', 'color: var(--primary);') + '
'; return; } const abi = data.abi || data.abi_interface || []; @@ -6271,7 +7089,7 @@ html += ''; html += 'Read / Write contract on Blockscout
'; + html += '' + institutionContractExploreAnchor(addr, 'Read / Write contract on Blockscout', 'Read / Write contract in explorer', 'color: var(--primary);') + '
'; el.innerHTML = html; if (viewFns.length > 0) { (function setupReadContract(contractAddr, abiJson, viewFunctions) { @@ -6434,9 +7252,11 @@ if (txContainer) txContainer.innerHTML = 'Failed to load transactions
'; } } else { + clearExplorerPendingAddressInitialTabFor(address); container.innerHTML = '