Add explorer liquidity access and live route proxies
This commit is contained in:
169
backend/api/rest/addresses_list.go
Normal file
169
backend/api/rest/addresses_list.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
57
backend/api/rest/routes_proxy.go
Normal file
57
backend/api/rest/routes_proxy.go
Normal file
@@ -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 ""
|
||||
}
|
||||
40
frontend/public/acknowledgments.html
Normal file
40
frontend/public/acknowledgments.html
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Acknowledgments | SolaceScanScout</title>
|
||||
<meta name="description" content="Acknowledgments for the SolaceScanScout explorer.">
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; }
|
||||
.shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
|
||||
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 20px; padding: 1.5rem; box-shadow: 0 18px 60px rgba(15,23,42,0.12); }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.muted { color: #64748b; }
|
||||
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 1rem; }
|
||||
.brand { color: #fff; font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScanScout Acknowledgments</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0;">Acknowledgments</h1>
|
||||
<p class="muted">This explorer and its companion tools are built with help from the open-source and infrastructure tools below.</p>
|
||||
<ul>
|
||||
<li><strong>Blockscout</strong> for explorer indexing and API compatibility.</li>
|
||||
<li><strong>MetaMask</strong> for wallet connectivity and Snap support.</li>
|
||||
<li><strong>Chainlink CCIP</strong> for bridge-related routing and transport.</li>
|
||||
<li><strong>ethers.js</strong> for wallet and Ethereum interaction support.</li>
|
||||
<li><strong>Font Awesome</strong> for iconography.</li>
|
||||
<li><strong>Next.js</strong> and the frontend contributors at Solace Bank Group PLC.</li>
|
||||
</ul>
|
||||
<p class="muted">If we have missed a contributor or dependency, please let us know at <a href="mailto:support@d-bis.org">support@d-bis.org</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
54
frontend/public/docs.html
Normal file
54
frontend/public/docs.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Documentation | SolaceScanScout</title>
|
||||
<meta name="description" content="Documentation landing page for the SolaceScanScout explorer.">
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; }
|
||||
.shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
|
||||
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 20px; padding: 1.5rem; box-shadow: 0 18px 60px rgba(15,23,42,0.12); }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.muted { color: #64748b; }
|
||||
.grid { display: grid; gap: 0.85rem; }
|
||||
.link { display: block; padding: 0.85rem 1rem; border: 1px solid #e5e7eb; border-radius: 14px; background: #f8fafc; }
|
||||
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 1rem; }
|
||||
.brand { color: #fff; font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScanScout Documentation</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0;">Documentation</h1>
|
||||
<p class="muted">This landing page collects the key explorer and deployment references used by the SolaceScanScout stack.</p>
|
||||
<div class="grid" style="margin-top:1rem;">
|
||||
<a class="link" href="/privacy.html">Privacy Policy</a>
|
||||
<a class="link" href="/terms.html">Terms of Service</a>
|
||||
<a class="link" href="/acknowledgments.html">Acknowledgments</a>
|
||||
<a class="link" href="/liquidity">
|
||||
<strong>Liquidity access</strong>
|
||||
<div class="muted" style="margin-top:0.35rem;">Public Chain 138 pool snapshot, live Mainnet stable bridge paths, route matrix links, partner payload templates, and the internal fallback execution plan endpoint.</div>
|
||||
</a>
|
||||
<div class="link">
|
||||
<strong>Repository docs</strong>
|
||||
<div class="muted" style="margin-top:0.35rem;">The full technical documentation lives in the repository's <code>docs/</code> directory, including API access, deployment, and explorer guidance.</div>
|
||||
</div>
|
||||
<div class="link">
|
||||
<strong>Public routing API base</strong>
|
||||
<div class="muted" style="margin-top:0.35rem;"><code>/token-aggregation/api/v1</code> on <code>explorer.d-bis.org</code> is the public access path for route discovery, partner payload generation, and internal execution planning.</div>
|
||||
</div>
|
||||
<div class="link">
|
||||
<strong>Need help?</strong>
|
||||
<div class="muted" style="margin-top:0.35rem;">Email <a href="mailto:support@d-bis.org">support@d-bis.org</a> for explorer-related questions.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -857,20 +1007,19 @@
|
||||
</script>
|
||||
<nav class="navbar">
|
||||
<div class="nav-container">
|
||||
<div class="logo">
|
||||
<a class="logo" href="/home" aria-label="Go to explorer home" style="text-decoration:none; color:inherit;">
|
||||
<i class="fas fa-cube"></i>
|
||||
<div style="display: flex; flex-direction: column; gap: 0.25rem;">
|
||||
<span>SolaceScanScout</span>
|
||||
<span style="font-size: 0.75rem; font-weight: normal; opacity: 0.9;">The Defi Oracle Meta Explorer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-box" style="display: flex; gap: 0.5rem;">
|
||||
<label for="searchInput" class="sr-only">Search blockchain explorer</label>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Address, tx hash, block number, or token/contract name..." aria-label="Search blockchain explorer" aria-describedby="search-help-text" style="flex: 1;">
|
||||
<button id="searchBtn" class="btn btn-primary" style="padding: 0.5rem 1rem; white-space: nowrap;" aria-label="Search">
|
||||
<i class="fas fa-search"></i>
|
||||
</a>
|
||||
<div class="search-box" style="display: flex; gap: 0.5rem; align-items: center;">
|
||||
<button id="searchLauncherBtn" type="button" class="btn btn-primary" style="padding: 0.5rem 1rem; white-space: nowrap;" aria-label="Open explorer search">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
<span class="search-label" style="margin-left: 0.4rem;">Search</span>
|
||||
</button>
|
||||
<span id="search-help-text" class="sr-only">Search by address (0x...40 hex), transaction hash (0x...64 hex), block number, or token/contract name</span>
|
||||
<span class="search-hint" style="font-size: 0.82rem; opacity: 0.9;">Press <strong>/</strong> or <strong>Ctrl+K</strong></span>
|
||||
</div>
|
||||
<button type="button" class="nav-toggle" id="navToggle" aria-label="Toggle menu" aria-expanded="false"><i class="fas fa-bars" id="navToggleIcon"></i></button>
|
||||
<ul class="nav-links" id="navLinks">
|
||||
@@ -880,6 +1029,7 @@
|
||||
<li role="none"><a href="/home" role="menuitem" onclick="event.preventDefault(); showHome(); updatePath('/home'); closeNavMenu();" aria-label="Navigate to home page"><i class="fas fa-home" aria-hidden="true"></i> <span data-i18n="home">Home</span></a></li>
|
||||
<li role="none"><a href="/blocks" role="menuitem" onclick="event.preventDefault(); showBlocks(); updatePath('/blocks'); closeNavMenu();" aria-label="View all blocks"><i class="fas fa-cubes" aria-hidden="true"></i> <span data-i18n="blocks">Blocks</span></a></li>
|
||||
<li role="none"><a href="/transactions" role="menuitem" onclick="event.preventDefault(); showTransactions(); updatePath('/transactions'); closeNavMenu();" aria-label="View all transactions"><i class="fas fa-exchange-alt" aria-hidden="true"></i> <span data-i18n="transactions">Transactions</span></a></li>
|
||||
<li role="none"><a href="/addresses" role="menuitem" onclick="event.preventDefault(); showAddresses(); updatePath('/addresses'); closeNavMenu();" aria-label="View all addresses"><i class="fas fa-address-book" aria-hidden="true"></i> <span data-i18n="addresses">Addresses</span></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-dropdown" id="navDropdownTools">
|
||||
@@ -887,15 +1037,16 @@
|
||||
<ul class="nav-dropdown-menu" id="navMenuTools" role="menu">
|
||||
<li role="none"><a href="/bridge" role="menuitem" onclick="event.preventDefault(); showBridgeMonitoring(); updatePath('/bridge'); closeNavMenu();" aria-label="View bridge monitoring"><i class="fas fa-bridge" aria-hidden="true"></i> <span data-i18n="bridge">Bridge</span></a></li>
|
||||
<li role="none"><a href="/weth" role="menuitem" onclick="event.preventDefault(); showWETHUtilities(); updatePath('/weth'); closeNavMenu();" aria-label="View WETH utilities"><i class="fas fa-coins" aria-hidden="true"></i> <span data-i18n="weth">WETH</span></a></li>
|
||||
<li role="none"><a href="/liquidity" role="menuitem" onclick="event.preventDefault(); showLiquidityAccess(); updatePath('/liquidity'); closeNavMenu();" aria-label="View liquidity access"><i class="fas fa-wave-square" aria-hidden="true"></i> <span>Liquidity</span></a></li>
|
||||
<li role="none"><a href="/tokens" role="menuitem" onclick="event.preventDefault(); if(typeof showTokensList==='function')showTokensList();else focusSearchWithHint('token'); updatePath('/tokens'); closeNavMenu();" aria-label="View token list"><i class="fas fa-tag" aria-hidden="true"></i> <span data-i18n="tokens">Tokens</span></a></li>
|
||||
<li role="none"><a href="/pools" role="menuitem" onclick="event.preventDefault(); showPools(); updatePath('/pools'); closeNavMenu();" aria-label="View pools"><i class="fas fa-water" aria-hidden="true"></i> <span data-i18n="pools">Pools</span></a></li>
|
||||
<li role="none"><a href="/pools" role="menuitem" onclick="event.preventDefault(); showPools(); updatePath('/pools'); closeNavMenu();" aria-label="View pools"><i class="fas fa-water" aria-hidden="true"></i> <span data-i18n="pools">Pools</span> <span id="poolsMissingQuoteBadge" class="badge badge-warning" style="display:none; margin-left:0.35rem; vertical-align:middle;">0</span></a></li>
|
||||
<li role="none"><a href="/watchlist" role="menuitem" onclick="event.preventDefault(); showWatchlist(); updatePath('/watchlist'); closeNavMenu();" aria-label="Watchlist"><i class="fas fa-star" aria-hidden="true"></i> <span data-i18n="watchlist">Watchlist</span></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="/snap/" target="_self" rel="noopener" aria-label="Chain 138 MetaMask Snap"><i class="fas fa-wallet" aria-hidden="true"></i> <span>MetaMask Snap</span></a></li>
|
||||
<li role="none"><a href="/more" role="menuitem" onclick="event.preventDefault(); showMore(); updatePath('/more'); closeNavMenu();" aria-label="View more pages"><i class="fas fa-ellipsis-h" aria-hidden="true"></i> <span data-i18n="more">More</span></a></li>
|
||||
</ul>
|
||||
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
||||
<div class="nav-actions">
|
||||
<select id="localeSelect" onchange="setLocale(this.value)" style="padding: 0.35rem 0.5rem; border-radius: 6px; background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); font-size: 0.875rem;" aria-label="Language">
|
||||
<option value="en">EN</option>
|
||||
<option value="de">DE</option>
|
||||
@@ -912,6 +1063,64 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="smartSearchModal" class="smart-search-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="smartSearchTitle" style="display:none; position:fixed; inset:0; z-index:20000;">
|
||||
<div id="smartSearchBackdrop" style="position:absolute; inset:0; background: rgba(8, 15, 32, 0.62); backdrop-filter: blur(10px);"></div>
|
||||
<div style="position:relative; z-index:1; display:flex; justify-content:center; align-items:flex-start; padding: 6vh 1rem 2rem; min-height:100%;">
|
||||
<div style="width:min(920px, 100%); background: var(--card-bg, var(--light)); color: var(--text); border:1px solid var(--border); border-radius: 24px; box-shadow: 0 24px 80px rgba(0,0,0,0.35); overflow:hidden;">
|
||||
<div style="padding: 1.1rem 1.25rem; border-bottom:1px solid var(--border); display:flex; align-items:flex-start; justify-content:space-between; gap:1rem;">
|
||||
<div>
|
||||
<div id="smartSearchTitle" style="font-size:1.05rem; font-weight:700;">Search Explorer</div>
|
||||
<div style="margin-top:0.3rem; color:var(--text-light); font-size:0.9rem;">Type an address, transaction, block, token, or contract. Press Esc to close.</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem; align-items:center;">
|
||||
<span style="padding:0.3rem 0.55rem; border:1px solid var(--border); border-radius:999px; font-size:0.8rem; color:var(--text-light);">Esc</span>
|
||||
<button id="smartSearchCloseBtn" type="button" class="btn btn-secondary" style="padding:0.45rem 0.75rem;">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:1.1rem 1.25rem 1.25rem;">
|
||||
<div style="display:grid; grid-template-columns: 160px minmax(0, 1fr); gap:1rem; align-items:start;">
|
||||
<aside style="border:1px solid var(--border); border-radius:18px; background: linear-gradient(180deg, rgba(0,0,0,0.02), rgba(0,0,0,0.05)); padding:0.9rem; position:sticky; top:1rem;">
|
||||
<div style="font-size:0.78rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.65rem;">Quick Filters</div>
|
||||
<div style="display:grid; gap:0.5rem;">
|
||||
<button type="button" class="btn btn-secondary smart-search-scope-btn" data-scope="all" onclick="setSmartSearchScope('all'); updateSmartSearchPreview(document.getElementById('smartSearchInput') ? document.getElementById('smartSearchInput').value : '')" style="justify-content:flex-start; padding:0.62rem 0.75rem;">All</button>
|
||||
<button type="button" class="btn btn-secondary smart-search-scope-btn" data-scope="addresses" onclick="setSmartSearchScope('addresses'); updateSmartSearchPreview(document.getElementById('smartSearchInput') ? document.getElementById('smartSearchInput').value : '')" style="justify-content:flex-start; padding:0.62rem 0.75rem;">Addresses</button>
|
||||
<button type="button" class="btn btn-secondary smart-search-scope-btn" data-scope="tokens" onclick="setSmartSearchScope('tokens'); updateSmartSearchPreview(document.getElementById('smartSearchInput') ? document.getElementById('smartSearchInput').value : '')" style="justify-content:flex-start; padding:0.62rem 0.75rem;">Tokens</button>
|
||||
<button type="button" class="btn btn-secondary smart-search-scope-btn" data-scope="blocks" onclick="setSmartSearchScope('blocks'); updateSmartSearchPreview(document.getElementById('smartSearchInput') ? document.getElementById('smartSearchInput').value : '')" style="justify-content:flex-start; padding:0.62rem 0.75rem;">Blocks</button>
|
||||
<button type="button" class="btn btn-secondary smart-search-scope-btn" data-scope="transactions" onclick="setSmartSearchScope('transactions'); updateSmartSearchPreview(document.getElementById('smartSearchInput') ? document.getElementById('smartSearchInput').value : '')" style="justify-content:flex-start; padding:0.62rem 0.75rem;">Transactions</button>
|
||||
</div>
|
||||
<div style="margin-top:0.9rem; padding-top:0.9rem; border-top:1px solid var(--border);">
|
||||
<div style="font-size:0.78rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.55rem;">Mode</div>
|
||||
<div id="smartSearchDetected" class="badge" style="display:inline-block; background: var(--accent, #2563eb); color:#fff;">All</div>
|
||||
</div>
|
||||
<div style="margin-top:0.9rem; padding-top:0.9rem; border-top:1px solid var(--border);">
|
||||
<div style="font-size:0.78rem; text-transform:uppercase; letter-spacing:0.08em; color:var(--text-light); margin-bottom:0.55rem;">Keys</div>
|
||||
<div style="display:grid; gap:0.45rem; color:var(--text-light); font-size:0.84rem; line-height:1.45;">
|
||||
<div style="display:flex; justify-content:space-between; gap:0.75rem;"><span>Tab</span><span>Move focus</span></div>
|
||||
<div style="display:flex; justify-content:space-between; gap:0.75rem;"><span>Enter</span><span>Run search</span></div>
|
||||
<div style="display:flex; justify-content:space-between; gap:0.75rem;"><span>Esc</span><span>Close</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<main style="min-width:0;">
|
||||
<label for="smartSearchInput" class="sr-only">Search blockchain explorer</label>
|
||||
<div style="display:flex; gap:0.75rem; align-items:center; padding:0.9rem 1rem; border:1px solid var(--border); border-radius:16px; background: var(--light);">
|
||||
<i class="fas fa-search" aria-hidden="true" style="color:var(--text-light);"></i>
|
||||
<input type="text" id="smartSearchInput" placeholder="Search address, tx, block, token, contract..." aria-label="Search blockchain explorer" autocomplete="off" spellcheck="false" style="flex:1; border:none; background:transparent; outline:none; color:var(--text); font-size:1rem;">
|
||||
<button id="smartSearchSubmitBtn" type="button" class="btn btn-primary" style="padding:0.5rem 0.85rem;">Search</button>
|
||||
</div>
|
||||
<div style="display:flex; flex-wrap:wrap; gap:0.5rem; margin-top:0.85rem; align-items:center;">
|
||||
<span style="color:var(--text-light); font-size:0.88rem;">Recent searches, trending tokens, and scoped matches appear below.</span>
|
||||
</div>
|
||||
<div id="smartSearchPreview" style="margin-top:1rem; display:grid; gap:1rem;">
|
||||
<div class="loading"><i class="fas fa-spinner"></i> Waiting for a query...</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="explorerLiveRegion" aria-live="polite" aria-atomic="true" class="sr-only" role="status"></div>
|
||||
<div class="container" id="mainContent">
|
||||
<!-- Home View -->
|
||||
@@ -920,22 +1129,26 @@
|
||||
<!-- Stats loaded dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="card" id="gasNetworkCard" style="margin-bottom: 1rem;">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title"><i class="fas fa-gas-pump"></i> Gas & Network</h2>
|
||||
<button type="button" class="btn btn-secondary" onclick="loadGasAndNetworkStats()" aria-label="Refresh gas and network"><i class="fas fa-sync-alt"></i> Refresh</button>
|
||||
</div>
|
||||
<div id="gasNetworkContent">
|
||||
<div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-light); margin-bottom: 0.25rem;">Current base fee</div>
|
||||
<div id="gasCurrentValue" style="font-size: 1.5rem; font-weight: 600;">—</div>
|
||||
<div style="margin-top: 0.75rem; font-size: 0.875rem;">TPS: <span id="gasTpsValue">—</span></div>
|
||||
<div style="font-size: 0.875rem;">Block time: <span id="gasBlockTimeValue">—</span></div>
|
||||
<div style="font-size: 0.875rem; margin-top: 0.25rem;">Failed (recent): <span id="gasFailedRateValue">—</span></div>
|
||||
<div class="card gas-network-card" id="gasNetworkCard">
|
||||
<div class="card-header gas-network-header">
|
||||
<div class="gas-network-row">
|
||||
<h2 class="card-title"><i class="fas fa-gas-pump"></i> Gas & Network</h2>
|
||||
<button type="button" class="btn btn-secondary" onclick="loadGasAndNetworkStats()" aria-label="Refresh gas and network"><i class="fas fa-sync-alt"></i> Refresh</button>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-light); margin-bottom: 0.5rem;">Gas history (last 10 blocks)</div>
|
||||
<div id="gasHistoryBars" style="display: flex; align-items: flex-end; gap: 4px; height: 60px;"></div>
|
||||
<div class="gas-network-row">
|
||||
<div class="gas-network-compact">
|
||||
<div class="gas-network-pill"><span class="gas-network-subtle">Base fee</span> <strong id="gasCurrentValue">—</strong></div>
|
||||
<div class="gas-network-pill"><span class="gas-network-subtle">Block time</span> <strong id="gasBlockTimeValue">—</strong></div>
|
||||
<div class="gas-network-pill"><span class="gas-network-subtle">TPS</span> <strong id="gasTpsValue">—</strong></div>
|
||||
<div class="gas-network-pill"><span class="gas-network-subtle">Failed</span> <strong id="gasFailedRateValue">—</strong></div>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap;">
|
||||
<div>
|
||||
<div style="font-size: 0.78rem; color: var(--text-light); margin-bottom: 0.25rem;">History (10 blocks)</div>
|
||||
<div id="gasHistoryBars" class="gas-network-spark"></div>
|
||||
</div>
|
||||
<div id="gasNetworkSummary" class="gas-network-subtle">Live chain health for Chain 138.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1178,11 +1391,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="addressesView" class="detail-view">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">All Addresses</h2>
|
||||
</div>
|
||||
<div id="addressesList">
|
||||
<div class="loading"><i class="fas fa-spinner"></i> Loading addresses...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="addressDetailView" class="detail-view">
|
||||
<div class="breadcrumb" id="addressDetailBreadcrumb"></div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back to home page"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
|
||||
<button class="btn btn-secondary" onclick="showAddresses()" aria-label="Go back to addresses list"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
|
||||
<h2 class="card-title">Address Details</h2>
|
||||
</div>
|
||||
<div id="addressDetail"></div>
|
||||
@@ -1252,6 +1476,10 @@
|
||||
<div class="card-header">
|
||||
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back to home page"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
|
||||
<h2 class="card-title"><i class="fas fa-water"></i> Pools</h2>
|
||||
<div style="display:flex; gap:0.5rem; margin-left:auto; flex-wrap:wrap;">
|
||||
<button type="button" class="btn btn-primary" onclick="exportPoolsCSV()" style="padding: 0.5rem 1rem;"><i class="fas fa-file-csv"></i> Export CSV</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="exportPoolsJSON()" style="padding: 0.5rem 1rem;"><i class="fas fa-download"></i> Export JSON</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="poolsContent">
|
||||
<div class="loading"><i class="fas fa-spinner"></i> Loading pools...</div>
|
||||
@@ -1259,6 +1487,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="liquidityView" class="detail-view">
|
||||
<div class="breadcrumb" id="liquidityBreadcrumb"><a href="/home">Home</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">Liquidity</span></div>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<button class="btn btn-secondary" onclick="showHome()" aria-label="Go back to home page"><i class="fas fa-arrow-left" aria-hidden="true"></i> Back</button>
|
||||
<h2 class="card-title"><i class="fas fa-wave-square"></i> Liquidity Access</h2>
|
||||
<div style="display:flex; gap:0.5rem; margin-left:auto; flex-wrap:wrap;">
|
||||
<button type="button" class="btn btn-primary" onclick="renderLiquidityAccessView()" style="padding: 0.5rem 1rem;"><i class="fas fa-sync-alt"></i> Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="liquidityContent">
|
||||
<div class="loading"><i class="fas fa-spinner"></i> Loading liquidity access...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="moreView" class="detail-view">
|
||||
<div class="breadcrumb" id="moreBreadcrumb"><a href="/home">Home</a><span class="breadcrumb-separator">/</span><span class="breadcrumb-current">More</span></div>
|
||||
<div class="card">
|
||||
@@ -1297,6 +1541,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/explorer-spa.js?v=16"></script>
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<div class="site-footer-grid">
|
||||
<div>
|
||||
<div style="font-size: 1.05rem; font-weight: 700; margin-bottom: 0.5rem;">SolaceScanScout</div>
|
||||
<div class="site-footer-note">
|
||||
Built on Blockscout foundations and Solace Bank Group PLC frontend development.
|
||||
Explorer data, block indexing, and public chain visibility are powered by Blockscout,
|
||||
Chain 138 RPC, and the MetaMask Snap companion.
|
||||
</div>
|
||||
<div class="site-footer-note" style="margin-top: 0.8rem;">
|
||||
© 2026 Solace Bank Group PLC. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="site-footer-title">Documentation</div>
|
||||
<div class="site-footer-links">
|
||||
<a href="/docs.html">Docs landing page</a>
|
||||
<a href="/liquidity">Liquidity access</a>
|
||||
<a href="/privacy.html">Privacy Policy</a>
|
||||
<a href="/terms.html">Terms of Service</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="site-footer-title">Acknowledgments & Contact</div>
|
||||
<div class="site-footer-links">
|
||||
<a href="/acknowledgments.html">Acknowledgments</a>
|
||||
<a href="mailto:support@d-bis.org">support@d-bis.org</a>
|
||||
<a href="/snap/">MetaMask Snap companion</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="/explorer-spa.js?v=28"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
39
frontend/public/privacy.html
Normal file
39
frontend/public/privacy.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Privacy Policy | SolaceScanScout</title>
|
||||
<meta name="description" content="Privacy policy for the SolaceScanScout explorer.">
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; }
|
||||
.shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
|
||||
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 20px; padding: 1.5rem; box-shadow: 0 18px 60px rgba(15,23,42,0.12); }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.muted { color: #64748b; }
|
||||
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 1rem; }
|
||||
.brand { color: #fff; font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScanScout Privacy Policy</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0;">Privacy Policy</h1>
|
||||
<p class="muted">Last updated: 2026-03-25</p>
|
||||
<p>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.</p>
|
||||
<ul>
|
||||
<li>We may store theme preference, locale, recent searches, and similar local UI settings in your browser.</li>
|
||||
<li>When you use wallet features or the Snap companion, the app may interact with your wallet provider to complete the request you initiate.</li>
|
||||
<li>Explorer queries are sent to the configured blockchain APIs and RPC endpoints so the site can display blocks, transactions, addresses, and related data.</li>
|
||||
<li>We do not sell personal data. We also do not intentionally track users with advertising cookies on this explorer.</li>
|
||||
</ul>
|
||||
<p>If you have privacy questions, contact <a href="mailto:support@d-bis.org">support@d-bis.org</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
39
frontend/public/terms.html
Normal file
39
frontend/public/terms.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Terms of Service | SolaceScanScout</title>
|
||||
<meta name="description" content="Terms of service for the SolaceScanScout explorer.">
|
||||
<style>
|
||||
body { margin: 0; font-family: Arial, Helvetica, sans-serif; background: linear-gradient(180deg, #0f172a 0%, #111827 45%, #f8fafc 46%, #ffffff 100%); color: #0f172a; }
|
||||
.shell { max-width: 980px; margin: 0 auto; padding: 2rem 1rem 3rem; }
|
||||
.card { background: #fff; border: 1px solid #e5e7eb; border-radius: 20px; padding: 1.5rem; box-shadow: 0 18px 60px rgba(15,23,42,0.12); }
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.muted { color: #64748b; }
|
||||
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; margin-bottom: 1rem; }
|
||||
.brand { color: #fff; font-weight: 700; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<div class="topbar">
|
||||
<div class="brand">SolaceScanScout Terms of Service</div>
|
||||
<a href="/">Back to explorer</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h1 style="margin-top:0;">Terms of Service</h1>
|
||||
<p class="muted">Last updated: 2026-03-25</p>
|
||||
<p>This explorer is provided for informational and operational purposes. By using it, you agree that:</p>
|
||||
<ul>
|
||||
<li>Blockchain data may be delayed, incomplete, or temporarily unavailable.</li>
|
||||
<li>You are responsible for verifying addresses, transactions, and contract details before acting on them.</li>
|
||||
<li>We may update features, endpoints, and policies as the explorer evolves.</li>
|
||||
<li>The explorer is not legal, financial, or tax advice.</li>
|
||||
</ul>
|
||||
<p>For service questions, contact <a href="mailto:support@d-bis.org">support@d-bis.org</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
257
frontend/src/app/liquidity/page.tsx
Normal file
257
frontend/src/app/liquidity/page.tsx
Normal file
@@ -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 (
|
||||
<main className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8 max-w-4xl">
|
||||
<div className="mb-3 inline-flex rounded-full border border-amber-200 bg-amber-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">
|
||||
Chain 138 Liquidity Access
|
||||
</div>
|
||||
<h1 className="mb-3 text-4xl font-bold text-gray-900 dark:text-white">
|
||||
Public liquidity, route discovery, and execution access points
|
||||
</h1>
|
||||
<p className="text-lg leading-8 text-gray-600 dark:text-gray-400">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid gap-4 md:grid-cols-3">
|
||||
<Card>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Live public pools</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">6</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Verified public DODO PMM pools on Chain 138.
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Public access path</div>
|
||||
<div className="mt-2 text-lg font-bold text-gray-900 dark:text-white">/token-aggregation/api/v1</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Explorer-hosted proxy path for route, quote, and reporting APIs.
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Partner status</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">Fallback Ready</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Mainnet stable bridge routing is live; 1inch, 0x, and LiFi templates remain available for partner integrations, with internal fallback for unsupported Chain 138 execution.
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
|
||||
<Card title="Live Pool Snapshot">
|
||||
<div className="space-y-4">
|
||||
{livePools.map((pool) => (
|
||||
<div
|
||||
key={pool.poolAddress}
|
||||
className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40"
|
||||
>
|
||||
<div className="flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">{pool.pair}</div>
|
||||
<div className="mt-1 break-all text-xs text-gray-500 dark:text-gray-400">
|
||||
Pool: {pool.poolAddress}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Reserves: {pool.reserves}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="What Integrators Need To Know">
|
||||
<div className="space-y-3 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{routeHighlights.map((item) => (
|
||||
<p key={item}>{item}</p>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mb-8">
|
||||
<Card title="Explorer Access Points">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{publicEndpoints.map((endpoint) => (
|
||||
<a
|
||||
key={endpoint.href}
|
||||
href={endpoint.href}
|
||||
className="rounded-2xl border border-gray-200 bg-white p-5 transition hover:border-primary-400 hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">{endpoint.name}</div>
|
||||
<span className="rounded-full bg-primary-50 px-2.5 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700 dark:bg-primary-900/30 dark:text-primary-300">
|
||||
{endpoint.method}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 break-all rounded-xl bg-gray-50 p-3 text-xs text-gray-700 dark:bg-gray-900 dark:text-gray-300">
|
||||
{endpoint.href}
|
||||
</div>
|
||||
<div className="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-400">{endpoint.notes}</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
||||
<Card title="Quick Request Examples">
|
||||
<div className="space-y-4">
|
||||
{requestExamples.map((example) => (
|
||||
<div key={example.title} className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="mb-2 text-sm font-semibold text-gray-900 dark:text-white">{example.title}</div>
|
||||
<code className="block break-all text-xs leading-6 text-gray-700 dark:text-gray-300">
|
||||
{example.code}
|
||||
</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Related Explorer Tools">
|
||||
<div className="space-y-4 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
<p>
|
||||
Use the wallet page for network onboarding and the explorer token list URL, then use this
|
||||
page for route and execution discovery.
|
||||
</p>
|
||||
<p>
|
||||
The route APIs complement the existing route decision tree and market-data APIs already
|
||||
proxied through the explorer.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/wallet"
|
||||
className="rounded-full bg-primary-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-primary-700"
|
||||
>
|
||||
Open wallet tools
|
||||
</Link>
|
||||
<a
|
||||
href={`${publicApiBase}/routes/tree?chainId=138&amountIn=1000000`}
|
||||
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
|
||||
>
|
||||
Route tree API
|
||||
</a>
|
||||
<a
|
||||
href="/docs.html"
|
||||
className="rounded-full border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:border-primary-400 hover:text-primary-700 dark:border-gray-600 dark:text-gray-300 dark:hover:text-primary-300"
|
||||
>
|
||||
Explorer docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -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() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="mt-4">
|
||||
<Link href="/blocks" className="text-primary-600 hover:underline">
|
||||
View all blocks →
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<Card title="Liquidity & Routes">
|
||||
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
Explore the public Chain 138 DODO PMM liquidity mesh, the canonical route matrix, and the
|
||||
partner payload endpoints exposed through the explorer.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Link href="/liquidity" className="text-primary-600 hover:underline">
|
||||
Open liquidity access →
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
<Card title="Wallet & Token Discovery">
|
||||
<p className="text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
Add Chain 138, Ethereum Mainnet, and ALL Mainnet to MetaMask, then use the explorer token
|
||||
list URL so supported tokens appear automatically.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<Link href="/wallet" className="text-primary-600 hover:underline">
|
||||
Open wallet tools →
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
</p>
|
||||
<AddToMetaMask />
|
||||
<div className="mt-6 rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400">
|
||||
Need swap and liquidity discovery too? Visit the{' '}
|
||||
<Link href="/liquidity" className="font-medium text-primary-600 hover:underline dark:text-primary-400">
|
||||
Liquidity Access
|
||||
</Link>{' '}
|
||||
page for live Chain 138 pools, route matrix links, partner payload templates, and the internal fallback execution plan endpoints.
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
65
frontend/src/components/common/Footer.tsx
Normal file
65
frontend/src/components/common/Footer.tsx
Normal file
@@ -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 (
|
||||
<footer className="mt-auto border-t border-gray-200 dark:border-gray-700 bg-white/90 dark:bg-gray-900/90 backdrop-blur">
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="grid gap-6 md:grid-cols-[1.5fr_1fr_1fr]">
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
SolaceScanScout
|
||||
</div>
|
||||
<p className="max-w-xl text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
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.
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-500">
|
||||
© {year} Solace Bank Group PLC. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Resources
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm">
|
||||
<li><a className={footerLinkClass} href="/docs.html">Documentation</a></li>
|
||||
<li><a className={footerLinkClass} href="/liquidity">Liquidity Access</a></li>
|
||||
<li><a className={footerLinkClass} href="/privacy.html">Privacy Policy</a></li>
|
||||
<li><a className={footerLinkClass} href="/terms.html">Terms of Service</a></li>
|
||||
<li><a className={footerLinkClass} href="/acknowledgments.html">Acknowledgments</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Contact
|
||||
</div>
|
||||
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<p>
|
||||
Support:{' '}
|
||||
<a className={footerLinkClass} href="mailto:support@d-bis.org">
|
||||
support@d-bis.org
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Snap site:{' '}
|
||||
<a className={footerLinkClass} href="https://explorer.d-bis.org/snap/">
|
||||
explorer.d-bis.org/snap/
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-xs leading-5 text-gray-500 dark:text-gray-500">
|
||||
Questions about the explorer, chain metadata, route discovery, or liquidity access
|
||||
can be sent to the support mailbox above.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
@@ -91,9 +91,21 @@ export default function Navbar() {
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center gap-4 md:gap-8">
|
||||
<Link href="/" className="text-xl font-bold text-primary-600 dark:text-primary-400 flex flex-col" onClick={() => setMobileMenuOpen(false)}>
|
||||
<span>SolaceScanScout</span>
|
||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">The Defi Oracle Meta Explorer</span>
|
||||
<Link
|
||||
href="/"
|
||||
className="group inline-flex flex-col rounded-xl px-3 py-2 text-xl font-bold text-primary-600 transition-colors hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-gray-700/70"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label="Go to explorer home"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="inline-flex h-8 w-8 items-center justify-center rounded-lg bg-primary-600 text-white shadow-sm transition-transform group-hover:-translate-y-0.5 dark:bg-primary-500">
|
||||
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||
<path d="M12 2.5 3.5 6.5v11l8.5 4 8.5-4v-11L12 2.5Zm0 2.24 6.44 3.03L12 10.8 5.56 7.77 12 4.74Zm-7 4.63L11 13.1v6.07L5 16.4V9.37Zm9 9.8v-6.07l6-2.92v6.03l-6 2.96Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<span>SolaceScanScout</span>
|
||||
</span>
|
||||
<span className="mt-0.5 text-xs font-normal text-gray-500 transition-colors group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-200">The Defi Oracle Meta Explorer</span>
|
||||
</Link>
|
||||
<div className="hidden md:flex items-center gap-1">
|
||||
<NavDropdown
|
||||
@@ -110,6 +122,7 @@ export default function Navbar() {
|
||||
>
|
||||
<DropdownItem href="/search">Search</DropdownItem>
|
||||
<DropdownItem href="/wallet">Wallet</DropdownItem>
|
||||
<DropdownItem href="/liquidity">Liquidity</DropdownItem>
|
||||
</NavDropdown>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,6 +167,7 @@ export default function Navbar() {
|
||||
<ul className="pl-4 mt-1 space-y-0.5">
|
||||
<li><Link href="/search" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Search</Link></li>
|
||||
<li><Link href="/wallet" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Wallet</Link></li>
|
||||
<li><Link href="/liquidity" className={`block px-3 py-2 rounded-md ${navLink}`} onClick={() => setMobileMenuOpen(false)}>Liquidity</Link></li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
93
scripts/check-explorer-health.sh
Executable file
93
scripts/check-explorer-health.sh
Executable file
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user