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
}

View File

@@ -0,0 +1,78 @@
package gateway
import (
"net/http/httptest"
"testing"
"time"
)
func TestAuthMiddlewareRejectsAnonymousByDefault(t *testing.T) {
t.Setenv("GATEWAY_ALLOW_ANONYMOUS", "")
t.Setenv("GATEWAY_API_KEYS", "")
auth := NewAuthMiddleware()
req := httptest.NewRequest("GET", "http://example.com", nil)
if auth.Authenticate(req) {
t.Fatal("expected anonymous request to be rejected by default")
}
}
func TestAuthMiddlewareAllowsConfiguredAPIKey(t *testing.T) {
t.Setenv("GATEWAY_ALLOW_ANONYMOUS", "")
t.Setenv("GATEWAY_API_KEYS", "alpha,beta")
auth := NewAuthMiddleware()
req := httptest.NewRequest("GET", "http://example.com", nil)
req.Header.Set("X-API-Key", "beta")
if !auth.Authenticate(req) {
t.Fatal("expected configured API key to be accepted")
}
}
func TestAuthMiddlewareAllowsAnonymousOnlyWhenEnabled(t *testing.T) {
t.Setenv("GATEWAY_ALLOW_ANONYMOUS", "true")
t.Setenv("GATEWAY_API_KEYS", "")
auth := NewAuthMiddleware()
req := httptest.NewRequest("GET", "http://example.com", nil)
if !auth.Authenticate(req) {
t.Fatal("expected anonymous request to be accepted when explicitly enabled")
}
}
func TestRateLimiterBlocksAfterWindowBudget(t *testing.T) {
limiter := NewRateLimiter()
req := httptest.NewRequest("GET", "http://example.com", nil)
req.RemoteAddr = "203.0.113.10:1234"
for i := 0; i < gatewayRequestsPerMinute; i++ {
if !limiter.Allow(req) {
t.Fatalf("expected request %d to pass", i+1)
}
}
if limiter.Allow(req) {
t.Fatal("expected request over the per-minute budget to be rejected")
}
}
func TestRateLimiterResetsAfterWindow(t *testing.T) {
limiter := NewRateLimiter()
req := httptest.NewRequest("GET", "http://example.com", nil)
req.RemoteAddr = "203.0.113.11:1234"
if !limiter.Allow(req) {
t.Fatal("expected first request to pass")
}
limiter.mu.Lock()
limiter.limits["203.0.113.11"].resetAt = time.Now().Add(-time.Second)
limiter.mu.Unlock()
if !limiter.Allow(req) {
t.Fatal("expected limiter window to reset")
}
}