diff --git a/backend/api/rest/addresses_list.go b/backend/api/rest/addresses_list.go new file mode 100644 index 0000000..03032eb --- /dev/null +++ b/backend/api/rest/addresses_list.go @@ -0,0 +1,169 @@ +package rest + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "time" +) + +type addressListRow struct { + Address string `json:"address"` + TxSent int64 `json:"tx_sent"` + TxReceived int64 `json:"tx_received"` + TransactionCnt int64 `json:"transaction_count"` + TokenCount int64 `json:"token_count"` + IsContract bool `json:"is_contract"` + Label string `json:"label,omitempty"` + LastSeenAt string `json:"last_seen_at,omitempty"` + FirstSeenAt string `json:"first_seen_at,omitempty"` +} + +// handleListAddresses handles GET /api/v1/addresses +func (s *Server) handleListAddresses(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + if !s.requireDB(w) { + return + } + + page, pageSize, err := validatePagination( + r.URL.Query().Get("page"), + r.URL.Query().Get("page_size"), + ) + if err != nil { + writeValidationError(w, err) + return + } + + offset := (page - 1) * pageSize + search := strings.TrimSpace(r.URL.Query().Get("q")) + + whereClause := "" + args := []interface{}{s.chainID} + if search != "" { + whereClause = "WHERE LOWER(a.address) LIKE LOWER($2) OR LOWER(COALESCE(l.label, '')) LIKE LOWER($2)" + args = append(args, "%"+search+"%") + } + + query := fmt.Sprintf(` + WITH activity AS ( + SELECT + address, + COUNT(*) FILTER (WHERE direction = 'sent') AS tx_sent, + COUNT(*) FILTER (WHERE direction = 'received') AS tx_received, + COUNT(*) AS transaction_count, + MIN(seen_at) AS first_seen_at, + MAX(seen_at) AS last_seen_at + FROM ( + SELECT + t.from_address AS address, + 'sent' AS direction, + b.timestamp AS seen_at + FROM transactions t + JOIN blocks b ON b.chain_id = t.chain_id AND b.number = t.block_number + WHERE t.chain_id = $1 AND t.from_address IS NOT NULL AND t.from_address <> '' + UNION ALL + SELECT + t.to_address AS address, + 'received' AS direction, + b.timestamp AS seen_at + FROM transactions t + JOIN blocks b ON b.chain_id = t.chain_id AND b.number = t.block_number + WHERE t.chain_id = $1 AND t.to_address IS NOT NULL AND t.to_address <> '' + ) entries + GROUP BY address + ), + token_activity AS ( + SELECT address, COUNT(DISTINCT token_address) AS token_count + FROM ( + SELECT from_address AS address, token_address + FROM token_transfers + WHERE chain_id = $1 AND from_address IS NOT NULL AND from_address <> '' + UNION ALL + SELECT to_address AS address, token_address + FROM token_transfers + WHERE chain_id = $1 AND to_address IS NOT NULL AND to_address <> '' + ) tokens + GROUP BY address + ), + label_activity AS ( + SELECT DISTINCT ON (address) + address, + label + FROM address_labels + WHERE chain_id = $1 AND label_type = 'public' + ORDER BY address, updated_at DESC, id DESC + ), + contract_activity AS ( + SELECT address, TRUE AS is_contract + FROM contracts + WHERE chain_id = $1 + ) + SELECT + a.address, + a.tx_sent, + a.tx_received, + a.transaction_count, + COALESCE(t.token_count, 0) AS token_count, + COALESCE(c.is_contract, FALSE) AS is_contract, + COALESCE(l.label, '') AS label, + COALESCE(a.last_seen_at::text, '') AS last_seen_at, + COALESCE(a.first_seen_at::text, '') AS first_seen_at + FROM activity a + LEFT JOIN token_activity t ON t.address = a.address + LEFT JOIN label_activity l ON l.address = a.address + LEFT JOIN contract_activity c ON c.address = a.address + %s + ORDER BY a.transaction_count DESC, a.last_seen_at DESC NULLS LAST, a.address ASC + LIMIT $%d OFFSET $%d + `, whereClause, len(args)+1, len(args)+2) + + args = append(args, pageSize, offset) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + rows, err := s.db.Query(ctx, query, args...) + if err != nil { + writeInternalError(w, "Database error") + return + } + defer rows.Close() + + items := []addressListRow{} + for rows.Next() { + var row addressListRow + if err := rows.Scan( + &row.Address, + &row.TxSent, + &row.TxReceived, + &row.TransactionCnt, + &row.TokenCount, + &row.IsContract, + &row.Label, + &row.LastSeenAt, + &row.FirstSeenAt, + ); err != nil { + continue + } + items = append(items, row) + } + + response := map[string]interface{}{ + "data": items, + "meta": map[string]interface{}{ + "pagination": map[string]interface{}{ + "page": page, + "page_size": pageSize, + }, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} diff --git a/backend/api/rest/routes.go b/backend/api/rest/routes.go index fa33a58..7334aab 100644 --- a/backend/api/rest/routes.go +++ b/backend/api/rest/routes.go @@ -17,6 +17,7 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) { mux.HandleFunc("/api/v1/transactions/", s.handleTransactionDetail) // Address routes + mux.HandleFunc("/api/v1/addresses", s.handleListAddresses) mux.HandleFunc("/api/v1/addresses/", s.handleAddressDetail) // Search route @@ -38,6 +39,10 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) { // Feature flags endpoint mux.HandleFunc("/api/v1/features", s.handleFeatures) + // Route decision tree proxy + mux.HandleFunc("/api/v1/routes/tree", s.handleRouteDecisionTree) + mux.HandleFunc("/api/v1/routes/depth", s.handleRouteDepth) + // Auth endpoints mux.HandleFunc("/api/v1/auth/nonce", s.handleAuthNonce) mux.HandleFunc("/api/v1/auth/wallet", s.handleAuthWallet) diff --git a/backend/api/rest/routes_proxy.go b/backend/api/rest/routes_proxy.go new file mode 100644 index 0000000..d63f677 --- /dev/null +++ b/backend/api/rest/routes_proxy.go @@ -0,0 +1,57 @@ +package rest + +import ( + "fmt" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strings" +) + +func (s *Server) handleRouteDecisionTree(w http.ResponseWriter, r *http.Request) { + s.proxyRouteTreeEndpoint(w, r, "/api/v1/routes/tree") +} + +func (s *Server) handleRouteDepth(w http.ResponseWriter, r *http.Request) { + s.proxyRouteTreeEndpoint(w, r, "/api/v1/routes/depth") +} + +func (s *Server) proxyRouteTreeEndpoint(w http.ResponseWriter, r *http.Request, path string) { + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + baseURL := strings.TrimSpace(firstNonEmptyEnv( + "TOKEN_AGGREGATION_API_BASE", + "TOKEN_AGGREGATION_URL", + "TOKEN_AGGREGATION_BASE_URL", + )) + if baseURL == "" { + writeError(w, http.StatusServiceUnavailable, "service_unavailable", "token aggregation api base url is not configured") + return + } + + target, err := url.Parse(strings.TrimRight(baseURL, "/")) + if err != nil { + writeError(w, http.StatusBadGateway, "bad_gateway", fmt.Sprintf("invalid token aggregation api base url: %v", err)) + return + } + + proxy := httputil.NewSingleHostReverseProxy(target) + proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, proxyErr error) { + writeError(rw, http.StatusBadGateway, "bad_gateway", fmt.Sprintf("route tree proxy failed for %s: %v", path, proxyErr)) + } + + proxy.ServeHTTP(w, r) +} + +func firstNonEmptyEnv(keys ...string) string { + for _, key := range keys { + if value := strings.TrimSpace(os.Getenv(key)); value != "" { + return value + } + } + return "" +} diff --git a/frontend/public/acknowledgments.html b/frontend/public/acknowledgments.html new file mode 100644 index 0000000..2e339d9 --- /dev/null +++ b/frontend/public/acknowledgments.html @@ -0,0 +1,40 @@ + + + + + + Acknowledgments | SolaceScanScout + + + + +
+
+
SolaceScanScout Acknowledgments
+ Back to explorer +
+
+

Acknowledgments

+

This explorer and its companion tools are built with help from the open-source and infrastructure tools below.

+ +

If we have missed a contributor or dependency, please let us know at support@d-bis.org.

+
+
+ + diff --git a/frontend/public/docs.html b/frontend/public/docs.html new file mode 100644 index 0000000..7ce78a3 --- /dev/null +++ b/frontend/public/docs.html @@ -0,0 +1,54 @@ + + + + + + Documentation | SolaceScanScout + + + + +
+
+
SolaceScanScout Documentation
+ Back to explorer +
+
+

Documentation

+

This landing page collects the key explorer and deployment references used by the SolaceScanScout stack.

