- 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
187 lines
5.3 KiB
Go
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")
|
|
}
|