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