feat: explorer API, wallet, CCIP scripts, and config refresh
- Backend REST/gateway/track routes, analytics, Blockscout proxy paths. - Frontend wallet and liquidity surfaces; MetaMask token list alignment. - Deployment docs, verification scripts, address inventory updates. Check: go build ./... under backend/ (pass). Made-with: Cursor
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed config/metamask/DUAL_CHAIN_NETWORKS.json
|
||||
@@ -14,6 +20,111 @@ var dualChainTokenListJSON []byte
|
||||
//go:embed config/metamask/CHAIN138_RPC_CAPABILITIES.json
|
||||
var chain138RPCCapabilitiesJSON []byte
|
||||
|
||||
type configPayload struct {
|
||||
body []byte
|
||||
source string
|
||||
modTime time.Time
|
||||
}
|
||||
|
||||
func uniqueConfigPaths(paths []string) []string {
|
||||
seen := make(map[string]struct{}, len(paths))
|
||||
out := make([]string, 0, len(paths))
|
||||
for _, candidate := range paths {
|
||||
trimmed := strings.TrimSpace(candidate)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[trimmed]; ok {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildConfigCandidates(envKeys []string, defaults []string) []string {
|
||||
candidates := make([]string, 0, len(envKeys)+len(defaults)*4)
|
||||
for _, key := range envKeys {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
candidates = append(candidates, value)
|
||||
}
|
||||
}
|
||||
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
for _, rel := range defaults {
|
||||
if filepath.IsAbs(rel) {
|
||||
candidates = append(candidates, rel)
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, filepath.Join(cwd, rel))
|
||||
candidates = append(candidates, rel)
|
||||
}
|
||||
}
|
||||
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
exeDir := filepath.Dir(exe)
|
||||
for _, rel := range defaults {
|
||||
if filepath.IsAbs(rel) {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates,
|
||||
filepath.Join(exeDir, rel),
|
||||
filepath.Join(exeDir, "..", rel),
|
||||
filepath.Join(exeDir, "..", "..", rel),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueConfigPaths(candidates)
|
||||
}
|
||||
|
||||
func loadConfigPayload(envKeys []string, defaults []string, embedded []byte) configPayload {
|
||||
for _, candidate := range buildConfigCandidates(envKeys, defaults) {
|
||||
body, err := os.ReadFile(candidate)
|
||||
if err != nil || len(body) == 0 {
|
||||
continue
|
||||
}
|
||||
payload := configPayload{
|
||||
body: body,
|
||||
source: "runtime-file",
|
||||
}
|
||||
if info, statErr := os.Stat(candidate); statErr == nil {
|
||||
payload.modTime = info.ModTime().UTC()
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
return configPayload{
|
||||
body: embedded,
|
||||
source: "embedded",
|
||||
}
|
||||
}
|
||||
|
||||
func payloadETag(body []byte) string {
|
||||
sum := sha256.Sum256(body)
|
||||
return `W/"` + hex.EncodeToString(sum[:]) + `"`
|
||||
}
|
||||
|
||||
func serveJSONConfig(w http.ResponseWriter, r *http.Request, payload configPayload, cacheControl string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", cacheControl)
|
||||
w.Header().Set("X-Config-Source", payload.source)
|
||||
|
||||
etag := payloadETag(payload.body)
|
||||
w.Header().Set("ETag", etag)
|
||||
if !payload.modTime.IsZero() {
|
||||
w.Header().Set("Last-Modified", payload.modTime.Format(http.TimeFormat))
|
||||
}
|
||||
|
||||
if match := strings.TrimSpace(r.Header.Get("If-None-Match")); match != "" && strings.Contains(match, etag) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write(payload.body)
|
||||
}
|
||||
|
||||
// handleConfigNetworks serves GET /api/config/networks (Chain 138 + Ethereum Mainnet params for wallet_addEthereumChain).
|
||||
func (s *Server) handleConfigNetworks(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
@@ -21,9 +132,17 @@ func (s *Server) handleConfigNetworks(w http.ResponseWriter, r *http.Request) {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
w.Write(dualChainNetworksJSON)
|
||||
payload := loadConfigPayload(
|
||||
[]string{"CONFIG_NETWORKS_JSON_PATH", "NETWORKS_CONFIG_JSON_PATH"},
|
||||
[]string{
|
||||
"explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json",
|
||||
"backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json",
|
||||
"api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json",
|
||||
"config/metamask/DUAL_CHAIN_NETWORKS.json",
|
||||
},
|
||||
dualChainNetworksJSON,
|
||||
)
|
||||
serveJSONConfig(w, r, payload, "public, max-age=0, must-revalidate")
|
||||
}
|
||||
|
||||
// handleConfigTokenList serves GET /api/config/token-list (Uniswap token list format for MetaMask).
|
||||
@@ -33,9 +152,17 @@ func (s *Server) handleConfigTokenList(w http.ResponseWriter, r *http.Request) {
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
w.Write(dualChainTokenListJSON)
|
||||
payload := loadConfigPayload(
|
||||
[]string{"CONFIG_TOKEN_LIST_JSON_PATH", "TOKEN_LIST_CONFIG_JSON_PATH"},
|
||||
[]string{
|
||||
"explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_TOKEN_LIST.tokenlist.json",
|
||||
"backend/api/rest/config/metamask/DUAL_CHAIN_TOKEN_LIST.tokenlist.json",
|
||||
"api/rest/config/metamask/DUAL_CHAIN_TOKEN_LIST.tokenlist.json",
|
||||
"config/metamask/DUAL_CHAIN_TOKEN_LIST.tokenlist.json",
|
||||
},
|
||||
dualChainTokenListJSON,
|
||||
)
|
||||
serveJSONConfig(w, r, payload, "public, max-age=0, must-revalidate")
|
||||
}
|
||||
|
||||
// handleConfigCapabilities serves GET /api/config/capabilities (Chain 138 wallet/RPC capability matrix).
|
||||
@@ -45,7 +172,15 @@ func (s *Server) handleConfigCapabilities(w http.ResponseWriter, r *http.Request
|
||||
writeMethodNotAllowed(w)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=900")
|
||||
w.Write(chain138RPCCapabilitiesJSON)
|
||||
payload := loadConfigPayload(
|
||||
[]string{"CONFIG_CAPABILITIES_JSON_PATH", "RPC_CAPABILITIES_JSON_PATH"},
|
||||
[]string{
|
||||
"explorer-monorepo/backend/api/rest/config/metamask/CHAIN138_RPC_CAPABILITIES.json",
|
||||
"backend/api/rest/config/metamask/CHAIN138_RPC_CAPABILITIES.json",
|
||||
"api/rest/config/metamask/CHAIN138_RPC_CAPABILITIES.json",
|
||||
"config/metamask/CHAIN138_RPC_CAPABILITIES.json",
|
||||
},
|
||||
chain138RPCCapabilitiesJSON,
|
||||
)
|
||||
serveJSONConfig(w, r, payload, "public, max-age=0, must-revalidate")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user