Files
mpc/pkg/api/ratelimit.go
T
zeekay f110e06800 feat: MPC wallet dashboard - full-stack implementation
Complete MPC wallet management platform rivaling Fireblocks/Utila/Fordefi:

Backend (Go):
- REST API with 60+ endpoints on chi router (pkg/api/)
- PostgreSQL schema with 13 tables (pkg/db/)
- JWT + MFA auth, bcrypt passwords, API key management
- Policy engine (deny/approve/require_approval)
- Transaction signing flow with approval workflow
- Subscription scheduler for recurring payments
- Payment requests with public pay pages
- Safe + ERC-4337 smart wallet integration (pkg/smart/)
- Rate limiting, webhook delivery (HMAC-SHA256), audit logging
- 27 passing tests (auth, policy engine, middleware)

Frontend (Next.js 14):
- 27 routes with monochrome dark theme (Tailwind + Geist fonts)
- Full API client with JWT auto-refresh
- React Query hooks for all resources
- Pages: dashboard, vaults, wallets, transactions, policies,
  whitelist, team, settings, API keys, webhooks, audit,
  subscriptions, payments, smart wallets

Infrastructure:
- Dockerfiles for API (Go) and dashboard (Next.js standalone)
- compose.yml for local development (PostgreSQL + API + dashboard)
- K8s manifests for mpc.lux.network + cloud.lux.network
- GitHub Actions deploy workflow (build → push GHCR → kubectl apply)
- TLS via cert-manager, Traefik IngressRoute for both domains
2026-02-28 13:27:34 -08:00

109 lines
2.6 KiB
Go

package api
import (
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
// visitor tracks request count for a single IP within a time window.
type visitor struct {
count atomic.Int64
lastSeen atomic.Int64 // unix seconds
}
// RateLimiter implements per-IP rate limiting using a sync.Map.
type RateLimiter struct {
visitors sync.Map // map[string]*visitor
limit int64
windowSecs int64
}
// NewRateLimiter creates a rate limiter with the given requests-per-minute limit.
// It starts a background goroutine that evicts stale entries every 60 seconds.
func NewRateLimiter(requestsPerMinute int) *RateLimiter {
rl := &RateLimiter{
limit: int64(requestsPerMinute),
windowSecs: 60,
}
go rl.cleanup()
return rl
}
// allow returns true if the IP has not exceeded the rate limit.
func (rl *RateLimiter) allow(ip string) bool {
now := time.Now().Unix()
val, loaded := rl.visitors.LoadOrStore(ip, &visitor{})
v := val.(*visitor)
if !loaded {
v.count.Store(1)
v.lastSeen.Store(now)
return true
}
last := v.lastSeen.Load()
if now-last >= rl.windowSecs {
// Window expired; reset.
v.count.Store(1)
v.lastSeen.Store(now)
return true
}
c := v.count.Add(1)
v.lastSeen.Store(now)
return c <= rl.limit
}
// cleanup removes entries not seen in the last 2 windows.
func (rl *RateLimiter) cleanup() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for range ticker.C {
cutoff := time.Now().Unix() - 2*rl.windowSecs
rl.visitors.Range(func(key, val any) bool {
v := val.(*visitor)
if v.lastSeen.Load() < cutoff {
rl.visitors.Delete(key)
}
return true
})
}
}
// clientIP extracts the client IP from X-Forwarded-For or RemoteAddr.
func clientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
// First IP in the chain is the original client.
if ip, _, ok := strings.Cut(xff, ","); ok {
return strings.TrimSpace(ip)
}
return strings.TrimSpace(xff)
}
// Strip port from RemoteAddr.
addr := r.RemoteAddr
if idx := strings.LastIndex(addr, ":"); idx != -1 {
return addr[:idx]
}
return addr
}
// RateLimitMiddleware returns chi-compatible middleware that limits requests per
// IP to requestsPerMinute within a rolling 60-second window.
func RateLimitMiddleware(requestsPerMinute int) func(http.Handler) http.Handler {
rl := NewRateLimiter(requestsPerMinute)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := clientIP(r)
if !rl.allow(ip) {
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}
next.ServeHTTP(w, r)
})
}
}