Add explorer liquidity access and live route proxies

This commit is contained in:
defiQUG
2026-03-27 12:02:36 -07:00
parent d02ee71cf6
commit 2491336b8e
17 changed files with 2746 additions and 125 deletions

View 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)
}

View File

@@ -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)

View 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 ""
}

View 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
View 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

View File

@@ -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 &amp; 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 &amp; 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>

View 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>

View 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>

View 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>
)
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View 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>
)
}

View File

@@ -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>

View 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

View File

@@ -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

View File

@@ -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 ""