mirror of
https://github.com/luxfi/mpc.git
synced 2026-09-24 00:26:24 +00:00
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
109 lines
2.6 KiB
Go
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)
|
|
})
|
|
}
|
|
}
|