Files
explorer-monorepo/backend/api/rest/config.go
defiQUG bdae5a9f6e 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
2026-04-07 23:22:12 -07:00

187 lines
5.3 KiB
Go

package rest
import (
"crypto/sha256"
_ "embed"
"encoding/hex"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
//go:embed config/metamask/DUAL_CHAIN_NETWORKS.json
var dualChainNetworksJSON []byte
//go:embed config/metamask/DUAL_CHAIN_TOKEN_LIST.tokenlist.json
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 {
w.Header().Set("Allow", "GET")
writeMethodNotAllowed(w)
return
}
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).
func (s *Server) handleConfigTokenList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
writeMethodNotAllowed(w)
return
}
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).
func (s *Server) handleConfigCapabilities(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
writeMethodNotAllowed(w)
return
}
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")
}