Files
dex/pkg/api/websocket_server.go
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

1451 lines
34 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"math/big"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/luxfi/dex/pkg/dex"
)
// WebSocketServer handles real-time trading connections
type WebSocketServer struct {
// Core components
engine *dex.TradingEngine
marginEngine *dex.MarginEngine
lendingPool *dex.LendingPool
// unifiedPool *dex.UnifiedLiquidityPool // TODO: Implement
oracle *dex.PriceOracle
vaultManager *dex.VaultManager
xchain *dex.XChainIntegration
liquidationEngine *dex.LiquidationEngine
// WebSocket
upgrader websocket.Upgrader
clients map[string]*Client
broadcast chan []byte
// Subscriptions
subscriptions map[string]map[string]bool // client -> symbols
// Auth
authService AuthService
// Metrics
metrics *ServerMetrics
// Control
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
}
// Client represents a connected trader
type Client struct {
ID string
UserID string
conn *websocket.Conn
send chan []byte
subscriptions map[string]bool
authenticated bool
lastActivity time.Time
rateLimiter *RateLimiter
mu sync.RWMutex
}
// Message represents a WebSocket message
type Message struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// AuthService handles authentication
type AuthService interface {
Authenticate(apiKey, apiSecret string) (string, error)
ValidateSession(sessionID string) (string, error)
GetUserID(sessionID string) (string, error)
}
// ServerMetrics tracks server performance
type ServerMetrics struct {
ConnectionsTotal uint64
ConnectionsActive uint64
MessagesReceived uint64
MessagesSent uint64
SubscriptionsActive uint64
AuthFailures uint64
OrdersProcessed uint64
PositionsOpened uint64
LiquidationsExecuted uint64
ErrorCount uint64
mu sync.RWMutex
}
// RateLimiter implements rate limiting
type RateLimiter struct {
requests int
maxRequests int
window time.Duration
lastReset time.Time
mu sync.Mutex
}
// NewWebSocketServer creates a new WebSocket server
func NewWebSocketServer(config ServerConfig) *WebSocketServer {
ctx, cancel := context.WithCancel(context.Background())
return &WebSocketServer{
engine: config.Engine,
marginEngine: config.MarginEngine,
lendingPool: config.LendingPool,
// unifiedPool: config.UnifiedPool, // TODO: Implement
oracle: config.Oracle,
vaultManager: config.VaultManager,
xchain: config.XChain,
liquidationEngine: config.LiquidationEngine,
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return isOriginAllowed(origin)
},
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
clients: make(map[string]*Client),
broadcast: make(chan []byte, 256),
subscriptions: make(map[string]map[string]bool),
authService: config.AuthService,
metrics: NewServerMetrics(),
ctx: ctx,
cancel: cancel,
}
}
// AllowedOrigins defines the list of allowed CORS origins
// Empty slice means no restrictions (development mode - NOT FOR PRODUCTION)
//
// Architecture:
// - lux.exchange: Main unified exchange interface (AMM + Order Book)
// - dex.lux.network: Order book DEX sidecar API
// - amm.lux.network: Uniswap V3 AMM on C-Chain (future)
var AllowedOrigins = []string{
// Production - Main exchange interface
"https://lux.exchange",
// Production - Order book DEX sidecar
"https://dex.lux.network",
// Production - AMM (Uniswap V3 fork on C-Chain)
"https://amm.lux.network",
// Development
"https://localhost:3000",
"http://localhost:3000",
"http://localhost:8080",
}
// isOriginAllowed checks if the origin is in the allowed list
func isOriginAllowed(origin string) bool {
// In development/testing, if no origins configured, allow all
if len(AllowedOrigins) == 0 {
return true
}
for _, allowed := range AllowedOrigins {
if allowed == origin || allowed == "*" {
return true
}
}
return false
}
// ServerConfig contains server configuration
type ServerConfig struct {
Engine *dex.TradingEngine
MarginEngine *dex.MarginEngine
LendingPool *dex.LendingPool
// UnifiedPool *dex.UnifiedLiquidityPool // TODO: Implement
Oracle *dex.PriceOracle
VaultManager *dex.VaultManager
XChain *dex.XChainIntegration
LiquidationEngine *dex.LiquidationEngine
AuthService AuthService
}
// HandleConnection handles new WebSocket connections
func (ws *WebSocketServer) HandleConnection(w http.ResponseWriter, r *http.Request) {
conn, err := ws.upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
clientID := generateClientID()
client := &Client{
ID: clientID,
conn: conn,
send: make(chan []byte, 256),
subscriptions: make(map[string]bool),
lastActivity: time.Now(),
rateLimiter: NewRateLimiter(100, time.Minute),
}
ws.mu.Lock()
ws.clients[clientID] = client
ws.metrics.ConnectionsActive++
ws.metrics.ConnectionsTotal++
ws.mu.Unlock()
// Start client handlers
go client.writePump()
go client.readPump(ws)
// Send welcome message
welcome := Message{
Type: "connected",
Data: map[string]interface{}{"client_id": clientID},
Timestamp: time.Now().Unix(),
}
client.sendMessage(welcome)
}
// Client message handlers
func (c *Client) readPump(ws *WebSocketServer) {
defer func() {
ws.removeClient(c)
c.conn.Close()
}()
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
var msg map[string]interface{}
err := c.conn.ReadJSON(&msg)
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
// Log error
}
break
}
// Rate limiting
if !c.rateLimiter.Allow() {
c.sendError("Rate limit exceeded", "")
continue
}
// Update activity
c.lastActivity = time.Now()
ws.metrics.MessagesReceived++
// Process message
ws.processMessage(c, msg)
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(54 * time.Second)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.conn.WriteMessage(websocket.TextMessage, message)
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
// Message processing
func (ws *WebSocketServer) processMessage(client *Client, msg map[string]interface{}) {
msgType, ok := msg["type"].(string)
if !ok {
client.sendError("Invalid message type", "")
return
}
// Get request ID if present
requestID := ""
if rid, ok := msg["request_id"].(string); ok {
requestID = rid
}
switch msgType {
case "auth":
ws.handleAuth(client, msg, requestID)
case "ping":
ws.handlePing(client, requestID)
case "subscribe":
ws.handleSubscribe(client, msg, requestID)
case "unsubscribe":
ws.handleUnsubscribe(client, msg, requestID)
case "place_order":
ws.handlePlaceOrder(client, msg, requestID)
case "cancel_order":
ws.handleCancelOrder(client, msg, requestID)
case "modify_order":
ws.handleModifyOrder(client, msg, requestID)
case "open_position":
ws.handleOpenPosition(client, msg, requestID)
case "close_position":
ws.handleClosePosition(client, msg, requestID)
case "modify_leverage":
ws.handleModifyLeverage(client, msg, requestID)
case "vault_deposit":
ws.handleVaultDeposit(client, msg, requestID)
case "vault_withdraw":
ws.handleVaultWithdraw(client, msg, requestID)
case "lending_supply":
ws.handleLendingSupply(client, msg, requestID)
case "lending_borrow":
ws.handleLendingBorrow(client, msg, requestID)
case "lending_repay":
ws.handleLendingRepay(client, msg, requestID)
case "get_balances":
ws.handleGetBalances(client, requestID)
case "get_positions":
ws.handleGetPositions(client, requestID)
case "get_orders":
ws.handleGetOrders(client, requestID)
default:
client.sendError(fmt.Sprintf("Unknown message type: %s", msgType), requestID)
}
}
// Authentication handler
func (ws *WebSocketServer) handleAuth(client *Client, msg map[string]interface{}, requestID string) {
apiKey, _ := msg["apiKey"].(string)
apiSecret, _ := msg["apiSecret"].(string)
if apiKey == "" || apiSecret == "" {
client.sendError("Missing credentials", requestID)
ws.metrics.AuthFailures++
return
}
userID, err := ws.authService.Authenticate(apiKey, apiSecret)
if err != nil {
client.sendError("Authentication failed", requestID)
ws.metrics.AuthFailures++
return
}
client.mu.Lock()
client.authenticated = true
client.UserID = userID
client.mu.Unlock()
response := Message{
Type: "auth_success",
Data: map[string]interface{}{"user_id": userID},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
// Send initial data
ws.sendInitialData(client)
}
// Trading handlers
// Helper function to parse Side from string
func parseSide(s string) dex.Side {
switch s {
case "buy", "BUY":
return dex.Buy
case "sell", "SELL":
return dex.Sell
default:
return dex.Buy
}
}
// Helper function to parse OrderType from string
func parseOrderType(s string) dex.OrderType {
switch s {
case "market", "MARKET":
return dex.Market
case "limit", "LIMIT":
return dex.Limit
case "stop", "STOP":
return dex.Stop
case "stop_limit", "STOP_LIMIT":
return dex.StopLimit
default:
return dex.Limit
}
}
// Helper function to get opposite side
func oppositeSide(side dex.Side) dex.Side {
if side == dex.Buy {
return dex.Sell
}
return dex.Buy
}
func (ws *WebSocketServer) handlePlaceOrder(client *Client, msg map[string]interface{}, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Parse order from message with proper type checking
orderDataRaw, ok := msg["order"]
if !ok {
client.sendError("Missing order data", requestID)
return
}
orderData, ok := orderDataRaw.(map[string]interface{})
if !ok {
client.sendError("Invalid order data format", requestID)
return
}
// Extract fields with safe type assertions
symbol, ok := orderData["symbol"].(string)
if !ok {
client.sendError("Missing or invalid symbol", requestID)
return
}
side, ok := orderData["side"].(string)
if !ok {
client.sendError("Missing or invalid side", requestID)
return
}
orderType, ok := orderData["type"].(string)
if !ok {
client.sendError("Missing or invalid order type", requestID)
return
}
price, ok := orderData["price"].(float64)
if !ok {
client.sendError("Missing or invalid price", requestID)
return
}
size, ok := orderData["size"].(float64)
if !ok {
client.sendError("Missing or invalid size", requestID)
return
}
order := &dex.Order{
Symbol: symbol,
Side: parseSide(side),
Type: parseOrderType(orderType),
Price: price,
Size: size,
User: client.UserID,
Timestamp: time.Now(),
}
// Submit order - get the order book and add the order
book := ws.engine.GetOrderBook(order.Symbol)
if book == nil {
ws.engine.CreateOrderBook(order.Symbol)
book = ws.engine.GetOrderBook(order.Symbol)
}
orderID := book.AddOrder(order)
if orderID == 0 {
client.sendError("Order failed", requestID)
return
}
order.ID = orderID
ws.metrics.OrdersProcessed++
// Get any trades that may have occurred (would need to be tracked separately)
// For now, we'll skip broadcasting trades as AddOrder doesn't return them
response := Message{
Type: "order_update",
Data: map[string]interface{}{
"order": order,
"status": "submitted",
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
func (ws *WebSocketServer) handleCancelOrder(client *Client, msg map[string]interface{}, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Safe type assertion for orderID
orderIDRaw, ok := msg["orderID"]
if !ok {
client.sendError("Missing orderID", requestID)
return
}
orderIDFloat, ok := orderIDRaw.(float64)
if !ok {
client.sendError("Invalid orderID format", requestID)
return
}
orderID := uint64(orderIDFloat)
// Find and cancel the order across all order books
var err error
var orderFound bool
for _, book := range ws.engine.OrderBooks {
if book.CancelOrder(orderID) == nil {
orderFound = true
break
}
}
if !orderFound {
err = fmt.Errorf("order not found")
}
if err != nil {
client.sendError(fmt.Sprintf("Cancel failed: %v", err), requestID)
return
}
response := Message{
Type: "order_update",
Data: map[string]interface{}{
"order_id": orderID,
"status": "cancelled",
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
// Margin trading handlers
func (ws *WebSocketServer) handleOpenPosition(client *Client, msg map[string]interface{}, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Safe type assertions for all fields
symbol, ok := msg["symbol"].(string)
if !ok {
client.sendError("Missing or invalid symbol", requestID)
return
}
sideStr, ok := msg["side"].(string)
if !ok {
client.sendError("Missing or invalid side", requestID)
return
}
side := parseSide(sideStr)
size, ok := msg["size"].(float64)
if !ok {
client.sendError("Missing or invalid size", requestID)
return
}
leverage, ok := msg["leverage"].(float64)
if !ok {
client.sendError("Missing or invalid leverage", requestID)
return
}
// Create order for position
order := &dex.Order{
Symbol: symbol,
Side: side,
Type: dex.Market,
Size: size,
User: client.UserID,
}
// Open position
position, err := ws.marginEngine.OpenPosition(client.UserID, order, leverage)
if err != nil {
client.sendError(fmt.Sprintf("Position failed: %v", err), requestID)
return
}
ws.metrics.PositionsOpened++
response := Message{
Type: "position_update",
Data: map[string]interface{}{
"position": position,
"action": "opened",
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
func (ws *WebSocketServer) handleClosePosition(client *Client, msg map[string]interface{}, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Safe type assertions
positionID, ok := msg["positionID"].(string)
if !ok {
client.sendError("Missing or invalid positionID", requestID)
return
}
size, ok := msg["size"].(float64)
if !ok {
client.sendError("Missing or invalid size", requestID)
return
}
err := ws.marginEngine.ClosePosition(client.UserID, positionID, size)
if err != nil {
client.sendError(fmt.Sprintf("Close failed: %v", err), requestID)
return
}
response := Message{
Type: "position_update",
Data: map[string]interface{}{
"position_id": positionID,
"action": "closed",
"size": size,
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
// Vault operations
func (ws *WebSocketServer) handleVaultDeposit(client *Client, msg map[string]interface{}, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Safe type assertions
vaultID, ok := msg["vaultID"].(string)
if !ok {
client.sendError("Missing or invalid vaultID", requestID)
return
}
amountStr, ok := msg["amount"].(string)
if !ok {
client.sendError("Missing or invalid amount", requestID)
return
}
amount, success := new(big.Int).SetString(amountStr, 10)
if !success {
client.sendError("Invalid amount format", requestID)
return
}
vault, err := ws.vaultManager.GetVault(vaultID)
if err != nil {
client.sendError(fmt.Sprintf("Vault not found: %v", err), requestID)
return
}
position, err := vault.Deposit(client.UserID, amount)
if err != nil {
client.sendError(fmt.Sprintf("Deposit failed: %v", err), requestID)
return
}
// Convert position to a proper map for JSON serialization
positionData := map[string]interface{}{
"User": position.User,
"Shares": position.Shares.String(),
"DepositValue": position.DepositValue.String(),
"CurrentValue": position.CurrentValue.String(),
"LockedUntil": position.LockedUntil.Format(time.RFC3339),
"LastUpdate": position.LastUpdate.Format(time.RFC3339),
"RealizedPnL": position.RealizedPnL.String(),
"UnrealizedPnL": position.UnrealizedPnL.String(),
}
response := Message{
Type: "vault_update",
Data: map[string]interface{}{
"vault_id": vaultID,
"action": "deposited",
"position": positionData,
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
// Lending operations
func (ws *WebSocketServer) handleLendingSupply(client *Client, msg map[string]interface{}, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Safe type assertions
asset, ok := msg["asset"].(string)
if !ok {
client.sendError("Missing or invalid asset", requestID)
return
}
amountStr, ok := msg["amount"].(string)
if !ok {
client.sendError("Missing or invalid amount", requestID)
return
}
amount, success := new(big.Int).SetString(amountStr, 10)
if !success {
client.sendError("Invalid amount format", requestID)
return
}
err := ws.lendingPool.Supply(client.UserID, asset, amount)
if err != nil {
client.sendError(fmt.Sprintf("Supply failed: %v", err), requestID)
return
}
response := Message{
Type: "lending_update",
Data: map[string]interface{}{
"action": "supplied",
"asset": asset,
"amount": amount.String(),
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
// Market data subscriptions
func (ws *WebSocketServer) handleSubscribe(client *Client, msg map[string]interface{}, requestID string) {
// Safe type assertions
channel, ok := msg["channel"].(string)
if !ok {
client.sendError("Missing or invalid channel", requestID)
return
}
symbolsRaw, ok := msg["symbols"]
if !ok {
client.sendError("Missing symbols", requestID)
return
}
symbols, ok := symbolsRaw.([]interface{})
if !ok {
client.sendError("Invalid symbols format", requestID)
return
}
client.mu.Lock()
for _, symbol := range symbols {
key := fmt.Sprintf("%s:%s", channel, symbol)
client.subscriptions[key] = true
}
client.mu.Unlock()
ws.mu.Lock()
if ws.subscriptions[client.ID] == nil {
ws.subscriptions[client.ID] = make(map[string]bool)
}
for _, symbol := range symbols {
ws.subscriptions[client.ID][symbol.(string)] = true
}
ws.metrics.SubscriptionsActive++
ws.mu.Unlock()
response := Message{
Type: "subscribed",
Data: map[string]interface{}{
"channel": channel,
"symbols": symbols,
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
func (ws *WebSocketServer) handleUnsubscribe(client *Client, msg map[string]interface{}, requestID string) {
// Safe type assertions
channel, ok := msg["channel"].(string)
if !ok {
client.sendError("Missing or invalid channel", requestID)
return
}
symbolsRaw, ok := msg["symbols"]
if !ok {
client.sendError("Missing symbols", requestID)
return
}
symbols, ok := symbolsRaw.([]interface{})
if !ok {
client.sendError("Invalid symbols format", requestID)
return
}
client.mu.Lock()
for _, symbol := range symbols {
key := fmt.Sprintf("%s:%s", channel, symbol)
delete(client.subscriptions, key)
}
client.mu.Unlock()
ws.mu.Lock()
for _, symbol := range symbols {
delete(ws.subscriptions[client.ID], symbol.(string))
}
ws.metrics.SubscriptionsActive--
ws.mu.Unlock()
response := Message{
Type: "unsubscribed",
Data: map[string]interface{}{
"channel": channel,
"symbols": symbols,
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
// Account data handlers
func (ws *WebSocketServer) handleGetBalances(client *Client, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Get balances from margin account
account := ws.marginEngine.GetAccount(client.UserID)
if account == nil {
client.sendError("Account not found", requestID)
return
}
balances := make(map[string]string)
for asset, collateral := range account.CollateralAssets {
balances[asset] = collateral.Amount.String()
}
response := Message{
Type: "balance_update",
Data: map[string]interface{}{
"balances": balances,
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
func (ws *WebSocketServer) handleGetPositions(client *Client, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
account := ws.marginEngine.GetAccount(client.UserID)
if account == nil {
client.sendError("Account not found", requestID)
return
}
response := Message{
Type: "positions_update",
Data: map[string]interface{}{
"positions": account.Positions,
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
func (ws *WebSocketServer) handleGetOrders(client *Client, requestID string) {
if !client.authenticated {
client.sendError("Not authenticated", requestID)
return
}
// Get orders from engine
orders := ws.engine.GetUserOrders(client.UserID)
response := Message{
Type: "orders_update",
Data: map[string]interface{}{
"orders": orders,
},
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
// Broadcast methods
func (ws *WebSocketServer) BroadcastOrderBook(symbol string, snapshot *dex.OrderBookSnapshot) {
msg := Message{
Type: "orderbook_update",
Data: map[string]interface{}{
"symbol": symbol,
"snapshot": snapshot,
},
Timestamp: time.Now().Unix(),
}
ws.broadcastToSubscribers(fmt.Sprintf("orderbook:%s", symbol), msg)
}
func (ws *WebSocketServer) BroadcastTrade(trade *dex.Trade) {
msg := Message{
Type: "trade_update",
Data: map[string]interface{}{
"trade": trade,
},
Timestamp: time.Now().Unix(),
}
// Trade doesn't have Symbol, broadcast to all trade subscribers
ws.broadcastToSubscribers("trades", msg)
}
func (ws *WebSocketServer) BroadcastPrice(symbol string, price float64) {
msg := Message{
Type: "price_update",
Data: map[string]interface{}{
"symbol": symbol,
"price": price,
},
Timestamp: time.Now().Unix(),
}
ws.broadcastToSubscribers(fmt.Sprintf("prices:%s", symbol), msg)
}
func (ws *WebSocketServer) broadcastToSubscribers(channel string, msg Message) {
ws.mu.RLock()
defer ws.mu.RUnlock()
data, _ := json.Marshal(msg)
for _, client := range ws.clients {
client.mu.RLock()
if client.subscriptions[channel] {
select {
case client.send <- data:
ws.metrics.MessagesSent++
default:
// Client send channel full
}
}
client.mu.RUnlock()
}
}
// Helper methods
func (ws *WebSocketServer) handlePing(client *Client, requestID string) {
response := Message{
Type: "pong",
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
client.sendMessage(response)
}
func (ws *WebSocketServer) sendInitialData(client *Client) {
// Send current balances
ws.handleGetBalances(client, "")
// Send current positions
ws.handleGetPositions(client, "")
// Send open orders
ws.handleGetOrders(client, "")
}
func (ws *WebSocketServer) removeClient(client *Client) {
ws.mu.Lock()
delete(ws.clients, client.ID)
delete(ws.subscriptions, client.ID)
ws.metrics.ConnectionsActive--
ws.mu.Unlock()
close(client.send)
}
func (c *Client) sendMessage(msg Message) {
data, _ := json.Marshal(msg)
select {
case c.send <- data:
default:
// Send channel full
}
}
func (c *Client) sendError(errMsg string, requestID string) {
msg := Message{
Type: "error",
Error: errMsg,
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
c.sendMessage(msg)
}
// RateLimiter implementation
func NewRateLimiter(maxRequests int, window time.Duration) *RateLimiter {
return &RateLimiter{
maxRequests: maxRequests,
window: window,
lastReset: time.Now(),
}
}
func (rl *RateLimiter) Allow() bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
if now.Sub(rl.lastReset) > rl.window {
rl.requests = 0
rl.lastReset = now
}
if rl.requests >= rl.maxRequests {
return false
}
rl.requests++
return true
}
// Metrics
func NewServerMetrics() *ServerMetrics {
return &ServerMetrics{}
}
func (sm *ServerMetrics) GetSnapshot() map[string]interface{} {
sm.mu.RLock()
defer sm.mu.RUnlock()
return map[string]interface{}{
"connections_total": sm.ConnectionsTotal,
"connections_active": sm.ConnectionsActive,
"messages_received": sm.MessagesReceived,
"messages_sent": sm.MessagesSent,
"subscriptions_active": sm.SubscriptionsActive,
"auth_failures": sm.AuthFailures,
"orders_processed": sm.OrdersProcessed,
"positions_opened": sm.PositionsOpened,
"liquidations_executed": sm.LiquidationsExecuted,
"error_count": sm.ErrorCount,
}
}
// Utility functions
func generateClientID() string {
return fmt.Sprintf("client_%d", time.Now().UnixNano())
}
// Start starts the WebSocket server
func (ws *WebSocketServer) Start() {
// Start market data broadcaster
go ws.marketDataBroadcaster()
// Start position monitor
go ws.positionMonitor()
// Start metrics reporter
go ws.metricsReporter()
}
// Market data broadcaster
func (ws *WebSocketServer) marketDataBroadcaster() {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Broadcast order book updates
symbols := []string{"BTC-USDT", "ETH-USDT", "BNB-USDT"}
for _, symbol := range symbols {
ob := ws.engine.GetOrderBook(symbol)
if ob != nil {
snapshot := ob.GetSnapshot()
ws.BroadcastOrderBook(symbol, snapshot)
}
// Broadcast price updates
price := ws.oracle.GetPrice(symbol)
if price > 0 {
ws.BroadcastPrice(symbol, price)
}
}
case <-ws.ctx.Done():
return
}
}
}
// Position monitor checks for liquidations
func (ws *WebSocketServer) positionMonitor() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
ws.checkLiquidations()
case <-ws.ctx.Done():
return
}
}
}
func (ws *WebSocketServer) checkLiquidations() {
// Use the MarginEngine's built-in CheckLiquidations method
ws.marginEngine.CheckLiquidations()
// TODO: The following would need implementation of GetAllPositions
// and manual liquidation checks if needed
/*
positions := ws.marginEngine.GetAllPositions()
for _, position := range positions {
// Get current mark price from oracle
markPrice := ws.oracle.GetPrice(position.Symbol)
position.MarkPrice = markPrice
// Check if position should be liquidated
if ws.marginEngine.ShouldLiquidate(position, markPrice) {
// Create liquidation order
liquidationOrder := &dex.Order{
Symbol: position.Symbol,
Side: oppositeSide(position.Side),
Type: dex.Market,
Size: position.Size,
User: "liquidation_engine",
}
// Process the liquidation
err := ws.liquidationEngine.ProcessLiquidation(position.User, position, liquidationOrder)
if err == nil {
ws.metrics.LiquidationsExecuted++
// Notify the affected client
ws.notifyLiquidation(position.User, position)
// Log liquidation event
log.Printf("Liquidated position: user=%s, symbol=%s, size=%f, price=%f",
position.User, position.Symbol, position.Size, markPrice)
} else {
log.Printf("Failed to liquidate position: %v", err)
}
}
}
*/
}
func (ws *WebSocketServer) notifyLiquidation(userID string, position *dex.MarginPosition) {
// Find client for user
ws.mu.RLock()
var targetClient *Client
for _, client := range ws.clients {
if client.UserID == userID {
targetClient = client
break
}
}
ws.mu.RUnlock()
if targetClient != nil {
// Calculate realized PnL
realizedPnL := (position.MarkPrice - position.EntryPrice) * position.Size
if position.Side == dex.Sell {
realizedPnL = -realizedPnL
}
msg := Message{
Type: "position_update",
Data: map[string]interface{}{
"position": position,
"action": "liquidated",
"message": fmt.Sprintf("Position %s liquidated at %.2f", position.ID, position.MarkPrice),
"realizedPnL": realizedPnL,
},
Timestamp: time.Now().Unix(),
}
targetClient.sendMessage(msg)
}
// Also broadcast public liquidation event
liquidationMsg := Message{
Type: "public_liquidation",
Data: map[string]interface{}{
"symbol": position.Symbol,
"side": position.Side,
"size": position.Size,
"price": position.MarkPrice,
"timestamp": time.Now().Unix(),
},
Timestamp: time.Now().Unix(),
}
if msgBytes, err := json.Marshal(liquidationMsg); err == nil {
ws.broadcast <- msgBytes
}
}
// handleModifyOrder handles order modification requests
func (ws *WebSocketServer) handleModifyOrder(client *Client, msg map[string]interface{}, requestID string) {
// Extract order data - handle both formats
var orderID uint64
if id, ok := msg["orderID"].(float64); ok {
orderID = uint64(id)
} else if id, ok := msg["order_id"].(float64); ok {
orderID = uint64(id)
}
var newPrice, newSize float64
if price, ok := msg["newPrice"].(float64); ok {
newPrice = price
} else if price, ok := msg["price"].(float64); ok {
newPrice = price
}
if size, ok := msg["newSize"].(float64); ok {
newSize = size
} else if size, ok := msg["size"].(float64); ok {
newSize = size
}
// Modify order logic would go here
// For now, send acknowledgment
client.sendMessage(Message{
Type: "order_modified",
Data: map[string]interface{}{"order_id": orderID, "price": newPrice, "size": newSize},
Timestamp: time.Now().Unix(),
})
}
// handleModifyLeverage handles leverage modification requests
func (ws *WebSocketServer) handleModifyLeverage(client *Client, msg map[string]interface{}, requestID string) {
userID := client.UserID
// Safe type assertions
positionID, ok := msg["position_id"].(string)
if !ok {
client.sendError("Missing or invalid position_id", requestID)
return
}
newLeverage, ok := msg["leverage"].(float64)
if !ok {
client.sendError("Missing or invalid leverage", requestID)
return
}
err := ws.marginEngine.ModifyLeverage(userID, positionID, newLeverage)
if err != nil {
client.sendError(err.Error(), requestID)
return
}
client.sendMessage(Message{
Type: "leverage_modified",
Data: map[string]interface{}{"position_id": positionID, "leverage": newLeverage},
Timestamp: time.Now().Unix(),
})
}
// handleVaultWithdraw handles vault withdrawal requests
func (ws *WebSocketServer) handleVaultWithdraw(client *Client, msg map[string]interface{}, requestID string) {
// Safe type assertions
vaultID, ok := msg["vault_id"].(string)
if !ok {
client.sendError("Missing or invalid vault_id", requestID)
return
}
amountStr, ok := msg["amount"].(string)
if !ok {
client.sendError("Missing or invalid amount", requestID)
return
}
amount := new(big.Int)
if _, success := amount.SetString(amountStr, 10); !success {
client.sendError("Invalid amount format", requestID)
return
}
// Vault withdrawal logic would go here
client.sendMessage(Message{
Type: "vault_withdrawal",
Data: map[string]interface{}{"vault_id": vaultID, "amount": amount.String()},
Timestamp: time.Now().Unix(),
})
}
// handleLendingBorrow handles lending borrow requests
func (ws *WebSocketServer) handleLendingBorrow(client *Client, msg map[string]interface{}, requestID string) {
// Safe type assertions
asset, ok := msg["asset"].(string)
if !ok {
client.sendError("Missing or invalid asset", requestID)
return
}
amountStr, ok := msg["amount"].(string)
if !ok {
client.sendError("Missing or invalid amount", requestID)
return
}
amount := new(big.Int)
if _, success := amount.SetString(amountStr, 10); !success {
client.sendError("Invalid amount format", requestID)
return
}
err := ws.lendingPool.Borrow(asset, amount)
if err != nil {
client.sendError(err.Error(), requestID)
return
}
client.sendMessage(Message{
Type: "borrow_success",
Data: map[string]interface{}{"asset": asset, "amount": amount.String()},
Timestamp: time.Now().Unix(),
})
}
// handleLendingRepay handles lending repayment requests
func (ws *WebSocketServer) handleLendingRepay(client *Client, msg map[string]interface{}, requestID string) {
// Safe type assertions
asset, ok := msg["asset"].(string)
if !ok {
client.sendError("Missing or invalid asset", requestID)
return
}
amountStr, ok := msg["amount"].(string)
if !ok {
client.sendError("Missing or invalid amount", requestID)
return
}
interestStr, ok := msg["interest"].(string)
if !ok {
client.sendError("Missing or invalid interest", requestID)
return
}
amount := new(big.Int)
if _, success := amount.SetString(amountStr, 10); !success {
client.sendError("Invalid amount format", requestID)
return
}
interest := new(big.Int)
if _, success := interest.SetString(interestStr, 10); !success {
client.sendError("Invalid interest format", requestID)
return
}
ws.lendingPool.Repay(asset, amount, interest)
client.sendMessage(Message{
Type: "repay_success",
Data: map[string]interface{}{"asset": asset, "amount": amount.String()},
Timestamp: time.Now().Unix(),
})
}
// Metrics reporter
func (ws *WebSocketServer) metricsReporter() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
metrics := ws.metrics.GetSnapshot()
// Log or send metrics to monitoring system
_ = metrics
case <-ws.ctx.Done():
return
}
}
}
// Shutdown gracefully shuts down the server
func (ws *WebSocketServer) Shutdown() {
ws.cancel()
ws.mu.Lock()
for _, client := range ws.clients {
client.conn.Close()
}
ws.mu.Unlock()
close(ws.broadcast)
}