Files
zeekayandHanzo Dev 10a084c0b2 refactor(dex): fold pkg/lx into pkg/dex — one matcher, one package
The matching engine (pkg/lx, 45.5K LOC incl. tests) and the deterministic
core layered on it (pkg/dex) are one matcher spread across two packages
(dex imported lx in 12 files; lx imported neither). Fold lx into dex and
drop the "lx" name entirely — pkg/dex is now the single matcher package.

Pure rename/fold, ZERO logic change. Behavior proven identical: the merged
pkg/dex runs 457 top-level tests (431 former-lx + 26 former-dex) and 489
test funcs (468 + 21), all green — matching, batch (BatchMatchEqualsSequential,
run-to-run determinism, tile boundary), GPU/CPU parity, truncation, and CL
swap math all pass unchanged.

What moved:
- git mv all 101 pkg/lx/*.go + 1 .c into pkg/dex/ (package lx -> package dex).
- lx's swap_test.go -> swap_math_test.go (only filename collision with dex's
  existing swap_test.go).
- Rewrote imports repo-wide: 54 pkg/lx import lines removed; 42 external
  importers (api, client, consensus, dchain, dpdk, lxgpu, marketdata, test/,
  tests/) repointed to pkg/dex with lx.X -> dex.X; the 12 in-package dex files
  had lx.X -> bare X. 717 selector lines rewritten. Stale pkg/lx comment refs
  updated (pkg/lxgpu left intact).

Collisions resolved (2, both package-level vars):
- ErrNoLiquidity: lx's pool-level error (dexError, used only in swap.go, never
  referenced via qualifier) renamed ErrPoolNoLiquidity to match the existing
  ErrPool* convention; dex's router-level ErrNoLiquidity (public, has errors.Is
  tests) kept.
- priceMultiplierBig: identical after fold (both big.NewInt(PriceMultiplier));
  deduped by deleting dex's copy.

Not folded — pkg/orderbook: it is NOT the GPU seam (that is orderbook_gpu.go,
which lives in the engine and folded in here). pkg/orderbook is a zero-importer,
placeholder-cgo island whose 13 core domain identifiers (Order, OrderBook, Trade,
OrderType, OrderStatus, PriceLevel, Buy, Sell, Market, Limit, Filled,
PartiallyFilled, NewOrderBook) collide incompatibly with the engine's. Folding
it verbatim breaks the build; folding via rename reintroduces the exact parallel
duplication this fold removes. Left for owner decision (recommend deletion as
dead code) — see refactor notes.

Follow-on (separate repos, not touched): 19 files under luxfi/dex import
pkg/lx externally — lux/precompile/dex, lux/precompile-dex/dex,
lux/consensus/examples/01-simple-bridge, lux/_consensus-v12537/examples,
lux/cex/pkg/engine — repoint to pkg/dex after this lands.

Verified: CGO_ENABLED=0 GOWORK=off go build ./... clean; go vet ./... clean;
go test ./... -count=1 all green (22 ok / 0 fail).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-02 00:57:08 -07:00

194 lines
4.3 KiB
Go

package api
import (
"encoding/json"
"net"
"net/http"
"sync"
"time"
"github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/log"
)
// IPRateLimiter tracks requests per IP
type IPRateLimiter struct {
requests map[string]*rateLimitEntry
mu sync.RWMutex
limit int // max requests per window
window time.Duration // time window
}
type rateLimitEntry struct {
count int
resetTime time.Time
}
// NewIPRateLimiter creates a new rate limiter
func NewIPRateLimiter(limit int, window time.Duration) *IPRateLimiter {
rl := &IPRateLimiter{
requests: make(map[string]*rateLimitEntry),
limit: limit,
window: window,
}
// Cleanup expired entries periodically
go rl.cleanup()
return rl
}
// Allow checks if a request from IP is allowed
func (rl *IPRateLimiter) Allow(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
entry, exists := rl.requests[ip]
if !exists || now.After(entry.resetTime) {
rl.requests[ip] = &rateLimitEntry{
count: 1,
resetTime: now.Add(rl.window),
}
return true
}
if entry.count >= rl.limit {
return false
}
entry.count++
return true
}
func (rl *IPRateLimiter) cleanup() {
ticker := time.NewTicker(rl.window)
defer ticker.Stop()
for range ticker.C {
rl.mu.Lock()
now := time.Now()
for ip, entry := range rl.requests {
if now.After(entry.resetTime) {
delete(rl.requests, ip)
}
}
rl.mu.Unlock()
}
}
// JSONRPCServer handles JSON-RPC requests
type JSONRPCServer struct {
orderBook *dex.OrderBook
logger log.Logger
rateLimiter *IPRateLimiter
}
// NewJSONRPCServer creates a new JSON-RPC server with rate limiting
// Default: 100 requests per second per IP
func NewJSONRPCServer(orderBook *dex.OrderBook, logger log.Logger) *JSONRPCServer {
return &JSONRPCServer{
orderBook: orderBook,
logger: logger,
rateLimiter: NewIPRateLimiter(100, time.Second),
}
}
// getClientIP extracts client IP from request
func getClientIP(r *http.Request) string {
// Check X-Forwarded-For header (for proxied requests)
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
// Take first IP in chain
if idx := len(xff); idx > 0 {
for i, c := range xff {
if c == ',' {
return xff[:i]
}
}
return xff
}
}
// Fall back to remote address
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return ip
}
// ServeHTTP handles JSON-RPC requests
func (s *JSONRPCServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Rate limit check
clientIP := getClientIP(r)
if !s.rateLimiter.Allow(clientIP) {
w.Header().Set("Retry-After", "1")
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Jsonrpc string `json:"jsonrpc"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
ID interface{} `json:"id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
s.writeError(w, req.ID, -32700, "Parse error")
return
}
if req.Jsonrpc != "2.0" {
s.writeError(w, req.ID, -32600, "Invalid Request")
return
}
switch req.Method {
case "orderbook.getBestBid":
bestBid := s.orderBook.GetBestBid()
s.writeResult(w, req.ID, bestBid)
case "orderbook.getBestAsk":
bestAsk := s.orderBook.GetBestAsk()
s.writeResult(w, req.ID, bestAsk)
case "orderbook.getStats":
stats := map[string]interface{}{
"symbol": s.orderBook.Symbol,
"orders": len(s.orderBook.Orders),
"trades": len(s.orderBook.Trades),
}
s.writeResult(w, req.ID, stats)
default:
s.writeError(w, req.ID, -32601, "Method not found")
}
}
func (s *JSONRPCServer) writeResult(w http.ResponseWriter, id interface{}, result interface{}) {
resp := map[string]interface{}{
"jsonrpc": "2.0",
"result": result,
"id": id,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (s *JSONRPCServer) writeError(w http.ResponseWriter, id interface{}, code int, message string) {
resp := map[string]interface{}{
"jsonrpc": "2.0",
"error": map[string]interface{}{
"code": code,
"message": message,
},
"id": id,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}