+
+ Privacy Policy + Terms of Service + Acknowledgments + + Liquidity access +
Public Chain 138 pool snapshot, live Mainnet stable bridge paths, route matrix links, partner payload templates, and the internal fallback execution plan endpoint.
+
+ + + +
+
+
+ + diff --git a/frontend/public/explorer-spa.js b/frontend/public/explorer-spa.js index 66f2f73..220f48d 100644 --- a/frontend/public/explorer-spa.js +++ b/frontend/public/explorer-spa.js @@ -1,4 +1,5 @@ const API_BASE = '/api'; + const TOKEN_AGGREGATION_API_BASE = '/token-aggregation/api'; const FETCH_TIMEOUT_MS = 15000; const RPC_HEALTH_TIMEOUT_MS = 5000; const FETCH_MAX_RETRIES = 3; @@ -64,14 +65,14 @@ 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'; var I18N = { - en: { home: 'Home', blocks: 'Blocks', transactions: 'Transactions', 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', 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' }, - fr: { home: 'Accueil', blocks: 'Blocs', transactions: 'Transactions', bridge: 'Pont', weth: 'WETH', tokens: 'Jetons', pools: 'Pools', more: 'Plus', analytics: 'Analyses', operator: 'Opérateur', watchlist: 'Liste de suivi', searchPlaceholder: 'Adresse, hash de tx, numéro de bloc ou nom de token/contrat…', connectWallet: 'Connecter le portefeuille', darkMode: 'Mode sombre', lightMode: 'Mode clair', back: 'Retour', exportCsv: 'Exporter CSV', tokenBalances: 'Soldes de jetons', internalTxns: 'Transactions internes', readContract: 'Lire le contrat', writeContract: 'Écrire le contrat', addToWatchlist: 'Ajouter à la liste', removeFromWatchlist: 'Retirer de la liste', checkApprovals: 'Vérifier les approbations', copied: 'Copié' } + 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' }, + fr: { home: 'Accueil', blocks: 'Blocs', transactions: 'Transactions', addresses: 'Adresses', bridge: 'Pont', weth: 'WETH', tokens: 'Jetons', pools: 'Pools', more: 'Plus', analytics: 'Analyses', operator: 'Opérateur', watchlist: 'Liste de suivi', searchPlaceholder: 'Adresse, hash de tx, numéro de bloc ou nom de token/contrat…', connectWallet: 'Connecter le portefeuille', darkMode: 'Mode sombre', lightMode: 'Mode clair', back: 'Retour', exportCsv: 'Exporter CSV', tokenBalances: 'Soldes de jetons', internalTxns: 'Transactions internes', readContract: 'Lire le contrat', writeContract: 'Écrire le contrat', addToWatchlist: 'Ajouter à la liste', removeFromWatchlist: 'Retirer de la liste', checkApprovals: 'Vérifier les approbations', copied: 'Copié' } }; var currentLocale = (function(){ try { return localStorage.getItem('explorerLocale') || 'en'; } catch(e){ return 'en'; } })(); function t(key) { return (I18N[currentLocale] && I18N[currentLocale][key]) || I18N.en[key] || key; } function setLocale(loc) { currentLocale = loc; try { localStorage.setItem('explorerLocale', loc); } catch(e){} if (typeof applyI18n === 'function') applyI18n(); } - function applyI18n() { document.querySelectorAll('[data-i18n]').forEach(function(el){ var k = el.getAttribute('data-i18n'); if (k) el.textContent = t(k); }); var searchIn = document.getElementById('searchInput'); if (searchIn) searchIn.placeholder = t('searchPlaceholder'); var localeSel = document.getElementById('localeSelect'); if (localeSel) localeSel.value = currentLocale; var wcBtn = document.getElementById('walletConnectBtn'); if (wcBtn) wcBtn.textContent = t('connectWallet'); } + function applyI18n() { document.querySelectorAll('[data-i18n]').forEach(function(el){ var k = el.getAttribute('data-i18n'); if (k) el.textContent = t(k); }); var searchIn = document.getElementById('smartSearchInput'); if (searchIn) searchIn.placeholder = t('searchPlaceholder'); var localeSel = document.getElementById('localeSelect'); if (localeSel) localeSel.value = currentLocale; var wcBtn = document.getElementById('walletConnectBtn'); if (wcBtn) wcBtn.textContent = t('connectWallet'); } var _explorerPageFilters = {}; function normalizeExplorerFilter(value) { return String(value == null ? '' : value).trim().toLowerCase(); } function getExplorerPageFilter(key) { return _explorerPageFilters[key] || ''; } @@ -79,6 +80,253 @@ function clearExplorerPageFilter(key) { delete _explorerPageFilters[key]; return ''; } function matchesExplorerFilter(haystack, filter) { if (!filter) return true; return String(haystack == null ? '' : haystack).toLowerCase().indexOf(filter) !== -1; } function escapeAttr(value) { return escapeHtml(String(value == null ? '' : value)).replace(/"/g, '"'); } + const SMART_SEARCH_HISTORY_KEY = 'explorerSmartSearchHistory'; + const SMART_SEARCH_HISTORY_LIMIT = 8; + let _smartSearchScope = 'all'; + let _smartSearchPreviewTimer = null; + let _smartSearchPreviewRequestId = 0; + let _smartSearchTrendingCache = null; + function getSmartSearchHistory() { + try { + var raw = localStorage.getItem(SMART_SEARCH_HISTORY_KEY); + var parsed = raw ? JSON.parse(raw) : []; + return Array.isArray(parsed) ? parsed.filter(Boolean) : []; + } catch (e) { + return []; + } + } + function saveSmartSearchHistory(query) { + var value = String(query || '').trim(); + if (!value) return; + try { + var history = getSmartSearchHistory().filter(function(item) { + return String(item).toLowerCase() !== value.toLowerCase(); + }); + history.unshift(value); + history = history.slice(0, SMART_SEARCH_HISTORY_LIMIT); + localStorage.setItem(SMART_SEARCH_HISTORY_KEY, JSON.stringify(history)); + } catch (e) {} + } + function detectSmartSearchType(query) { + var value = String(query || '').trim(); + var normalized = value.replace(/\s/g, ''); + if (!value) return { type: 'recent', label: 'Recent searches', detail: 'Start typing to narrow the explorer.' }; + if (/^0x[a-fA-F0-9]{64}$/.test(normalized)) return { type: 'transaction', label: 'Transaction hash', detail: 'Enter will open the transaction detail page.' }; + if (/^0x[a-fA-F0-9]{40}$/.test(normalized)) return { type: 'address', label: 'Address', detail: 'Enter will open the address detail page.' }; + if (/^\d+$/.test(value)) return { type: 'block', label: 'Block number', detail: 'Enter will open the block detail page.' }; + if (/\.eth$/i.test(value)) return { type: 'ens', label: 'ENS/domain', detail: 'The explorer will search or resolve this name.' }; + if (/^[a-z0-9][a-z0-9._:-]{1,31}$/i.test(value)) return { type: 'token', label: 'Token / asset symbol', detail: 'The explorer will search token and contract matches.' }; + return { type: 'search', label: 'Explorer search', detail: 'The explorer will search across indexed results.' }; + } + function normalizeSmartSearchItemType(item) { + var type = String((item && (item.type || item.address_type || item.entity_type || item.kind)) || '').toLowerCase(); + if (item && (item.tx_hash || (item.hash && String(item.hash).length === 66))) return 'transactions'; + if (item && (item.block_number != null)) return 'blocks'; + if (item && (item.token_address || item.token_contract_address_hash)) return 'tokens'; + if (type.indexOf('tx') !== -1 || type.indexOf('transaction') !== -1) return 'transactions'; + if (type.indexOf('block') !== -1) return 'blocks'; + if (type.indexOf('token') !== -1 || type.indexOf('contract') !== -1) return 'tokens'; + if (type.indexOf('address') !== -1) return 'addresses'; + return 'all'; + } + function setSmartSearchScope(scope) { + _smartSearchScope = scope || 'all'; + try { + document.querySelectorAll('.smart-search-scope-btn').forEach(function(btn) { + var active = btn.getAttribute('data-scope') === _smartSearchScope; + btn.classList.toggle('btn-primary', active); + btn.classList.toggle('btn-secondary', !active); + }); + } catch (e) {} + var input = document.getElementById('smartSearchInput'); + updateSmartSearchPreview(input ? input.value : ''); + } + window.setSmartSearchScope = setSmartSearchScope; + function renderSmartSearchHistory() { + var history = getSmartSearchHistory(); + if (!history.length) { + return '
No recent searches yet.
'; + } + var html = '
'; + history.forEach(function(item) { + var safeItem = String(item).replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + html += ''; + }); + html += '
'; + return html; + } + function renderSmartSearchResultCard(item, action) { + var type = (item.type || item.address_type || '').toLowerCase(); + var title = item.name || item.symbol || item.address_hash || item.hash || item.tx_hash || (item.block_number != null ? 'Block #' + item.block_number : '') || 'Result'; + var subtitle = item.symbol && item.name ? item.symbol + ' • ' + item.name : item.symbol || item.name || item.address_hash || item.hash || item.tx_hash || ''; + var meta = []; + if (item.address_hash || item.hash || item.token_address || item.token_contract_address_hash) { + meta.push(shortenHash(item.address_hash || item.hash || item.token_address || item.token_contract_address_hash)); + } + if (item.block_number != null) meta.push('Block #' + item.block_number); + if (item.transaction_count != null) meta.push(String(item.transaction_count) + ' tx'); + var html = ''; + return html; + } + function renderSmartSearchPreviewItems(items, query) { + if (!items || !items.length) { + return '
No live suggestions yet. Press Enter to search everything, or try a more specific address, hash, block, or token symbol.
'; + } + var html = '
'; + items.slice(0, 6).forEach(function(item) { + if (_smartSearchScope !== 'all' && normalizeSmartSearchItemType(item) !== _smartSearchScope) return; + var action = null; + if (item.token_address || item.token_contract_address_hash) { + var tokenAddr = item.token_address || item.token_contract_address_hash; + if (/^0x[a-f0-9]{40}$/i.test(tokenAddr)) action = 'showTokenDetail(\'' + escapeHtml(tokenAddr) + '\')'; + } + if (!action && (item.address_hash || item.hash) && /^0x[a-f0-9]{40}$/i.test(item.address_hash || item.hash)) { + var addr = item.address_hash || item.hash; + action = 'showAddressDetail(\'' + escapeHtml(addr) + '\')'; + } + if (!action && (item.tx_hash || (item.hash && item.hash.length === 66)) && /^0x[a-f0-9]{64}$/i.test(item.tx_hash || item.hash)) { + var txHash = item.tx_hash || item.hash; + action = 'showTransactionDetail(\'' + escapeHtml(txHash) + '\')'; + } + if (!action && item.block_number != null) { + action = 'showBlockDetail(\'' + escapeHtml(String(item.block_number)) + '\')'; + } + html += renderSmartSearchResultCard(item, action); + }); + html += '
'; + return html; + } + function renderSmartSearchTrendingTokens(tokens) { + if (!tokens || !tokens.length) { + return '
No trending tokens found yet.
'; + } + var html = '
'; + tokens.slice(0, 6).forEach(function(token) { + var tokenAddr = (token.address && (token.address.hash || token.address)) || token.address_hash || token.token_address || token.contract_address_hash || ''; + if (!tokenAddr) return; + var item = { + type: 'token', + name: token.name || token.symbol || 'Token', + symbol: token.symbol || '', + token_address: tokenAddr + }; + html += renderSmartSearchResultCard(item, 'showTokenDetail(\'' + String(tokenAddr).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + '\')'); + }); + html += '
'; + return html; + } + async function fetchSmartSearchTrendingTokens() { + if (Array.isArray(_smartSearchTrendingCache)) return _smartSearchTrendingCache; + try { + var resp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/tokens?page=1&page_size=6'); + var items = (resp && (resp.items || resp.data)) || []; + _smartSearchTrendingCache = Array.isArray(items) ? items : []; + } catch (e) { + _smartSearchTrendingCache = []; + } + return _smartSearchTrendingCache; + } + async function updateSmartSearchPreview(query) { + var input = document.getElementById('smartSearchInput'); + var preview = document.getElementById('smartSearchPreview'); + var detected = document.getElementById('smartSearchDetected'); + if (!preview || !detected) return; + + var info = detectSmartSearchType(query); + if (!query) { + detected.style.display = 'inline-block'; + detected.textContent = _smartSearchScope === 'all' ? 'All' : (_smartSearchScope.charAt(0).toUpperCase() + _smartSearchScope.slice(1)); + if (input) input.setAttribute('aria-describedby', 'smartSearchDetected'); + var emptyRequestId = ++_smartSearchPreviewRequestId; + preview.innerHTML = '
Recent searches
' + renderSmartSearchHistory() + '
Trending tokens
Loading trending token watchlist...
Scope
Current filter: ' + escapeHtml(_smartSearchScope) + '. Use the left rail to narrow the search feed.
Quick tips
Try an address, transaction hash, block number, token symbol, or contract name. Press Enter to search and Esc to close.
'; + fetchSmartSearchTrendingTokens().then(function(tokens) { + if (emptyRequestId !== _smartSearchPreviewRequestId) return; + preview.innerHTML = '
Recent searches
' + renderSmartSearchHistory() + '
Trending tokens
' + renderSmartSearchTrendingTokens(tokens) + '
Scope
Current filter: ' + escapeHtml(_smartSearchScope) + '. Use the left rail to narrow the search feed.
Quick tips
Try an address, transaction hash, block number, token symbol, or contract name. Press Enter to search and Esc to close.
'; + }); + return; + } + + detected.style.display = 'inline-block'; + detected.textContent = _smartSearchScope === 'all' ? info.label : (_smartSearchScope.charAt(0).toUpperCase() + _smartSearchScope.slice(1)); + preview.innerHTML = '
Searching live suggestions...
'; + + var requestId = ++_smartSearchPreviewRequestId; + if (_smartSearchPreviewTimer) { + clearTimeout(_smartSearchPreviewTimer); + _smartSearchPreviewTimer = null; + } + _smartSearchPreviewTimer = setTimeout(async function() { + try { + var liveItems = []; + if (CHAIN_ID === 138) { + var resp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/search?q=' + encodeURIComponent(query)); + if (requestId !== _smartSearchPreviewRequestId) return; + liveItems = (resp && resp.items) ? resp.items : []; + } + if (requestId !== _smartSearchPreviewRequestId) return; + if (_smartSearchScope !== 'all') { + liveItems = liveItems.filter(function(item) { + return normalizeSmartSearchItemType(item) === _smartSearchScope; + }); + } + preview.innerHTML = '
Detected: ' + escapeHtml(info.label) + '
' + escapeHtml(info.detail) + '
Live suggestions
' + renderSmartSearchPreviewItems(liveItems, query) + '
'; + } catch (error) { + if (requestId !== _smartSearchPreviewRequestId) return; + if (_smartSearchScope !== 'all') { + preview.innerHTML = '
Detected: ' + escapeHtml(info.label) + '
' + escapeHtml(info.detail) + '
Live suggestions unavailable for the selected scope. Press Enter to search directly.
'; + return; + } + preview.innerHTML = '
Detected: ' + escapeHtml(info.label) + '
' + escapeHtml(info.detail) + '
Live suggestions unavailable. Press Enter to search directly.
'; + } + }, 220); + } + function openSmartSearchModal(prefill) { + var modal = document.getElementById('smartSearchModal'); + var input = document.getElementById('smartSearchInput'); + if (!modal || !input) return; + modal.style.display = 'block'; + modal.setAttribute('aria-hidden', 'false'); + document.body.style.overflow = 'hidden'; + setSmartSearchScope(_smartSearchScope || 'all'); + if (prefill != null) { + input.value = prefill; + } + updateSmartSearchPreview(input.value || ''); + setTimeout(function() { + input.focus(); + input.select(); + }, 0); + } + function closeSmartSearchModal() { + var modal = document.getElementById('smartSearchModal'); + if (!modal) return; + modal.style.display = 'none'; + modal.setAttribute('aria-hidden', 'true'); + document.body.style.overflow = ''; + } + window.openSmartSearchModal = openSmartSearchModal; + window.closeSmartSearchModal = closeSmartSearchModal; function renderPageFilterBar(key, placeholder, helperText, reloadJs) { var inputId = key + 'FilterInput'; var value = getExplorerPageFilter(key); @@ -110,22 +358,455 @@ 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' }; - var LIQUIDITY_POOL_ROWS = [ - { category: 'Public Liquidity Pools', poolPair: 'cUSDT / cUSDC', poolType: 'DODO PMM', address: '0x9fcB06Aa1FD5215DC0E91Fd098aeff4B62fEa5C8', status: 'Created', notes: 'Pool created via CreateCUSDTCUSDCPool.s.sol' }, - { category: 'Public Liquidity Pools', poolPair: 'cUSDT / USDT (official)', poolType: 'DODO PMM', address: '0xa3Ee6091696B28e5497b6F491fA1e99047250c59', status: 'Created', notes: 'Pool created via CreateCUSDTUSDTPool.s.sol' }, - { category: 'Public Liquidity Pools', poolPair: 'cUSDC / USDC (official)', poolType: 'DODO PMM', address: '0x90bd9Bf18Daa26Af3e814ea224032d015db58Ea5', status: 'Created', notes: 'Pool created via CreateCUSDCUSDCPool.s.sol' }, - { category: 'Public Liquidity Pools', poolPair: 'cUSDT / XAU', poolType: 'DODO PMM', address: '', status: 'Not deployed', notes: 'Requires XAU token (not on chain)' }, - { category: 'Public Liquidity Pools', poolPair: 'cUSDC / XAU', poolType: 'DODO PMM', address: '', status: 'Not deployed', notes: 'Requires XAU token' }, - { category: 'Public Liquidity Pools', poolPair: 'cEURT / XAU', poolType: 'DODO PMM', address: '', status: 'Not deployed', notes: 'Requires XAU; cEURT is deployed' }, - { category: 'Private Stabilization Pools', poolPair: 'cUSDT ↔ XAU', poolType: 'PrivatePoolRegistry', address: '', status: 'Not deployed', notes: 'Stabilizer-only swap path' }, - { category: 'Private Stabilization Pools', poolPair: 'cUSDC ↔ XAU', poolType: 'PrivatePoolRegistry', address: '', status: 'Not deployed', notes: 'Stabilizer-only swap path' }, - { category: 'Private Stabilization Pools', poolPair: 'cEURT ↔ XAU', poolType: 'PrivatePoolRegistry', address: '', status: 'Not deployed', notes: 'Requires cEURT + XAU' }, - { category: 'Reserve Pools / Vault Backing', poolPair: 'ReserveSystem', poolType: 'Reserve', address: '0x607e97cD626f209facfE48c1464815DDE15B5093', status: 'Deployed', notes: 'Reserve core' }, - { category: 'Reserve Pools / Vault Backing', poolPair: 'ReserveTokenIntegration', poolType: 'Reserve', address: '0x34B73e6EDFd9f85a7c25EeD31dcB13aB6E969b96', status: 'Deployed', notes: 'Reserve token integration' }, - { category: 'Reserve Pools / Vault Backing', poolPair: 'StablecoinReserveVault', poolType: 'Reserve', address: '', status: 'Not on Chain 138', notes: 'Designed for Ethereum Mainnet' }, - { category: 'Reserve Pools / Vault Backing', poolPair: 'Bridge_Vault', poolType: 'Vault', address: '0x31884f84555210FFB36a19D2471b8eBc7372d0A8', status: 'Deployed', notes: 'Bridge vault' }, - { category: 'Bridge Liquidity Pool', poolPair: 'LiquidityPoolETH', poolType: 'Bridge LP', address: '', status: 'Placeholder', notes: 'ETH, WETH' } + 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 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'); } } @@ -135,7 +816,9 @@ 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; @@ -147,12 +830,16 @@ // 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' && _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','pools','more']; + var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','addresses','pools','liquidity','more']; if (detailViews.indexOf(viewName) === -1) currentDetailKey = ''; var homeEl = document.getElementById('homeView'); if (homeEl) homeEl.style.display = viewName === 'home' ? 'block' : 'none'; @@ -160,20 +847,25 @@ var target = document.getElementById(viewName + 'View'); if (target) target.classList.add('active'); } - // Expose nav handlers: re-entrancy guard prevents hashchange from calling stub again while we're inside + // 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; } }; - window.showPools = function() { if (_inNavHandler) return; _inNavHandler = true; try { switchToView('pools'); if (window._showPools) window._showPools(); } 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.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; } }; - // Defer to next tick to avoid synchronous recursion (applyHashRoute -> detail -> updatePath -> popstate/hash -> applyHashRoute -> detail) + // 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); }; @@ -598,15 +1290,21 @@ 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(); }); - console.log('Loading stats, blocks, and transactions...'); - loadStats(); - loadLatestBlocks(); - loadLatestTransactions(); - startTransactionUpdates(); + 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 { @@ -1254,23 +1952,23 @@ } window.showWETHTab = showWETHTab; - async function showWETHUtilities() { + async function renderWETHUtilitiesView() { showView('weth'); if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'weth') updatePath('/weth'); if (userAddress) { await refreshWETHBalances(); } } - window._showWETHUtilities = showWETHUtilities; + window._showWETHUtilities = renderWETHUtilitiesView; - async function showBridgeMonitoring() { + async function renderBridgeView() { showView('bridge'); if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'bridge') updatePath('/bridge'); await refreshBridgeData(); } - window._showBridgeMonitoring = showBridgeMonitoring; + window._showBridgeMonitoring = renderBridgeView; - async function showHome() { + async function renderHomeView() { showView('home'); if ((window.location.pathname || '').replace(/^\//, '').replace(/\/$/, '') !== 'home') updatePath('/home'); await loadStats(); @@ -1279,45 +1977,52 @@ // Start real-time transaction updates startTransactionUpdates(); } - window._showHome = showHome; + window._showHome = renderHomeView; - async function showBlocks() { + async function renderBlocksView() { showView('blocks'); if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'blocks') updatePath('/blocks'); await loadAllBlocks(); } - window._showBlocks = showBlocks; + window._showBlocks = renderBlocksView; - async function showTransactions() { + async function renderTransactionsView() { showView('transactions'); if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'transactions') updatePath('/transactions'); await loadAllTransactions(); } - window._showTransactions = showTransactions; + window._showTransactions = renderTransactionsView; + + async function renderAddressesView() { + showView('addresses'); + if ((window.location.pathname || '').replace(/^\//, '').split('/')[0] !== 'addresses') updatePath('/addresses'); + await loadAllAddresses(); + } + window._showAddresses = renderAddressesView; // Analytics view (Track 3+) - function showAnalytics() { + function renderAnalyticsView() { if (!hasAccess(3)) { showToast('Analytics features require Track 3 access. Please connect your wallet and ensure you are approved.', 'error'); return; } switchToView('analytics'); } - window._showAnalytics = showAnalytics; + window._showAnalytics = renderAnalyticsView; // Operator view (Track 4) - function showOperator() { + function renderOperatorView() { if (!hasAccess(4)) { showToast('Operator features require Track 4 access. Please connect your wallet and ensure you are approved.', 'error'); return; } switchToView('operator'); } - window._showOperator = showOperator; + window._showOperator = renderOperatorView; function showView(viewName) { currentView = viewName; - var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','pools','more']; + var detailViews = ['blockDetail','transactionDetail','addressDetail','tokenDetail','nftDetail','watchlist','searchResults','tokens','addresses','pools','liquidity','more']; if (detailViews.indexOf(viewName) === -1) currentDetailKey = ''; document.querySelectorAll('.detail-view').forEach(v => v.classList.remove('active')); const homeView = document.getElementById('homeView'); @@ -1375,10 +2080,12 @@ if (parts[0] === 'home') { if (currentView !== 'home') showHome(); return; } if (parts[0] === 'blocks') { if (currentView !== 'blocks') showBlocks(); return; } if (parts[0] === 'transactions') { if (currentView !== 'transactions') showTransactions(); return; } + if (parts[0] === 'addresses') { if (currentView !== 'addresses') showAddresses(); return; } if (parts[0] === 'bridge') { if (currentView !== 'bridge') showBridgeMonitoring(); return; } if (parts[0] === 'weth') { if (currentView !== 'weth') showWETHUtilities(); return; } if (parts[0] === 'watchlist') { if (currentView !== 'watchlist') showWatchlist(); return; } - if (parts[0] === 'pools') { if (currentView !== 'pools') showPools(); return; } + if (parts[0] === 'pools') { if (currentView !== 'pools') openPoolsView(); return; } + 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] === 'analytics') { if (currentView !== 'analytics') showAnalytics(); return; } @@ -1390,12 +2097,11 @@ window.toggleDarkMode = toggleDarkMode; function focusSearchWithHint(kind) { - showHome(); - var input = document.getElementById('searchInput'); - if (input) { - input.focus(); - if (kind === 'token') showToast('Enter token contract address (0x...) or search by name/symbol', 'info', 4000); + var prefill = kind === 'token' ? 'token' : ''; + if (kind === 'token') { + showToast('Enter token contract address (0x...) or search by name/symbol', 'info', 4000); } + openSmartSearchModal(prefill); } window.focusSearchWithHint = focusSearchWithHint; @@ -1642,6 +2348,7 @@ stats = await fetchAPIWithRetry(`${API_BASE}/v2/stats`); } + const activeBridgeContracts = getActiveBridgeContractCount(); statsGrid.innerHTML = `
Total Blocks
@@ -1657,7 +2364,7 @@
Bridge Contracts
-
2 Active
+
${activeBridgeContracts} Active
Network
@@ -1682,7 +2389,7 @@
Bridge Contracts
-
2 Active
+
${activeBridgeContracts} Active
Network
-
`; @@ -1692,7 +2399,7 @@ async function loadGasAndNetworkStats() { var el = document.getElementById('networkStatValue'); var gasCard = document.getElementById('gasNetworkCard'); - var gasContent = document.getElementById('gasNetworkContent'); + var summaryEl = document.getElementById('gasNetworkSummary'); try { var blocksResp = await fetchAPIWithRetry(BLOCKSCOUT_API + '/v2/blocks?page=1&page_size=20'); var blocks = blocksResp.items || blocksResp || []; @@ -1716,7 +2423,10 @@ if (statsResp.average_block_time != null) blockTimeSec = Number(statsResp.average_block_time).toFixed(1) + 's'; if (statsResp.transactions_per_second != null) tps = Number(statsResp.transactions_per_second).toFixed(2); if (el) el.innerHTML = (gasGwei !== '-' ? 'Gas: ' + escapeHtml(gasGwei) + '
' : '') + (blockTimeSec !== '-' ? 'Block: ' + escapeHtml(blockTimeSec) + '
' : '') + (tps !== '-' ? 'TPS: ' + escapeHtml(tps) : '') || 'Gas / TPS'; - if (gasCard && gasContent && CHAIN_ID === 138) { + if (summaryEl) { + summaryEl.textContent = 'Live chain health for Chain 138 updated ' + new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + '.'; + } + if (gasCard && CHAIN_ID === 138) { var curEl = document.getElementById('gasCurrentValue'); var tpsEl = document.getElementById('gasTpsValue'); var btEl = document.getElementById('gasBlockTimeValue'); @@ -1737,12 +2447,13 @@ var maxFee = Math.max.apply(null, fees) || 1; barsEl.innerHTML = fees.map(function(g, i) { var pct = maxFee > 0 ? (g / maxFee * 100) : 0; - return ''; + return ''; }).join(''); } } } catch (e) { if (el) el.textContent = '-'; + if (summaryEl) summaryEl.textContent = 'Live chain health unavailable right now.'; } } @@ -2082,6 +2793,7 @@ var blocksListPage = 1; var transactionsListPage = 1; + var addressesListPage = 1; const LIST_PAGE_SIZE = 25; async function loadAllBlocks(page) { @@ -2256,6 +2968,94 @@ } } + async function loadAllAddresses(page) { + if (page != null) addressesListPage = Math.max(1, parseInt(page, 10) || 1); + const container = document.getElementById('addressesList'); + if (!container) { return; } + + try { + container.innerHTML = '
Loading addresses...
'; + + const params = new URLSearchParams({ + page: String(addressesListPage), + page_size: String(LIST_PAGE_SIZE) + }); + const q = getExplorerPageFilter('addressesList'); + if (q) params.set('q', q); + + let addresses = []; + if (CHAIN_ID === 138) { + try { + const response = await fetchAPIWithRetry(`${BLOCKSCOUT_API}/v2/addresses?${params.toString()}`); + const items = response && (response.items || response.data); + if (Array.isArray(items) && items.length > 0) { + addresses = items.map(normalizeAddress).filter(function(addr) { return addr !== null; }); + } + } catch (blockscoutError) { + console.warn('Blockscout address list failed, trying explorer API:', blockscoutError.message || blockscoutError); + } + } + if (addresses.length === 0) { + const response = await fetchAPIWithRetry(`${API_BASE}/v1/addresses?${params.toString()}`); + const items = response && (response.data || response.items || []); + addresses = Array.isArray(items) ? items.map(normalizeAddress).filter(function(addr) { return addr !== null; }) : []; + } + + const filter = getExplorerPageFilter('addressesList'); + const filteredAddresses = filter ? addresses.filter(function(item) { + return matchesExplorerFilter([ + item.address, + item.label || '', + item.is_contract ? 'contract' : 'externally owned', + item.tx_sent, + item.tx_received, + item.transaction_count, + item.token_count, + item.first_seen_at || '', + item.last_seen_at || '' + ].join(' '), filter); + }) : addresses; + const filterBar = renderPageFilterBar('addressesList', 'Filter addresses by address, label, type, or activity...', 'Filters the indexed address list below.', 'loadAllAddresses(' + addressesListPage + ')'); + let html = filterBar + ''; + + if (addresses.length === 0) { + html += ''; + } else if (filteredAddresses.length === 0) { + html += ''; + } else { + filteredAddresses.forEach(function(item) { + var addr = String(item.address || ''); + var label = String(item.label || ''); + var isContract = !!item.is_contract; + var type = isContract ? 'Contract' : 'EOA'; + var txSent = Number(item.tx_sent || 0); + var txReceived = Number(item.tx_received || 0); + var tokenCount = Number(item.token_count || 0); + var lastSeen = String(item.last_seen_at || '—'); + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + } + + var pagination = '
'; + pagination += 'Page ' + addressesListPage + ''; + pagination += '
'; + html += '
AddressLabelTypeTx SentTx ReceivedTokensLast Seen
No addresses found
No addresses match the current filter
' + escapeHtml(shortenHash(addr)) + '' + escapeHtml(label || '—') + '' + escapeHtml(type) + '' + escapeHtml(String(txSent)) + '' + escapeHtml(String(txReceived)) + '' + escapeHtml(String(tokenCount)) + '' + escapeHtml(lastSeen) + '
' + pagination; + container.innerHTML = html; + } catch (error) { + container.innerHTML = '
Failed to load addresses: ' + escapeHtml(error.message || 'Unknown error') + '.
'; + } + } + window._loadAllAddresses = loadAllAddresses; + window.loadAllAddresses = loadAllAddresses; + async function loadTokensList() { var container = document.getElementById('tokensListContent'); if (!container) return; @@ -2309,19 +3109,371 @@ } window._loadTokensList = loadTokensList; - function showPools() { + function normalizeRouteStatus(status) { + return status || 'unavailable'; + } + + function renderRouteMetric(label, value) { + return '
' + escapeHtml(label) + '' + escapeHtml(value) + '
'; + } + + 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 = '
'; + html += '
'; + html += '
'; + html += '
' + escapeHtml(node.kind || 'route') + '
'; + html += '
' + escapeHtml(node.label || 'Untitled route') + '
'; + html += '
'; + html += '' + escapeHtml(status) + ''; + html += '
'; + html += '
'; + html += renderRouteMetric('Chain', (node.chainName || 'Chain') + ' (' + (node.chainId || '-') + ')'); + if (node.dexType) html += renderRouteMetric('DEX', node.dexType); + if (node.poolAddress) html += renderRouteMetric('Pool', shortenHash(node.poolAddress)); + if (node.depth) { + html += renderRouteMetric('TVL', '$' + Number(node.depth.tvlUsd || 0).toFixed(2)); + html += renderRouteMetric('Capacity', '$' + Number(node.depth.estimatedTradeCapacityUsd || 0).toFixed(2)); + html += renderRouteMetric('Freshness', node.depth.freshnessSeconds == null ? 'n/a' : node.depth.freshnessSeconds + 's'); + } + html += '
'; + if (node.notes && node.notes.length) { + html += ''; + } + if (node.children && node.children.length) { + html += '
'; + node.children.forEach(function(child) { + html += renderRouteNode(child, (depthLevel || 0) + 1); + }); + html += '
'; + } + html += '
'; + return html; + } + + function renderMissingQuotePools(missingPools) { + if (!missingPools || !missingPools.length) { + return '
No quote-token metadata gaps detected in the current indexed pool set.
'; + } + var html = '
'; + missingPools.forEach(function(pool) { + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + html += '
PoolChainToken 0Token 1Reason
' + escapeHtml(shortenHash(pool.poolAddress)) + '' + escapeHtml(String(pool.chainId)) + '' + escapeHtml((pool.token0Symbol || '') + ' ' + shortenHash(pool.token0Address)) + '' + escapeHtml((pool.token1Symbol || '') + ' ' + shortenHash(pool.token1Address)) + '' + escapeHtml(pool.reason || 'Missing quote token metadata') + '
'; + return html; + } + + function routeTreeQueryParams(query) { + var params = new URLSearchParams({ + chainId: '138', + tokenIn: query.tokenIn, + amountIn: query.amountIn || '1000000' + }); + if (query.tokenOut) params.set('tokenOut', query.tokenOut); + if (query.destinationChainId != null) params.set('destinationChainId', String(query.destinationChainId)); + return params.toString(); + } + + async function fetchRouteTree(query) { + var response = await fetchAPIWithRetry(`${TOKEN_AGGREGATION_API_BASE}/v1/routes/tree?${routeTreeQueryParams(query)}`); + if ((!response || response.decision === 'unresolved' || !Array.isArray(response.tree) || response.tree.length === 0) && + Number(query.destinationChainId || query.chainId || 138) === 138 && + safeAddress(query.tokenOut)) { + try { + var ctx = await fetchCurrentPmmContext(); + var liveFallback = await buildLiveDirectRouteFallback(query, ctx); + if (liveFallback) response = liveFallback; + } catch (e) {} + } + return { query: query, response: response }; + } + + function uniqueMissingQuotePools(results) { + var map = {}; + results.forEach(function(entry) { + var pools = (entry.response && entry.response.missingQuoteTokenPools) || []; + pools.forEach(function(pool) { + var key = String(pool.chainId) + ':' + String(pool.poolAddress || '').toLowerCase(); + if (!map[key]) map[key] = pool; + }); + }); + return Object.keys(map).sort().map(function(key) { return map[key]; }); + } + + function renderRouteSweepSummary(results) { + var html = '
'; + results.forEach(function(entry) { + var response = entry.response || {}; + var pools = Array.isArray(response.pools) ? response.pools : []; + var missing = Array.isArray(response.missingQuoteTokenPools) ? response.missingQuoteTokenPools : []; + var freshest = pools.length ? pools[0].depth && pools[0].depth.status ? pools[0].depth.status : 'unknown' : 'none'; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + html += ''; + }); + html += '
TokenDirect PoolsMissing Quote PoolsDecisionFreshest Status
' + escapeHtml(entry.query.symbol + ' ' + shortenHash(entry.query.tokenIn)) + '' + escapeHtml(String(pools.length)) + '' + escapeHtml(String(missing.length)) + '' + escapeHtml(response.decision || 'unresolved') + '' + escapeHtml(freshest) + '
'; + return html; + } + + function renderPriorityRouteCard(entry) { + var response = entry.response || {}; + var rootNodes = Array.isArray(response.tree) ? response.tree : []; + var decisionLabel = escapeHtml(response.decision || 'unresolved'); + if (response.decision === 'bridge-only' && response.destination && Number(response.destination.chainId) === 1) { + decisionLabel = 'bridge-only (destination completion on Mainnet)'; + } + var html = '
'; + html += '
'; + html += '
'; + html += '

' + escapeHtml(entry.query.title) + '

'; + html += '
Decision: ' + decisionLabel + ' | Generated: ' + escapeHtml((response.generatedAt || '').replace('T', ' ').replace('Z', ' UTC')) + '
'; + html += '
'; + html += ''; + html += '
'; + html += '
'; + if (response.source) { + html += '
'; + html += '
Source
'; + html += '
' + escapeHtml(response.source.chainName || 'Chain 138') + '
'; + html += '
' + escapeHtml(response.source.tokenIn ? (response.source.tokenIn.symbol + ' ' + shortenHash(response.source.tokenIn.address)) : entry.query.title) + '
'; + html += '
'; + } + if (response.destination) { + html += '
'; + html += '
Destination
'; + html += '
' + escapeHtml(response.destination.chainName || 'Destination') + '
'; + html += '
Chain ' + escapeHtml(String(response.destination.chainId || '-')) + '
'; + html += '
'; + } + html += '
'; + html += '
Coverage
'; + html += '
' + escapeHtml(String((response.pools || []).length)) + ' direct pool(s)
'; + html += '
' + escapeHtml(String((response.missingQuoteTokenPools || []).length)) + ' missing quote-token pool(s)
'; + html += '
'; + html += '
'; + if (rootNodes.length === 0) { + html += '
No live route nodes available for this query yet.
'; + } else { + rootNodes.slice(0, 4).forEach(function(node) { + html += renderRouteNode(node, 0); + }); + } + html += '
'; + return html; + } + + function updatePoolsMissingQuoteBadge(count) { + var badge = document.getElementById('poolsMissingQuoteBadge'); + if (!badge) return; + var n = Math.max(0, Number(count || 0)); + badge.textContent = 'Missing quote routes ' + String(n); + badge.style.display = n > 0 ? 'inline-block' : 'none'; + } + + async function loadLiveRouteTrees() { + var container = document.getElementById('poolRouteTreeContent'); + if (!container) return; + container.innerHTML = '
Loading live route tree...
'; + + try { + var ctx = await fetchCurrentPmmContext(); + var priorityQueries = buildRoutePriorityQueries(ctx); + var priorityResults = await Promise.allSettled(priorityQueries.map(fetchRouteTree)); + var sweepResults = await Promise.allSettled(CHAIN_138_ROUTE_SWEEP_TOKENS.map(function(token) { + return fetchRouteTree({ + key: token.symbol.toLowerCase(), + title: token.symbol + ' coverage sweep', + symbol: token.symbol, + tokenIn: token.address, + amountIn: '1000000' + }); + })); + var priorityOkResults = priorityResults.filter(function(result) { return result.status === 'fulfilled'; }).map(function(result) { return result.value; }); + var priorityErrors = priorityResults.filter(function(result) { return result.status === 'rejected'; }); + var sweepOkResults = sweepResults.filter(function(result) { return result.status === 'fulfilled'; }).map(function(result) { return result.value; }); + var allSweepMissing = uniqueMissingQuotePools(sweepOkResults); + + var html = '
'; + html += '
'; + html += '
'; + html += '

Route Coverage Sweep

'; + html += ''; + html += '
'; + html += '
'; + html += '
Priority routes
' + escapeHtml(String(priorityOkResults.length)) + ' ok' + (priorityErrors.length ? ' / ' + String(priorityErrors.length) + ' failed' : '') + '
'; + html += '
Sweep tokens
' + escapeHtml(String(sweepOkResults.length)) + ' ok
'; + html += '
Missing quote routes
' + escapeHtml(String(allSweepMissing.length)) + '
'; + html += '
'; + html += '
This sweep queries every known Chain 138 compliant token so we can catch direct pool depth, bridge-leg paths to Mainnet or other destinations, and any pools whose quote-token metadata is still missing in the index.
'; + 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 += '
'; + + container.innerHTML = html; + updatePoolsMissingQuoteBadge(allSweepMissing.length); + } catch (err) { + container.innerHTML = '
Failed to load live route tree: ' + escapeHtml(err.message || 'Unknown error') + '
'; + updatePoolsMissingQuoteBadge(0); + } + } + + 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 the pools and reserve-related entries we know about.', 'showPools()'); - var rows = LIQUIDITY_POOL_ROWS.map(function(row) { + 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 page lists known liquidity, reserve, and bridge pool references. Pool entries are grouped by role, and placeholder rows mark planned or external assets.
'; + 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.
'; + html += '
'; + html += '
Live local pools
' + escapeHtml(String(summary.liveLocal)) + '
Funded or deployed directly on Chain 138
'; + html += '
External Mainnet-side
' + escapeHtml(String(summary.externalMainnet)) + '
Expected to live off Chain 138 by design
'; + html += '
Not yet created
' + escapeHtml(String(summary.notYetCreated)) + '
No live pool mapping currently registered
'; + html += '
Needs attention
' + escapeHtml(String(summary.missingCode + summary.partial)) + '
Missing code, partial funding, or unfunded rows
'; + html += '
'; html += '
'; if (rows.length === 0) { html += ''; @@ -2342,14 +3494,178 @@ }); } html += '
CategoryPool PairSystemAddressStatusNotes
No pool data available yet.
'; + html += '
'; + html += '
'; + html += '

Live Route Decision Tree

'; + html += ''; + html += '
'; + html += '
This live panel follows the Chain 138 DEX and bridge graph end-to-end so we can see direct pool depth, bridge leg availability, destination swap branches, and any missing quote-token metadata that still needs cleanup.
'; + html += '
Loading live route tree...
'; + html += '
'; container.innerHTML = html; + setTimeout(function() { + loadLiveRouteTrees(); + }, 0); + _poolsRouteTreeRefreshTimer = setInterval(function() { + if (currentView === 'pools') { + loadLiveRouteTrees(); + } + }, ROUTE_TREE_REFRESH_MS); } catch (err) { container.innerHTML = '
Failed to load pools: ' + escapeHtml(err.message || 'Unknown error') + '
'; } } - window._showPools = showPools; + window.renderPoolsView = renderPoolsView; - function showMore() { + 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 += '
'; + html += '
' + escapeHtml(card.title) + '
'; + html += '' + escapeHtml(card.method) + ''; + html += '
'; + html += '
' + escapeHtml(card.href) + '
'; + html += '
' + escapeHtml(card.notes) + '
'; + html += '
'; + }); + html += '
'; + + html += '
'; + html += '

Quick Request Examples

'; + 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 use Pools for live route-tree diagnostics and contract-state checks.
'; + 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'); @@ -2357,9 +3673,11 @@ var cards = [ { href: '/bridge', icon: 'fa-bridge', title: 'Bridge', desc: 'Inspect CCIP routes, bridge endpoints, and cross-chain references.', action: 'showBridgeMonitoring();' }, { href: '/weth', icon: 'fa-coins', title: 'WETH', desc: 'Wrap and unwrap WETH9 / WETH10 and review utility contract details.', action: 'showWETHUtilities();' }, + { href: '/liquidity', icon: 'fa-wave-square', title: 'Liquidity', desc: 'Open the public liquidity map, route matrix endpoints, partner payload templates, and fallback execution access.', action: 'showLiquidityAccess();' }, { href: '/tokens', icon: 'fa-tag', title: 'Tokens', desc: 'Browse the indexed token list and jump into token detail pages.', action: 'showTokensList();' }, + { href: '/addresses', icon: 'fa-address-book', title: 'Addresses', desc: 'Review indexed addresses, labels, contract status, and recent activity.', action: 'showAddresses();' }, { href: '/watchlist', icon: 'fa-star', title: 'Watchlist', desc: 'Track saved addresses and revisit them quickly.', action: 'showWatchlist();' }, - { href: '/pools', icon: 'fa-water', title: 'Pools', desc: 'Review the liquidity snapshot and config caps for public pools and reserve links.', action: 'showPools();' }, + { href: '/pools', icon: 'fa-water', title: 'Pools', desc: 'Review the liquidity snapshot and config caps for public pools and reserve links.', action: 'openPoolsView();' }, { href: '/analytics', icon: 'fa-chart-line', title: 'Analytics', desc: 'Open the Track 3 analytics hub for network and bridge insight.', action: 'showAnalytics();' }, { href: '/operator', icon: 'fa-cog', title: 'Operator', desc: 'Open the Track 4 operator panel for deployment and maintenance tools.', action: 'showOperator();' } ]; @@ -2373,7 +3691,7 @@ html += '
'; container.innerHTML = html; } - window._showMore = showMore; + window._showMore = renderMoreView; async function refreshBridgeData() { const container = document.getElementById('bridgeContent'); @@ -2389,6 +3707,22 @@ // Ethereum Mainnet Bridge Contracts const WETH9_BRIDGE_MAINNET = '0x2A0840e5117683b11682ac46f5CF5621E67269E3'; const WETH10_BRIDGE_MAINNET = '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03'; + + const explorerLinks = { + 'BSC (56)': { label: 'BscScan', baseUrl: 'https://bscscan.com/address/' }, + 'Polygon (137)': { label: 'PolygonScan', baseUrl: 'https://polygonscan.com/address/' }, + 'Avalanche (43114)': { label: 'Avalanche Explorer', baseUrl: 'https://subnets.avax.network/c-chain/address/' }, + 'Base (8453)': { label: 'BaseScan', baseUrl: 'https://basescan.org/address/' }, + 'Arbitrum (42161)': { label: 'Arbiscan', baseUrl: 'https://arbiscan.io/address/' }, + 'Optimism (10)': { label: 'Optimistic Etherscan', baseUrl: 'https://optimistic.etherscan.io/address/' }, + 'Ethereum Mainnet (1)': { label: 'Etherscan', baseUrl: 'https://etherscan.io/address/' } + }; + function renderExplorerLink(chainLabel, address) { + const explorer = explorerLinks[chainLabel]; + if (!explorer) return 'No explorer'; + const url = explorer.baseUrl + address; + return 'View on ' + escapeHtml(explorer.label) + ''; + } // Bridge routes configuration const routes = { @@ -2411,6 +3745,13 @@ 'Ethereum Mainnet (1)': WETH10_BRIDGE_MAINNET } }; + const sourceBridgeCount = 2; + const mainnetBridgeCount = 2; + const routeBridgeCount = new Set([ + ...Object.values(routes.weth9), + ...Object.values(routes.weth10) + ].map(function(addr) { return String(addr || '').toLowerCase(); })).size; + const totalBridgeCount = getActiveBridgeContractCount(); const bridgeFilter = getExplorerPageFilter('bridgeRoutes'); const filterBar = renderPageFilterBar('bridgeRoutes', 'Filter by chain name, chain ID, or bridge address...', 'Filters the route tables below.', 'refreshBridgeData()'); @@ -2419,6 +3760,7 @@ let html = filterBar + `
CCIP Bridge Ecosystem
+
${sourceBridgeCount} source contracts / ${mainnetBridgeCount} mainnet contracts / ${routeBridgeCount} unique route contracts
Cross-chain interoperability powered by Chainlink CCIP
@@ -2428,6 +3770,7 @@

