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:
defiQUG
2026-04-07 23:22:12 -07:00
parent d931be8e19
commit 6eef6b07f6
224 changed files with 19671 additions and 3291 deletions

View File

@@ -1,13 +1,19 @@
package gateway
import (
"crypto/subtle"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"sync"
"time"
httperrors "github.com/explorer/backend/libs/go-http-errors"
httpmiddleware "github.com/explorer/backend/libs/go-http-middleware"
)
// Gateway represents the API gateway
@@ -64,7 +70,9 @@ func (g *Gateway) handleRequest(proxy *httputil.ReverseProxy) http.HandlerFunc {
}
// Add headers
r.Header.Set("X-Forwarded-For", r.RemoteAddr)
if clientIP := httpmiddleware.ClientIP(r); clientIP != "" {
r.Header.Set("X-Forwarded-For", clientIP)
}
if apiKey := g.auth.GetAPIKey(r); apiKey != "" {
r.Header.Set("X-API-Key", apiKey)
}
@@ -92,14 +100,17 @@ func (g *Gateway) addSecurityHeaders(w http.ResponseWriter) {
// RateLimiter handles rate limiting
type RateLimiter struct {
// Simple in-memory rate limiter (should use Redis in production)
mu sync.Mutex
limits map[string]*limitEntry
}
type limitEntry struct {
count int
resetAt int64
resetAt time.Time
}
const gatewayRequestsPerMinute = 120
func NewRateLimiter() *RateLimiter {
return &RateLimiter{
limits: make(map[string]*limitEntry),
@@ -107,26 +118,62 @@ func NewRateLimiter() *RateLimiter {
}
func (rl *RateLimiter) Allow(r *http.Request) bool {
_ = r.RemoteAddr // Will be used in production for per-IP limiting
// In production, use Redis with token bucket algorithm
// For now, simple per-IP limiting
return true // Simplified - implement proper rate limiting
clientIP := httpmiddleware.ClientIP(r)
if clientIP == "" {
clientIP = r.RemoteAddr
}
now := time.Now()
rl.mu.Lock()
defer rl.mu.Unlock()
entry, ok := rl.limits[clientIP]
if !ok || now.After(entry.resetAt) {
rl.limits[clientIP] = &limitEntry{
count: 1,
resetAt: now.Add(time.Minute),
}
return true
}
if entry.count >= gatewayRequestsPerMinute {
return false
}
entry.count++
return true
}
// AuthMiddleware handles authentication
type AuthMiddleware struct {
// In production, validate against database
allowAnonymous bool
apiKeys []string
}
func NewAuthMiddleware() *AuthMiddleware {
return &AuthMiddleware{}
return &AuthMiddleware{
allowAnonymous: parseBoolEnv("GATEWAY_ALLOW_ANONYMOUS"),
apiKeys: splitNonEmptyEnv("GATEWAY_API_KEYS"),
}
}
func (am *AuthMiddleware) Authenticate(r *http.Request) bool {
// Allow anonymous access for now
// In production, validate API key
apiKey := am.GetAPIKey(r)
return apiKey != "" || true // Allow anonymous for MVP
if apiKey == "" {
return am.allowAnonymous
}
if len(am.apiKeys) == 0 {
return am.allowAnonymous
}
for _, allowedKey := range am.apiKeys {
if subtle.ConstantTimeCompare([]byte(apiKey), []byte(allowedKey)) == 1 {
return true
}
}
return false
}
func (am *AuthMiddleware) GetAPIKey(r *http.Request) string {
@@ -140,3 +187,29 @@ func (am *AuthMiddleware) GetAPIKey(r *http.Request) string {
}
return ""
}
func parseBoolEnv(key string) bool {
value := strings.TrimSpace(os.Getenv(key))
return strings.EqualFold(value, "1") ||
strings.EqualFold(value, "true") ||
strings.EqualFold(value, "yes") ||
strings.EqualFold(value, "on")
}
func splitNonEmptyEnv(key string) []string {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
values := make([]string, 0, len(parts))
for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
values = append(values, trimmed)
}
}
return values
}