Chain 138 (Source Chain)

+ ${sourceBridgeCount} source contracts / ${mainnetBridgeCount} mainnet contracts / ${routeBridgeCount} unique route contracts
@@ -2455,7 +3798,7 @@

CCIPWETH9Bridge Routes

- 7 Destinations + ${Object.keys(routes.weth9).length} Destinations
@@ -2464,6 +3807,7 @@ + @@ -2477,7 +3821,7 @@ return !bridgeFilter || matchesExplorerFilter(entry[0] + ' ' + entry[1], bridgeFilter); }); if (weth9Routes.length === 0) { - html += ''; + html += ''; } for (const [chain, address] of weth9Routes) { const chainId = chain.match(/\\((\d+)\\)/)?.[1] || ''; @@ -2486,6 +3830,7 @@ + `; } @@ -2500,7 +3845,7 @@

CCIPWETH10Bridge Routes

- 7 Destinations + ${Object.keys(routes.weth10).length} Destinations
Destination Chain Chain ID Bridge AddressExplorer
No WETH9 routes match the current filter.
No WETH9 routes match the current filter.
${chain.replace(/\s*\\(\\d+\\)/, '')} ${chainId} ${escapeHtml(shortenHash(address))}${renderExplorerLink(chain, address)}
@@ -2509,6 +3854,7 @@ + @@ -2516,7 +3862,7 @@ // Add WETH10 routes if (weth10Routes.length === 0) { - html += ''; + html += ''; } for (const [chain, address] of weth10Routes) { const chainId = chain.match(/\\((\d+)\\)/)?.[1] || ''; @@ -2525,6 +3871,7 @@ + `; } @@ -2539,6 +3886,7 @@

Ethereum Mainnet Bridges

+ ${sourceBridgeCount} source contracts / ${mainnetBridgeCount} mainnet contracts / ${routeBridgeCount} unique route contracts
@@ -2564,7 +3912,10 @@
-

Bridge Information

+
+

Bridge Information

+ ${totalBridgeCount} Active Bridge Contracts +

CCIP Bridge Ecosystem enables cross-chain transfers of WETH9 and WETH10 tokens using Chainlink CCIP (Cross-Chain Interoperability Protocol).

@@ -2605,7 +3956,12 @@ function safeBlockNumber(v) { const n = String(v).replace(/[^0-9]/g, ''); return n ? n : null; } function safeTxHash(v) { const s = String(v); return /^0x[a-fA-F0-9]{64}$/.test(s) ? s : null; } - function safeAddress(v) { const s = String(v); return /^0x[a-fA-F0-9]{40}$/i.test(s) ? s : null; } + function safeAddress(v) { + const s = String(v || ''); + if (!/^0x[a-fA-F0-9]{40}$/i.test(s)) return null; + if (/^0x0{40}$/i.test(s)) return null; + return s; + } async function showBlockDetail(blockNumber) { const bn = safeBlockNumber(blockNumber); if (!bn) { showToast('Invalid block number', 'error'); return; } @@ -3782,6 +5138,8 @@ return; } + saveSmartSearchHistory(query); + closeSmartSearchModal(); const normalizedQuery = query.toLowerCase().replace(/\s/g, ''); try { @@ -3980,24 +5338,63 @@ `; document.head.appendChild(style); - // Search input and button handlers; mobile nav close on link click + // Search launcher, modal handlers, and mobile nav close on link click document.addEventListener('DOMContentLoaded', () => { - const searchInput = document.getElementById('searchInput'); - const searchBtn = document.getElementById('searchBtn'); - if (searchInput) { - searchInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { + const launchBtn = document.getElementById('searchLauncherBtn'); + const modal = document.getElementById('smartSearchModal'); + const backdrop = document.getElementById('smartSearchBackdrop'); + const closeBtn = document.getElementById('smartSearchCloseBtn'); + const input = document.getElementById('smartSearchInput'); + const submitBtn = document.getElementById('smartSearchSubmitBtn'); + if (launchBtn) { + launchBtn.addEventListener('click', function(e) { + e.preventDefault(); + openSmartSearchModal(''); + }); + } + if (closeBtn) { + closeBtn.addEventListener('click', function(e) { + e.preventDefault(); + closeSmartSearchModal(); + }); + } + if (backdrop) { + backdrop.addEventListener('click', closeSmartSearchModal); + } + if (input) { + input.addEventListener('input', function(e) { + updateSmartSearchPreview(e.target.value); + }); + input.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + e.preventDefault(); + closeSmartSearchModal(); + } else if (e.key === 'Enter') { e.preventDefault(); handleSearch(e.target.value); } }); } - if (searchBtn) { - searchBtn.addEventListener('click', (e) => { + if (submitBtn && input) { + submitBtn.addEventListener('click', function(e) { e.preventDefault(); - if (searchInput) handleSearch(searchInput.value); + handleSearch(input.value); }); } + window.addEventListener('keydown', function(e) { + var target = e.target; + var tag = target && target.tagName ? target.tagName.toLowerCase() : ''; + var isEditable = !!(target && (target.isContentEditable || tag === 'input' || tag === 'textarea' || tag === 'select')); + if (e.key === 'Escape' && modal && modal.style.display === 'block') { + e.preventDefault(); + closeSmartSearchModal(); + return; + } + if ((e.key === '/' || ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k')) && !isEditable) { + e.preventDefault(); + openSmartSearchModal(''); + } + }); var navLinks = document.getElementById('navLinks'); if (navLinks) { navLinks.addEventListener('click', function(e) { diff --git a/frontend/public/index.html b/frontend/public/index.html index d2adf33..ce09ccb 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -129,12 +129,18 @@ --bridge-blue: #3b82f6; --dark: #1f2937; --light: #f9fafb; + --card-bg: #ffffff; + --muted-surface: rgba(15, 23, 42, 0.04); + --muted-surface-strong: rgba(15, 23, 42, 0.08); --border: #e5e7eb; --text: #111827; --text-light: #6b7280; } body.dark-theme { --light: #111827; + --card-bg: #1e293b; + --muted-surface: rgba(148, 163, 184, 0.08); + --muted-surface-strong: rgba(148, 163, 184, 0.14); --border: #374151; --text: #f9fafb; --text-light: #9ca3af; @@ -159,7 +165,7 @@ .navbar { background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%); color: white; - padding: 1rem 2rem; + padding: 0.85rem 2rem; box-shadow: 0 2px 10px rgba(0,0,0,0.1); position: sticky; top: 0; @@ -169,6 +175,7 @@ max-width: 1400px; margin: 0 auto; display: flex; + gap: 1rem; justify-content: space-between; align-items: center; } @@ -178,6 +185,22 @@ display: flex; align-items: center; gap: 0.5rem; + padding: 0.45rem 0.7rem; + border-radius: 14px; + transition: background 0.2s, transform 0.2s, box-shadow 0.2s; + text-decoration: none; + color: inherit; + } + .logo:hover, + .logo:focus-visible { + background: rgba(255,255,255,0.12); + transform: translateY(-1px); + box-shadow: 0 8px 20px rgba(0,0,0,0.08); + outline: none; + } + .logo i { + width: 1.2rem; + text-align: center; } .nav-links { display: flex; @@ -189,18 +212,20 @@ .nav-links a, .nav-dropdown-trigger { color: white; text-decoration: none; - transition: opacity 0.2s; + transition: opacity 0.2s, background 0.2s; display: inline-flex; align-items: center; gap: 0.35rem; + padding: 0.45rem 0.7rem; + border-radius: 999px; } - .nav-links a:hover, .nav-dropdown-trigger:hover { opacity: 0.9; } + .nav-links a:hover, .nav-dropdown-trigger:hover { opacity: 1; background: rgba(255,255,255,0.12); } .nav-dropdown-trigger { background: none; border: none; cursor: pointer; font: inherit; - padding: 0.5rem 0.25rem; + padding: 0.45rem 0.7rem; } .nav-dropdown-trigger i.fa-chevron-down { font-size: 0.7rem; @@ -244,8 +269,25 @@ .nav-dropdown-menu li { margin: 0; } .search-box { flex: 1; - max-width: 600px; - margin: 0 2rem; + max-width: 560px; + margin: 0 1.25rem; + } + .search-box .btn { + flex-shrink: 0; + } + .search-box .btn, + .search-box .search-hint { + white-space: nowrap; + } + .search-box .search-label { + display: inline; + } + .nav-actions { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; + justify-content: flex-end; } .search-input { width: 100%; @@ -816,16 +858,124 @@ .nav-dropdown-trigger { width: 100%; justify-content: space-between; padding: 0.6rem 0.5rem; } } @media (max-width: 768px) { - .nav-container { flex-direction: column; gap: 1rem; align-items: stretch; } - .search-box { max-width: 100%; margin: 0; } + .nav-container { flex-direction: column; gap: 0.75rem; align-items: stretch; } + .logo { + align-self: flex-start; + padding: 0.3rem 0.5rem; + font-size: 1.2rem; + border-radius: 12px; + } + .logo > div > span:first-child { line-height: 1.1; } + .logo > div > span:last-child { font-size: 0.68rem !important; } + .search-box { max-width: 100%; margin: 0; width: 100%; gap: 0.4rem; } + .search-box .btn { + padding: 0.55rem 0.7rem !important; + min-width: 2.75rem; + justify-content: center; + } + .search-box .search-label { display: none; } + .search-box .search-hint { display: none; } + .nav-actions { width: 100%; justify-content: space-between; gap: 0.5rem; } + .nav-actions > * { flex: 0 0 auto; } + .nav-actions #walletConnect { margin-left: auto; } + .nav-actions #walletConnectBtn, + .nav-actions #themeToggle, + .nav-actions #localeSelect { padding-left: 0.55rem; padding-right: 0.55rem; } .nav-links { flex-wrap: wrap; justify-content: center; } } - #gasNetworkContent { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } - @media (max-width: 600px) { - #gasNetworkContent { grid-template-columns: 1fr; } + .gas-network-card { margin-bottom: 1rem; } + .gas-network-header { + display: flex; + flex-direction: column; + gap: 0.75rem; + align-items: stretch; } + .gas-network-row, + .gas-network-compact, + .gas-network-pill, + .gas-network-spark { + display: flex; + } + .gas-network-row, + .gas-network-pill, + .gas-network-spark { + align-items: center; + } + .gas-network-row { + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + } + .gas-network-compact { align-items: center; gap: 0.75rem; flex-wrap: wrap; } + .gas-network-pill { gap: 0.4rem; padding: 0.45rem 0.7rem; border-radius: 999px; background: rgba(37, 99, 235, 0.08); color: var(--text); font-size: 0.86rem; border: 1px solid rgba(37, 99, 235, 0.12); white-space: nowrap; } + .gas-network-pill strong { font-size: 0.92rem; } + .gas-network-spark { align-items: flex-end; gap: 3px; height: 36px; min-width: 110px; } + .gas-network-spark span { + width: 8px; + border-radius: 4px 4px 0 0; + background: var(--primary); + opacity: 0.85; + } + .gas-network-subtle { color: var(--text-light); font-size: 0.82rem; white-space: nowrap; } .btn-copy { background: none; border: none; cursor: pointer; padding: 0.25rem; margin-left: 0.35rem; color: var(--text-light); vertical-align: middle; } .btn-copy:hover { color: var(--primary); } + .site-footer { + margin-top: 2.5rem; + padding: 2rem 0 2.5rem; + border-top: 1px solid var(--border); + background: rgba(255, 255, 255, 0.65); + backdrop-filter: blur(8px); + } + body.dark-theme .site-footer { + background: rgba(15, 23, 42, 0.78); + } + .site-footer-grid { + display: grid; + grid-template-columns: minmax(0, 1.6fr) repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + align-items: start; + } + .site-footer-title { + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-light); + margin-bottom: 0.6rem; + } + .site-footer-links { + display: grid; + gap: 0.45rem; + font-size: 0.92rem; + } + .site-footer-links a { + color: var(--text); + } + .site-footer-links a:hover { + color: var(--primary); + } + .site-footer-note { + color: var(--text-light); + font-size: 0.9rem; + line-height: 1.65; + } + @media (max-width: 900px) { + .site-footer-grid { grid-template-columns: 1fr; } + } + body.dark-theme #smartSearchModal .btn.btn-secondary { + background: rgba(148, 163, 184, 0.12); + color: var(--text); + border-color: rgba(148, 163, 184, 0.24); + } + body.dark-theme #smartSearchModal aside { + background: linear-gradient(180deg, var(--muted-surface), var(--muted-surface-strong)); + } + body.dark-theme #smartSearchModal main > div:first-of-type { + background: rgba(15, 23, 42, 0.72); + } + body.dark-theme #smartSearchModal #smartSearchPreview .loading { + color: var(--text-light); + } @@ -857,20 +1007,19 @@
+
+
+
@@ -920,22 +1129,26 @@
-
-
-

Gas & Network

- -
-
-
-
Current base fee
-
-
TPS:
-
Block time:
-
Failed (recent):
+
+
+
+

Gas & Network

+
-
-
Gas history (last 10 blocks)
-
+
+
+
Base fee
+
Block time
+
TPS
+
Failed
+
+
+
+
History (10 blocks)
+
+
+
Live chain health for Chain 138.
+
@@ -1178,11 +1391,22 @@
+
+
+
+

All Addresses

+
+
+
Loading addresses...
+
+
+
+
- +

Address Details

@@ -1252,6 +1476,10 @@

Pools

+
+ + +
Loading pools...
@@ -1259,6 +1487,22 @@
+
+ +
+
+ +

Liquidity Access

+
+ +
+
+
+
Loading liquidity access...
+
+
+
+
@@ -1297,6 +1541,41 @@
- + + + diff --git a/frontend/public/privacy.html b/frontend/public/privacy.html new file mode 100644 index 0000000..6fa92af --- /dev/null +++ b/frontend/public/privacy.html @@ -0,0 +1,39 @@ + + + + + + Privacy Policy | SolaceScanScout + + + + +
+
+
SolaceScanScout Privacy Policy
+ Back to explorer +
+
+

Privacy Policy

+

Last updated: 2026-03-25

+

SolaceScanScout is a blockchain explorer. Most content you view comes from public blockchain data and public APIs. We do not ask for personal information to browse the explorer.

+
    +
  • We may store theme preference, locale, recent searches, and similar local UI settings in your browser.
  • +
  • When you use wallet features or the Snap companion, the app may interact with your wallet provider to complete the request you initiate.
  • +
  • Explorer queries are sent to the configured blockchain APIs and RPC endpoints so the site can display blocks, transactions, addresses, and related data.
  • +
  • We do not sell personal data. We also do not intentionally track users with advertising cookies on this explorer.
  • +
+

If you have privacy questions, contact support@d-bis.org.

+
+
+ + diff --git a/frontend/public/terms.html b/frontend/public/terms.html new file mode 100644 index 0000000..e2d60ab --- /dev/null +++ b/frontend/public/terms.html @@ -0,0 +1,39 @@ + + + + + + Terms of Service | SolaceScanScout + + + + +
+
+
SolaceScanScout Terms of Service
+ Back to explorer +
+
+

Terms of Service

+

Last updated: 2026-03-25

+

This explorer is provided for informational and operational purposes. By using it, you agree that:

+
    +
  • Blockchain data may be delayed, incomplete, or temporarily unavailable.
  • +
  • You are responsible for verifying addresses, transactions, and contract details before acting on them.
  • +
  • We may update features, endpoints, and policies as the explorer evolves.
  • +
  • The explorer is not legal, financial, or tax advice.
  • +
+

For service questions, contact support@d-bis.org.

+
+
+ + diff --git a/frontend/src/app/liquidity/page.tsx b/frontend/src/app/liquidity/page.tsx new file mode 100644 index 0000000..0b8ef1c --- /dev/null +++ b/frontend/src/app/liquidity/page.tsx @@ -0,0 +1,257 @@ +import Link from 'next/link' +import { Card } from '@/libs/frontend-ui-primitives/Card' + +const publicApiBase = '/token-aggregation/api/v1' + +const 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', + }, +] + +const publicEndpoints = [ + { + name: 'Canonical route matrix', + method: 'GET', + href: `${publicApiBase}/routes/matrix`, + notes: 'All live and optional non-live route inventory with counts and filters.', + }, + { + name: 'Live ingestion export', + method: 'GET', + href: `${publicApiBase}/routes/ingestion?family=LiFi`, + notes: 'Flat live-route export for adapter ingestion and route discovery.', + }, + { + name: 'Partner payload templates', + method: 'GET', + href: `${publicApiBase}/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true`, + notes: 'Builds exact 1inch, 0x, and LiFi request templates from live routes.', + }, + { + name: 'Resolve supported partner payloads', + method: 'POST', + href: `${publicApiBase}/routes/partner-payloads/resolve`, + notes: 'Accepts partner, amount, and addresses and returns supported payloads by default.', + }, + { + name: 'Dispatch supported partner payload', + method: 'POST', + href: `${publicApiBase}/routes/partner-payloads/dispatch`, + notes: 'Resolves then dispatches a single supported partner payload when the chain is supported.', + }, + { + name: 'Internal Chain 138 execution plan', + method: 'POST', + href: `${publicApiBase}/routes/internal-execution-plan`, + notes: 'Returns the internal DODO PMM fallback plan when external partner support is unavailable.', + }, +] + +const routeHighlights = [ + 'Direct live routes: cUSDT <-> cUSDC, cUSDT <-> USDT, cUSDC <-> USDC, cUSDT <-> cXAUC, cUSDC <-> cXAUC, cEURT <-> cXAUC.', + 'Multi-hop public routes exist through cXAUC for cEURT <-> cUSDT, cEURT <-> cUSDC, and an alternate cUSDT <-> cUSDC path.', + 'Mainnet bridge discovery is live for cUSDT -> USDT and cUSDC -> USDC through the configured UniversalCCIPBridge lane.', + 'External partner templates are available for 1inch, 0x, and LiFi, but Chain 138 remains unsupported on those public partner networks today.', + 'When partner support is unavailable, the explorer can surface the internal DODO PMM execution plan instead of a dead end.', +] + +const requestExamples = [ + { + title: 'Inspect the full route matrix', + code: `GET ${publicApiBase}/routes/matrix?includeNonLive=true`, + }, + { + title: 'Filter live same-chain swap routes on Chain 138', + code: `GET ${publicApiBase}/routes/ingestion?fromChainId=138&routeType=swap`, + }, + { + title: 'Generate partner templates for review', + code: `GET ${publicApiBase}/routes/partner-payloads?partner=LiFi&amount=1000000&includeUnsupported=true`, + }, + { + title: 'Resolve a dispatch candidate', + code: `POST ${publicApiBase}/routes/partner-payloads/resolve`, + }, + { + title: 'Build the internal fallback plan', + code: `POST ${publicApiBase}/routes/internal-execution-plan`, + }, +] + +export default function LiquidityPage() { + return ( +
+
+
+ Chain 138 Liquidity Access +
+

+ Public liquidity, route discovery, and execution access points +

+

+ This explorer page pulls together the live public DODO PMM liquidity on Chain 138 and the + token-aggregation endpoints that DEX aggregators, integrators, and operators can use for + route discovery, payload generation, and internal fallback execution planning. +

+
+ +
+ +
Live public pools
+
6
+
+ Verified public DODO PMM pools on Chain 138. +
+
+ +
Public access path
+
/token-aggregation/api/v1
+
+ Explorer-hosted proxy path for route, quote, and reporting APIs. +
+
+ +
Partner status
+
Fallback Ready
+
+ Mainnet stable bridge routing is live; 1inch, 0x, and LiFi templates remain available for partner integrations, with internal fallback for unsupported Chain 138 execution. +
+
+
+ +
+ +
+ {livePools.map((pool) => ( +
+
+
+
{pool.pair}
+
+ Pool: {pool.poolAddress} +
+
+
+ Reserves: {pool.reserves} +
+
+
+ ))} +
+
+ + +
+ {routeHighlights.map((item) => ( +

{item}

+ ))} +
+
+
+ + + +
+ +
+ {requestExamples.map((example) => ( +
+
{example.title}
+ + {example.code} + +
+ ))} +
+
+ + +
+

+ Use the wallet page for network onboarding and the explorer token list URL, then use this + page for route and execution discovery. +

+

+ The route APIs complement the existing route decision tree and market-data APIs already + proxied through the explorer. +

+
+ + Open wallet tools + + + Route tree API + + + Explorer docs + +
+
+
+
+
+ ) +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index d1d87c3..5c4dcf0 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useState } from 'react' -import { Card } from '@/libs/frontend-ui-primitives' +import { Card } from '@/libs/frontend-ui-primitives/Card' import Link from 'next/link' import { blocksApi } from '@/services/api/blocks' @@ -93,12 +93,37 @@ export default function Home() {
))}
-
+
View all blocks →
+ +
+ +

+ Explore the public Chain 138 DODO PMM liquidity mesh, the canonical route matrix, and the + partner payload endpoints exposed through the explorer. +

+
+ + Open liquidity access → + +
+
+ +

+ Add Chain 138, Ethereum Mainnet, and ALL Mainnet to MetaMask, then use the explorer token + list URL so supported tokens appear automatically. +

+
+ + Open wallet tools → + +
+
+
) } diff --git a/frontend/src/app/wallet/page.tsx b/frontend/src/app/wallet/page.tsx index 9ede604..a2af1c1 100644 --- a/frontend/src/app/wallet/page.tsx +++ b/frontend/src/app/wallet/page.tsx @@ -1,4 +1,5 @@ import { AddToMetaMask } from '@/components/wallet/AddToMetaMask' +import Link from 'next/link' export default function WalletPage() { return ( @@ -8,6 +9,13 @@ export default function WalletPage() { Connect Chain 138 (DeFi Oracle Meta Mainnet) and Ethereum Mainnet to MetaMask and other Web3 wallets. Use the token list URL so tokens and oracles are discoverable.

+
+ Need swap and liquidity discovery too? Visit the{' '} + + Liquidity Access + {' '} + page for live Chain 138 pools, route matrix links, partner payload templates, and the internal fallback execution plan endpoints. +
) } diff --git a/frontend/src/components/common/Footer.tsx b/frontend/src/components/common/Footer.tsx new file mode 100644 index 0000000..df4bdf6 --- /dev/null +++ b/frontend/src/components/common/Footer.tsx @@ -0,0 +1,65 @@ +const footerLinkClass = + 'text-gray-600 dark:text-gray-400 hover:text-primary-600 dark:hover:text-primary-400 transition-colors' + +export default function Footer() { + const year = new Date().getFullYear() + + return ( +
+
+
+
+
+ SolaceScanScout +
+

+ Built from Blockscout foundations and Solace Bank Group PLC frontend + work. Explorer data is powered by Blockscout, Chain 138 RPC, and the + companion MetaMask Snap. +

+

+ © {year} Solace Bank Group PLC. All rights reserved. +

+
+ + + +
+
+ Contact +
+
+

+ Support:{' '} + + support@d-bis.org + +

+

+ Snap site:{' '} + + explorer.d-bis.org/snap/ + +

+

+ Questions about the explorer, chain metadata, route discovery, or liquidity access + can be sent to the support mailbox above. +

+
+
+
+
+
+ ) +} diff --git a/frontend/src/components/common/Navbar.tsx b/frontend/src/components/common/Navbar.tsx index 40f3f64..6e8d599 100644 --- a/frontend/src/components/common/Navbar.tsx +++ b/frontend/src/components/common/Navbar.tsx @@ -91,9 +91,21 @@ export default function Navbar() {
- setMobileMenuOpen(false)}> - SolaceScanScout - The Defi Oracle Meta Explorer + setMobileMenuOpen(false)} + aria-label="Go to explorer home" + > + + + + + + + SolaceScanScout + + The Defi Oracle Meta Explorer
Search Wallet + Liquidity
@@ -154,6 +167,7 @@ export default function Navbar() {
  • setMobileMenuOpen(false)}>Search
  • setMobileMenuOpen(false)}>Wallet
  • +
  • setMobileMenuOpen(false)}>Liquidity
)}
diff --git a/scripts/check-explorer-health.sh b/scripts/check-explorer-health.sh new file mode 100755 index 0000000..b104323 --- /dev/null +++ b/scripts/check-explorer-health.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +set -euo pipefail + +BASE_URL="${1:-https://explorer.d-bis.org}" + +python3 - "$BASE_URL" <<'PY' +import re +import sys +import requests + +base = sys.argv[1].rstrip("/") +session = requests.Session() +session.headers.update({"User-Agent": "ExplorerHealthCheck/1.0"}) + +checks = [ + "/", + "/home", + "/blocks", + "/transactions", + "/addresses", + "/bridge", + "/weth", + "/tokens", + "/pools", + "/watchlist", + "/more", + "/analytics", + "/operator", + "/liquidity", + "/snap/", + "/docs.html", + "/privacy.html", + "/terms.html", + "/acknowledgments.html", + "/api/v2/stats", + "/api/config/token-list", + "/api/config/networks", + "/token-aggregation/api/v1/routes/tree?chainId=138&tokenIn=0x93E66202A11B1772E55407B32B44e5Cd8eda7f22&tokenOut=0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1&amountIn=1000000", + "/token-aggregation/api/v1/routes/matrix", + "/token-aggregation/api/v1/routes/ingestion?fromChainId=138&routeType=swap", + "/token-aggregation/api/v1/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true", +] + +failed = False + +print("== Core routes ==") +for path in checks: + url = base + path + try: + resp = session.get(url, timeout=20, allow_redirects=True) + ctype = resp.headers.get("content-type", "") + print(f"{resp.status_code:>3} {path} [{ctype[:50]}]") + if resp.status_code >= 400: + failed = True + except Exception as exc: + failed = True + print(f"ERR {path} [{exc}]") + +print("\n== Internal href targets from homepage ==") +try: + home = session.get(base + "/", timeout=20).text + hrefs = sorted(set(re.findall(r'href="([^"]+)"', home))) + for href in hrefs: + if href.startswith("/") and not href.startswith("//"): + resp = session.get(base + href, timeout=20, allow_redirects=True) + print(f"{resp.status_code:>3} {href}") + if resp.status_code >= 400: + failed = True +except Exception as exc: + failed = True + print(f"ERR homepage href sweep failed: {exc}") + +print("\n== Static explorer domains referenced by bridge page ==") +external_roots = [ + "https://etherscan.io/", + "https://bscscan.com/", + "https://polygonscan.com/", + "https://subnets.avax.network/c-chain", + "https://basescan.org/", + "https://arbiscan.io/", + "https://optimistic.etherscan.io/", +] +for url in external_roots: + try: + resp = session.get(url, timeout=20, allow_redirects=True) + print(f"{resp.status_code:>3} {url}") + except Exception as exc: + print(f"ERR {url} [{exc}]") + +if failed: + sys.exit(1) +PY diff --git a/scripts/complete-explorer-api-access.sh b/scripts/complete-explorer-api-access.sh index cf1e174..af5b4c7 100644 --- a/scripts/complete-explorer-api-access.sh +++ b/scripts/complete-explorer-api-access.sh @@ -115,6 +115,30 @@ server { add_header Access-Control-Allow-Origin *; } + location /token-aggregation/api/v1/ { + proxy_pass http://192.168.11.140:3001/api/v1/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + add_header Access-Control-Allow-Origin *; + } + + location = /api/config/token-list { + default_type application/json; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=3600"; + alias /var/www/html/config/DUAL_CHAIN_TOKEN_LIST.tokenlist.json; + } + location = /api/config/networks { + default_type application/json; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=3600"; + alias /var/www/html/config/DUAL_CHAIN_NETWORKS.json; + } + location = / { root /var/www/html; try_files /index.html =404; @@ -147,6 +171,12 @@ server { try_files /index.html =404; } + location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|liquidity|watchlist|nft|home|analytics|operator|more|pools)(/|$) { + root /var/www/html; + try_files /index.html =404; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /var/www/html; expires 1y; @@ -165,6 +195,17 @@ server { add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type"; } + + location /token-aggregation/api/v1/ { + proxy_pass http://192.168.11.140:3001/api/v1/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + add_header Access-Control-Allow-Origin *; + } } NGINX_CONF diff --git a/scripts/fix-nginx-serve-custom-frontend.sh b/scripts/fix-nginx-serve-custom-frontend.sh index 39a1976..83f16fc 100755 --- a/scripts/fix-nginx-serve-custom-frontend.sh +++ b/scripts/fix-nginx-serve-custom-frontend.sh @@ -47,6 +47,34 @@ server { add_header Access-Control-Allow-Headers "Content-Type"; } + # Token-aggregation API for live route-tree, quotes, and market data + location /token-aggregation/api/v1/ { + proxy_pass http://127.0.0.1:3001/api/v1/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "Content-Type"; + } + + # Explorer config API (token list, networks) - serve from /var/www/html/config/ + location = /api/config/token-list { + default_type application/json; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=3600"; + alias /var/www/html/config/DUAL_CHAIN_TOKEN_LIST.tokenlist.json; + } + location = /api/config/networks { + default_type application/json; + add_header Access-Control-Allow-Origin *; + add_header Cache-Control "public, max-age=3600"; + alias /var/www/html/config/DUAL_CHAIN_NETWORKS.json; + } + location /health { access_log off; proxy_pass http://127.0.0.1:4000/api/v2/status; @@ -95,7 +123,7 @@ server { } # SPA paths on HTTP (for internal/LAN tests) - serve index.html before redirect - location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|watchlist|nft|home|analytics|operator)(/|$) { + location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|liquidity|watchlist|nft|home|analytics|operator)(/|$) { root /var/www/html; try_files /index.html =404; add_header Cache-Control "no-store, no-cache, must-revalidate"; @@ -171,7 +199,7 @@ server { add_header Cache-Control "public, immutable"; } - # Token-aggregation API at /api/v1/ (Chain 138 Snap: market data, swap quote, bridge). Service runs on port 3001. + # Token-aggregation API at /api/v1/ for the Snap site. Service runs on port 3001. location /api/v1/ { proxy_pass http://127.0.0.1:3001/api/v1/; proxy_http_version 1.1; @@ -183,6 +211,18 @@ server { add_header Access-Control-Allow-Origin *; } + # Token-aggregation API for the explorer SPA live route-tree and pool intelligence. + location /token-aggregation/api/v1/ { + proxy_pass http://127.0.0.1:3001/api/v1/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 60s; + add_header Access-Control-Allow-Origin *; + } + # Explorer config API (token list, networks) - serve from /var/www/html/config/ location = /api/config/token-list { default_type application/json; @@ -231,9 +271,9 @@ server { proxy_connect_timeout 75s; } - # SPA paths: /address, /tx, /block, /token, /tokens, /blocks, /transactions, /bridge, /weth, /watchlist, /nft, /home, /analytics, /operator + # SPA paths: /address, /tx, /block, /token, /tokens, /blocks, /transactions, /bridge, /weth, /liquidity, /watchlist, /nft, /home, /analytics, /operator # Must serve index.html so path-based routing works (regex takes precedence over proxy) - location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|watchlist|nft|home|analytics|operator)(/|$) { + location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|liquidity|watchlist|nft|home|analytics|operator)(/|$) { root /var/www/html; try_files /index.html =404; add_header Cache-Control "no-store, no-cache, must-revalidate"; @@ -334,4 +374,3 @@ echo "Next steps:" echo "1. Deploy custom frontend: ./scripts/deploy-frontend-to-vmid5000.sh" echo "2. Or manually copy: cp explorer-monorepo/frontend/public/index.html /var/www/html/index.html" echo "" -
Destination Chain Chain ID Bridge AddressExplorer
No WETH10 routes match the current filter.
No WETH10 routes match the current filter.
${chain.replace(/\s*\\(\\d+\\)/, '')} ${chainId} ${escapeHtml(shortenHash(address))}${renderExplorerLink(chain, address)}