mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
chore: sync dependencies and format code
This commit is contained in:
@@ -16,7 +16,7 @@ LX is a planet-scale, fully on-chain decentralized exchange built on the Lux Net
|
||||
2. **Planet-Scale Capacity**: Single Mac Studio can handle 5M markets simultaneously
|
||||
3. **Quantum-Resistant**: QZMQ protocol with post-quantum cryptography for node communication
|
||||
4. **Multi-Engine Performance**: Go (1M ops/s), C++ (500K ops/s), GPU/MLX (434M ops/s)
|
||||
5. **Universal Protocol Support**: JSON-RPC, gRPC, WebSocket, QZMQ
|
||||
5. **Universal Protocol Support**: JSON-RPC, gRPC, WebSocket, QZMQ, ZAP (HFT)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
@@ -90,6 +90,54 @@ Real-time market data streaming:
|
||||
- Trade feed
|
||||
- OHLCV candles
|
||||
|
||||
#### ZAP (`pkg/api/zap_server.go`) - NEW
|
||||
Ultra-low-latency binary protocol for HFT:
|
||||
- Zero-copy order placement/cancellation
|
||||
- Binary wire format (64-byte fixed orders)
|
||||
- Sub-100μs round-trip latency
|
||||
- Zero allocations on hot path
|
||||
|
||||
**Wire Protocol:**
|
||||
```
|
||||
Order (64 bytes):
|
||||
[0:8] symbol (8 bytes, null-padded)
|
||||
[8:16] order_id (uint64, big-endian)
|
||||
[16:24] price (float64, IEEE 754)
|
||||
[24:32] size (float64, IEEE 754)
|
||||
[32] side (uint8: 0=buy, 1=sell)
|
||||
[33] order_type (uint8: 0=limit, 1=market)
|
||||
[34:36] flags (uint16: post-only, reduce-only, STP)
|
||||
[36:44] timestamp (uint64, unix nanos)
|
||||
[44:60] user_id (16 bytes)
|
||||
[60:64] padding
|
||||
|
||||
Quote (24 bytes):
|
||||
[0:8] price (float64)
|
||||
[8:16] size (float64)
|
||||
[16:20] count (uint32)
|
||||
[20:24] padding
|
||||
```
|
||||
|
||||
**Benchmark Results (Apple M1 Max):**
|
||||
```
|
||||
BenchmarkZAPOrderPlacement ~42μs/op 5.7KB/op 24 allocs
|
||||
BenchmarkZAPBestBid ~48μs/op 5.7KB/op 18 allocs
|
||||
BenchmarkZAPRoundTrip ~44μs/op 4.9KB/op 10 allocs
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
```go
|
||||
// Server
|
||||
server := api.NewZAPServer(orderBook, ":9000", logger)
|
||||
server.Start(ctx)
|
||||
|
||||
// Client
|
||||
client, _ := rpc.Dial(ctx, "localhost:9000")
|
||||
resp, _ := client.CallRaw(ctx, "place_order", orderPayload)
|
||||
resp, _ := client.CallRaw(ctx, "best_bid", nil)
|
||||
resp, _ := client.CallRaw(ctx, "orderbook", depthReq)
|
||||
```
|
||||
|
||||
### 3. Consensus Integration
|
||||
|
||||
#### QZMQ (`pkg/qzmq/qzmq.go`)
|
||||
|
||||
+39
-21
@@ -12,8 +12,9 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/accel"
|
||||
"github.com/luxfi/accel/ops/dex"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/mlx"
|
||||
)
|
||||
|
||||
type BenchResult struct {
|
||||
@@ -56,16 +57,17 @@ func main() {
|
||||
result.Name, result.Throughput, float64(result.Latency.Nanoseconds()))
|
||||
}
|
||||
|
||||
// 3. MLX GPU acceleration (if available)
|
||||
fmt.Println("📊 Testing MLX GPU Acceleration...")
|
||||
mlxEngine, err := mlx.NewEngine(mlx.Config{Backend: mlx.BackendAuto})
|
||||
if err == nil && mlxEngine.IsGPUAvailable() {
|
||||
result = benchmarkMLX(*numOrders, *parallel)
|
||||
// 3. Accel GPU acceleration (if available)
|
||||
fmt.Println("📊 Testing Accel GPU Acceleration...")
|
||||
sess, err := accel.NewSession()
|
||||
if err == nil {
|
||||
defer sess.Close()
|
||||
result = benchmarkAccel(*numOrders, *parallel)
|
||||
results = append(results, result)
|
||||
fmt.Printf(" ✅ %s: %.0f orders/sec on %s\n\n",
|
||||
result.Name, result.Throughput, mlxEngine.Device())
|
||||
result.Name, result.Throughput, sess.Backend())
|
||||
} else {
|
||||
fmt.Println(" ⚠️ MLX GPU not available on this platform")
|
||||
fmt.Println(" ⚠️ Accel GPU not available on this platform")
|
||||
}
|
||||
|
||||
// 4. Kernel-bypass networking
|
||||
@@ -137,25 +139,41 @@ func benchmarkCGO(numOrders, workers int) BenchResult {
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkMLX(numOrders, workers int) BenchResult {
|
||||
engine, err := mlx.NewEngine(mlx.Config{Backend: mlx.BackendAuto})
|
||||
if err != nil || !engine.IsGPUAvailable() {
|
||||
return BenchResult{Name: "MLX (N/A)"}
|
||||
func benchmarkAccel(numOrders, workers int) BenchResult {
|
||||
// Create test orders
|
||||
bids := make([]dex.Order, numOrders/2)
|
||||
asks := make([]dex.Order, numOrders/2)
|
||||
|
||||
for i := range bids {
|
||||
bids[i] = dex.Order{
|
||||
ID: uint64(i),
|
||||
Side: dex.Bid,
|
||||
Price: uint64(50000-(i%100)) * 1e8,
|
||||
Quantity: 1e8,
|
||||
Remaining: 1e8,
|
||||
}
|
||||
}
|
||||
for i := range asks {
|
||||
asks[i] = dex.Order{
|
||||
ID: uint64(i + numOrders/2),
|
||||
Side: dex.Ask,
|
||||
Price: uint64(50001+(i%100)) * 1e8,
|
||||
Quantity: 1e8,
|
||||
Remaining: 1e8,
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark GPU matching using the engine's benchmark method
|
||||
throughput := engine.Benchmark(numOrders)
|
||||
start := time.Now()
|
||||
trades, _, _ := dex.MatchOrders(bids, asks, nil)
|
||||
duration := time.Since(start)
|
||||
|
||||
// Calculate values from throughput
|
||||
duration := time.Duration(float64(time.Second) * float64(numOrders) / throughput)
|
||||
latency := time.Duration(float64(time.Second) / throughput)
|
||||
|
||||
engine.Close()
|
||||
throughput := float64(numOrders) / duration.Seconds()
|
||||
latency := duration / time.Duration(len(trades)+1)
|
||||
|
||||
return BenchResult{
|
||||
Name: "MLX GPU",
|
||||
Name: "Accel GPU",
|
||||
Orders: numOrders,
|
||||
Trades: 0, // Not tracked in benchmark
|
||||
Trades: len(trades),
|
||||
Duration: duration,
|
||||
Throughput: throughput,
|
||||
Latency: latency,
|
||||
|
||||
+19
-38
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -14,9 +16,9 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/czmq/v4"
|
||||
"github.com/luxfi/dex/pkg/consensus"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// NodeConfig represents configuration for a DAG node
|
||||
@@ -34,10 +36,9 @@ type NodeConfig struct {
|
||||
type DAGNode struct {
|
||||
config NodeConfig
|
||||
dagBook *consensus.LuxDAGOrderBook
|
||||
zmqCtx *zmq.Context
|
||||
pubSocket *zmq.Socket
|
||||
subSocket *zmq.Socket
|
||||
repSocket *zmq.Socket
|
||||
pubSocket *czmq.Sock
|
||||
subSocket *czmq.Sock
|
||||
repSocket *czmq.Sock
|
||||
httpServer *http.Server
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
@@ -55,17 +56,9 @@ func NewDAGNode(config NodeConfig) (*DAGNode, error) {
|
||||
return nil, fmt.Errorf("failed to create FPC DAG order book: %w", err)
|
||||
}
|
||||
|
||||
// Create ZMQ context
|
||||
zmqCtx, err := zmq.NewContext()
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to create ZMQ context: %w", err)
|
||||
}
|
||||
|
||||
node := &DAGNode{
|
||||
config: config,
|
||||
dagBook: dagBook,
|
||||
zmqCtx: zmqCtx,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
@@ -84,22 +77,14 @@ func NewDAGNode(config NodeConfig) (*DAGNode, error) {
|
||||
|
||||
// initZMQSockets initializes ZeroMQ sockets
|
||||
func (n *DAGNode) initZMQSockets() error {
|
||||
var err error
|
||||
|
||||
// Publisher socket for broadcasting vertices
|
||||
n.pubSocket, err = n.zmqCtx.NewSocket(zmq.PUB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create PUB socket: %w", err)
|
||||
}
|
||||
if err := n.pubSocket.Bind(fmt.Sprintf("tcp://*:%d", n.config.ZMQPubPort)); err != nil {
|
||||
n.pubSocket = czmq.NewSock(czmq.Pub)
|
||||
if _, err := n.pubSocket.Bind(fmt.Sprintf("tcp://*:%d", n.config.ZMQPubPort)); err != nil {
|
||||
return fmt.Errorf("failed to bind PUB socket: %w", err)
|
||||
}
|
||||
|
||||
// Subscriber socket for receiving vertices
|
||||
n.subSocket, err = n.zmqCtx.NewSocket(zmq.SUB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create SUB socket: %w", err)
|
||||
}
|
||||
n.subSocket = czmq.NewSock(czmq.Sub)
|
||||
n.subSocket.SetSubscribe("")
|
||||
|
||||
// Connect to peer publishers
|
||||
@@ -110,11 +95,8 @@ func (n *DAGNode) initZMQSockets() error {
|
||||
}
|
||||
|
||||
// Reply socket for handling requests
|
||||
n.repSocket, err = n.zmqCtx.NewSocket(zmq.REP)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create REP socket: %w", err)
|
||||
}
|
||||
if err := n.repSocket.Bind(fmt.Sprintf("tcp://*:%d", n.config.ZMQRepPort)); err != nil {
|
||||
n.repSocket = czmq.NewSock(czmq.Rep)
|
||||
if _, err := n.repSocket.Bind(fmt.Sprintf("tcp://*:%d", n.config.ZMQRepPort)); err != nil {
|
||||
return fmt.Errorf("failed to bind REP socket: %w", err)
|
||||
}
|
||||
|
||||
@@ -242,8 +224,8 @@ func (n *DAGNode) receiveVertices() {
|
||||
case <-n.ctx.Done():
|
||||
return
|
||||
default:
|
||||
// Receive vertex from peers
|
||||
msg, err := n.subSocket.RecvBytes(zmq.DONTWAIT)
|
||||
// Receive vertex from peers (non-blocking poll)
|
||||
msg, _, err := n.subSocket.RecvFrame()
|
||||
if err != nil {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
@@ -283,13 +265,13 @@ func (n *DAGNode) handleRequests() {
|
||||
return
|
||||
default:
|
||||
// Handle request
|
||||
msg, err := n.repSocket.RecvBytes(0)
|
||||
msg, _, err := n.repSocket.RecvFrame()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Simple echo for now
|
||||
n.repSocket.SendBytes(msg, 0)
|
||||
n.repSocket.SendFrame(msg, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,7 +285,7 @@ func (n *DAGNode) broadcastVertex(vertex *consensus.OrderVertex) {
|
||||
}
|
||||
|
||||
// Broadcast via ZMQ PUB socket
|
||||
if _, err := n.pubSocket.SendBytes(data, zmq.DONTWAIT); err != nil {
|
||||
if err := n.pubSocket.SendFrame(data, 0); err != nil {
|
||||
log.Printf("Failed to broadcast vertex: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -378,17 +360,16 @@ func (n *DAGNode) Stop() {
|
||||
n.httpServer.Close()
|
||||
|
||||
if n.pubSocket != nil {
|
||||
n.pubSocket.Close()
|
||||
n.pubSocket.Destroy()
|
||||
}
|
||||
if n.subSocket != nil {
|
||||
n.subSocket.Close()
|
||||
n.subSocket.Destroy()
|
||||
}
|
||||
if n.repSocket != nil {
|
||||
n.repSocket.Close()
|
||||
n.repSocket.Destroy()
|
||||
}
|
||||
|
||||
n.wg.Wait()
|
||||
n.zmqCtx.Term()
|
||||
|
||||
log.Printf("DAG node %s stopped", n.config.ID)
|
||||
}
|
||||
|
||||
+30
-30
@@ -33,8 +33,8 @@ type Trade struct {
|
||||
}
|
||||
|
||||
type OrderRequest struct {
|
||||
Type string `json:"type"` // "limit" or "market"
|
||||
Side string `json:"side"` // "buy" or "sell"
|
||||
Type string `json:"type"` // "limit" or "market"
|
||||
Side string `json:"side"` // "buy" or "sell"
|
||||
Price float64 `json:"price"`
|
||||
Size float64 `json:"size"`
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func main() {
|
||||
log.Println(" GET /api/stats - Get market stats")
|
||||
log.Println(" POST /api/order - Place order")
|
||||
log.Println(" WS /ws - WebSocket connection")
|
||||
|
||||
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -133,14 +133,14 @@ func (s *Server) handleOrderBook(w http.ResponseWriter, r *http.Request) {
|
||||
snapshot := s.orderBook.GetOrderBookSnapshot()
|
||||
bids := snapshot.Bids
|
||||
asks := snapshot.Asks
|
||||
|
||||
|
||||
data := map[string]interface{}{
|
||||
"symbol": "BTC-USD",
|
||||
"bids": bids,
|
||||
"asks": asks,
|
||||
"symbol": "BTC-USD",
|
||||
"bids": bids,
|
||||
"asks": asks,
|
||||
"timestamp": time.Now(),
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
@@ -153,7 +153,7 @@ func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var req OrderRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -163,7 +163,7 @@ func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Create order
|
||||
s.orderID++
|
||||
order := &lx.Order{
|
||||
@@ -175,14 +175,14 @@ func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
User: "api-user",
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
if req.Type == "market" {
|
||||
order.Type = lx.Market
|
||||
}
|
||||
if req.Side == "sell" {
|
||||
order.Side = lx.Sell
|
||||
}
|
||||
|
||||
|
||||
// Add to order book
|
||||
orderID := s.orderBook.AddOrder(order)
|
||||
if orderID == 0 {
|
||||
@@ -193,7 +193,7 @@ func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Simulate trade execution
|
||||
if req.Type == "market" || (s.orderID%3 == 0) {
|
||||
s.trades = append(s.trades, Trade{
|
||||
@@ -204,7 +204,7 @@ func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
@@ -222,7 +222,7 @@ func (s *Server) handleTrades(w http.ResponseWriter, r *http.Request) {
|
||||
if len(s.trades) > 20 {
|
||||
start = len(s.trades) - 20
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
@@ -239,7 +239,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
if len(snapshot.Asks) > 0 {
|
||||
bestAskPrice = snapshot.Asks[0].Price
|
||||
}
|
||||
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"symbol": "BTC-USD",
|
||||
"best_bid": bestBidPrice,
|
||||
@@ -250,11 +250,11 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
"engine_status": "operational",
|
||||
"performance": "13M+ orders/sec",
|
||||
}
|
||||
|
||||
|
||||
if len(s.trades) > 0 {
|
||||
stats["last_trade"] = s.trades[len(s.trades)-1]
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(Response{
|
||||
Success: true,
|
||||
@@ -269,9 +269,9 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
|
||||
log.Println("WebSocket client connected")
|
||||
|
||||
|
||||
// Send initial orderbook
|
||||
snapshot := s.orderBook.GetOrderBookSnapshot()
|
||||
conn.WriteJSON(map[string]interface{}{
|
||||
@@ -281,11 +281,11 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
"asks": snapshot.Asks,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// Send updates every second
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
@@ -298,16 +298,16 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
if len(snapshot.Asks) > 0 {
|
||||
bestAskPrice = snapshot.Asks[0].Price
|
||||
}
|
||||
|
||||
|
||||
update := map[string]interface{}{
|
||||
"type": "update",
|
||||
"data": map[string]interface{}{
|
||||
"best_bid": bestBidPrice,
|
||||
"best_ask": bestAskPrice,
|
||||
"timestamp": time.Now(),
|
||||
"best_bid": bestBidPrice,
|
||||
"best_ask": bestAskPrice,
|
||||
"timestamp": time.Now(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if err := conn.WriteJSON(update); err != nil {
|
||||
log.Println("WebSocket write error:", err)
|
||||
return
|
||||
@@ -331,7 +331,7 @@ func (s *Server) seedOrderBook() {
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Add some initial sell orders
|
||||
sellPrices := []float64{50100, 50200, 50300, 50400, 50500}
|
||||
for i, price := range sellPrices {
|
||||
@@ -346,6 +346,6 @@ func (s *Server) seedOrderBook() {
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
log.Printf("📊 Seeded order book with %d initial orders", s.orderID)
|
||||
}
|
||||
}
|
||||
|
||||
+31
-54
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
// E2E FIX over ZeroMQ Benchmark against LX Cluster
|
||||
package main
|
||||
|
||||
@@ -17,7 +19,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
"github.com/luxfi/czmq/v4"
|
||||
)
|
||||
|
||||
// FIX Message Types
|
||||
@@ -147,7 +149,7 @@ func min(a, b int) int {
|
||||
|
||||
// Test 1: Direct HTTP API Test
|
||||
func testHTTPAPI(nodeURL string, messageCount int) TestResult {
|
||||
fmt.Printf("\n📡 Testing HTTP API: %s\n", nodeURL)
|
||||
fmt.Printf("\n[HTTP] Testing HTTP API: %s\n", nodeURL)
|
||||
|
||||
var processed int64
|
||||
var errors int64
|
||||
@@ -210,15 +212,7 @@ func testHTTPAPI(nodeURL string, messageCount int) TestResult {
|
||||
|
||||
// Test 2: Binary FIX over ZeroMQ
|
||||
func testFIXOverZMQ(zmqEndpoint string, messageCount int) TestResult {
|
||||
fmt.Printf("\n⚡ Testing Binary FIX over ZeroMQ: %s\n", zmqEndpoint)
|
||||
|
||||
// Create ZMQ context
|
||||
context, err := zmq.NewContext()
|
||||
if err != nil {
|
||||
log.Printf("Failed to create ZMQ context: %v", err)
|
||||
return TestResult{TestName: "FIX/ZMQ", ErrorCount: int64(messageCount)}
|
||||
}
|
||||
defer context.Term()
|
||||
fmt.Printf("\n[ZMQ] Testing Binary FIX over ZeroMQ: %s\n", zmqEndpoint)
|
||||
|
||||
var processed int64
|
||||
var errors int64
|
||||
@@ -237,19 +231,13 @@ func testFIXOverZMQ(zmqEndpoint string, messageCount int) TestResult {
|
||||
defer wg.Done()
|
||||
|
||||
// Create DEALER socket for async req/rep
|
||||
socket, err := context.NewSocket(zmq.DEALER)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&errors, int64(count))
|
||||
return
|
||||
}
|
||||
defer socket.Close()
|
||||
socket := czmq.NewSock(czmq.Dealer)
|
||||
defer socket.Destroy()
|
||||
|
||||
// Set socket options for low latency
|
||||
socket.SetSndhwm(10000)
|
||||
socket.SetRcvhwm(10000)
|
||||
socket.SetLinger(0)
|
||||
socket.SetTcpKeepalive(1)
|
||||
socket.SetTcpKeepaliveIdle(30)
|
||||
|
||||
// Connect to endpoint
|
||||
if err := socket.Connect(zmqEndpoint); err != nil {
|
||||
@@ -266,7 +254,7 @@ func testFIXOverZMQ(zmqEndpoint string, messageCount int) TestResult {
|
||||
orderStart := time.Now()
|
||||
|
||||
// Send binary FIX message
|
||||
if _, err := socket.SendBytes(data, zmq.DONTWAIT); err != nil {
|
||||
if err := socket.SendFrame(data, 0); err != nil {
|
||||
atomic.AddInt64(&errors, 1)
|
||||
continue
|
||||
}
|
||||
@@ -307,29 +295,18 @@ func testFIXOverZMQ(zmqEndpoint string, messageCount int) TestResult {
|
||||
|
||||
// Test 3: Batched Binary FIX over ZMQ
|
||||
func testBatchedFIXOverZMQ(zmqEndpoint string, messageCount int, batchSize int) TestResult {
|
||||
fmt.Printf("\n🚀 Testing Batched FIX over ZeroMQ (batch=%d): %s\n", batchSize, zmqEndpoint)
|
||||
|
||||
context, err := zmq.NewContext()
|
||||
if err != nil {
|
||||
log.Printf("Failed to create ZMQ context: %v", err)
|
||||
return TestResult{TestName: "Batched FIX/ZMQ", ErrorCount: int64(messageCount)}
|
||||
}
|
||||
defer context.Term()
|
||||
fmt.Printf("\n[ZMQ] Testing Batched FIX over ZeroMQ (batch=%d): %s\n", batchSize, zmqEndpoint)
|
||||
|
||||
var processed int64
|
||||
var errors int64
|
||||
latencies := make([]int64, 0, messageCount/batchSize)
|
||||
|
||||
// Create PUSH socket for one-way high throughput
|
||||
socket, err := context.NewSocket(zmq.PUSH)
|
||||
if err != nil {
|
||||
return TestResult{TestName: "Batched FIX/ZMQ", ErrorCount: int64(messageCount)}
|
||||
}
|
||||
defer socket.Close()
|
||||
socket := czmq.NewSock(czmq.Push)
|
||||
defer socket.Destroy()
|
||||
|
||||
// Optimize for throughput
|
||||
socket.SetSndhwm(100000)
|
||||
socket.SetSndtimeo(1000) // 1 second timeout
|
||||
|
||||
if err := socket.Connect(zmqEndpoint); err != nil {
|
||||
return TestResult{TestName: "Batched FIX/ZMQ", ErrorCount: int64(messageCount)}
|
||||
@@ -352,7 +329,7 @@ func testBatchedFIXOverZMQ(zmqEndpoint string, messageCount int, batchSize int)
|
||||
}
|
||||
|
||||
// Send entire batch
|
||||
if _, err := socket.SendBytes(batchBuf.Bytes(), 0); err != nil {
|
||||
if err := socket.SendFrame(batchBuf.Bytes(), 0); err != nil {
|
||||
atomic.AddInt64(&errors, int64(batchSize))
|
||||
} else {
|
||||
atomic.AddInt64(&processed, int64(batchSize))
|
||||
@@ -379,7 +356,7 @@ func testBatchedFIXOverZMQ(zmqEndpoint string, messageCount int, batchSize int)
|
||||
|
||||
// Test 4: E2E Cluster Test - Submit to all nodes
|
||||
func testClusterE2E(nodeURLs []string, messageCount int) TestResult {
|
||||
fmt.Printf("\n🌐 Testing E2E Cluster (K=3): %v\n", nodeURLs)
|
||||
fmt.Printf("\n[CLUSTER] Testing E2E Cluster (K=3): %v\n", nodeURLs)
|
||||
|
||||
var processed int64
|
||||
var errors int64
|
||||
@@ -510,7 +487,7 @@ func main() {
|
||||
fmt.Println(string(bytes.Repeat([]byte("-"), 100)))
|
||||
|
||||
for _, r := range results {
|
||||
fmt.Printf("%-30s | %10.0f/s | %8.2f μs | %8.2f μs | %7.1f%% | %6d\n",
|
||||
fmt.Printf("%-30s | %10.0f/s | %8.2f us | %8.2f us | %7.1f%% | %6d\n",
|
||||
r.TestName,
|
||||
r.Throughput,
|
||||
r.LatencyStats.Avg,
|
||||
@@ -528,7 +505,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n🏆 Best Throughput: %s (%.0f msgs/sec)\n", best.TestName, best.Throughput)
|
||||
fmt.Printf("\n[BEST] Best Throughput: %s (%.0f msgs/sec)\n", best.TestName, best.Throughput)
|
||||
|
||||
// Find lowest latency
|
||||
bestLatency := results[0]
|
||||
@@ -537,7 +514,7 @@ func main() {
|
||||
bestLatency = r
|
||||
}
|
||||
}
|
||||
fmt.Printf("⚡ Lowest P99 Latency: %s (%d μs)\n", bestLatency.TestName, bestLatency.LatencyStats.P99)
|
||||
fmt.Printf("[FAST] Lowest P99 Latency: %s (%d us)\n", bestLatency.TestName, bestLatency.LatencyStats.P99)
|
||||
}
|
||||
|
||||
// Save results
|
||||
@@ -549,7 +526,7 @@ func main() {
|
||||
if err := os.WriteFile(filename, data, 0644); err != nil {
|
||||
log.Printf("Failed to save results: %v", err)
|
||||
} else {
|
||||
fmt.Printf("\n📊 Results saved to: %s\n", filename)
|
||||
fmt.Printf("\n[SAVE] Results saved to: %s\n", filename)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,24 +542,24 @@ func main() {
|
||||
}
|
||||
avgThroughput /= float64(len(results))
|
||||
|
||||
fmt.Printf("\n📈 Average Throughput: %.0f msgs/sec\n", avgThroughput)
|
||||
fmt.Printf("\n[AVG] Average Throughput: %.0f msgs/sec\n", avgThroughput)
|
||||
|
||||
if avgThroughput < 100000 {
|
||||
fmt.Println("\n⚠️ Performance is below 100K msgs/sec. Consider:")
|
||||
fmt.Println(" • Enable kernel bypass (DPDK)")
|
||||
fmt.Println(" • Use RDMA for node communication")
|
||||
fmt.Println(" • Increase batch sizes")
|
||||
fmt.Println(" • Optimize network buffers")
|
||||
fmt.Println("\n[WARN] Performance is below 100K msgs/sec. Consider:")
|
||||
fmt.Println(" - Enable kernel bypass (DPDK)")
|
||||
fmt.Println(" - Use RDMA for node communication")
|
||||
fmt.Println(" - Increase batch sizes")
|
||||
fmt.Println(" - Optimize network buffers")
|
||||
} else if avgThroughput < 1000000 {
|
||||
fmt.Println("\n✅ Good performance! To reach 1M+ msgs/sec:")
|
||||
fmt.Println(" • Use binary protocols exclusively")
|
||||
fmt.Println(" • Enable GPU acceleration for matching")
|
||||
fmt.Println(" • Implement lock-free data structures")
|
||||
fmt.Println("\n[OK] Good performance! To reach 1M+ msgs/sec:")
|
||||
fmt.Println(" - Use binary protocols exclusively")
|
||||
fmt.Println(" - Enable GPU acceleration for matching")
|
||||
fmt.Println(" - Implement lock-free data structures")
|
||||
} else {
|
||||
fmt.Println("\n🎯 Excellent performance! You're achieving:")
|
||||
fmt.Printf(" • %.2fM messages/second\n", avgThroughput/1000000)
|
||||
fmt.Println(" • Near-hardware limits")
|
||||
fmt.Println(" • Ready for production deployment")
|
||||
fmt.Println("\n[EXCELLENT] Excellent performance! You're achieving:")
|
||||
fmt.Printf(" - %.2fM messages/second\n", avgThroughput/1000000)
|
||||
fmt.Println(" - Near-hardware limits")
|
||||
fmt.Println(" - Ready for production deployment")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -47,7 +47,7 @@ func main() {
|
||||
CacheEnabled: true,
|
||||
CacheTTLSeconds: 60,
|
||||
}
|
||||
|
||||
|
||||
gw := gateway.New(config)
|
||||
|
||||
// Register Lux native provider with RPC connectivity (highest priority)
|
||||
@@ -88,7 +88,7 @@ func main() {
|
||||
gateway.ChainIDZoo,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
if err := gw.RegisterProvider(uniswapProvider); err != nil {
|
||||
log.Fatalf("Failed to register Uniswap provider: %v", err)
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func main() {
|
||||
log.Printf("Gateway server starting on %s", *addr)
|
||||
log.Println("Provider priority: lux (10) > uniswap (100)")
|
||||
log.Println("Supported chains: Lux (96369), Zoo (200200), Ethereum (1), Arbitrum (42161), Optimism (10), Polygon (137), Base (8453), BNB (56)")
|
||||
|
||||
|
||||
if err := gw.StartWithGracefulShutdown(*addr); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
|
||||
+34
-42
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -13,8 +15,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/czmq/v4"
|
||||
"github.com/nats-io/nats.go"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
type ServerInfo struct {
|
||||
@@ -56,10 +58,10 @@ func main() {
|
||||
nodeID := fmt.Sprintf("%s-%d", hostname, os.Getpid())
|
||||
nodeIP := getLocalIP()
|
||||
|
||||
log.Printf("🚀 HYBRID Auto-Discovery (NATS + LX)")
|
||||
log.Printf("📍 Node ID: %s", nodeID)
|
||||
log.Printf("🌐 IP: %s", nodeIP)
|
||||
log.Printf("⚙️ Mode: %s", *mode)
|
||||
log.Printf("[HYBRID] Auto-Discovery (NATS + LX)")
|
||||
log.Printf("[NODE] Node ID: %s", nodeID)
|
||||
log.Printf("[NET] IP: %s", nodeIP)
|
||||
log.Printf("[MODE] Mode: %s", *mode)
|
||||
|
||||
// Auto-discover NATS
|
||||
natsConn := *natsURL
|
||||
@@ -70,7 +72,7 @@ func main() {
|
||||
// Connect to NATS for discovery only
|
||||
nc, err := nats.Connect(natsConn)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ No NATS found, starting local NATS server...")
|
||||
log.Printf("[WARN] No NATS found, starting local NATS server...")
|
||||
startLocalNATS()
|
||||
time.Sleep(2 * time.Second)
|
||||
nc, err = nats.Connect(nats.DefaultURL)
|
||||
@@ -104,7 +106,7 @@ func main() {
|
||||
}
|
||||
|
||||
func (n *HybridNode) autoMode(workers, traders int, rate int, duration time.Duration) {
|
||||
log.Println("🤖 AUTO MODE - Using NATS for discovery, LX for trading")
|
||||
log.Println("[AUTO] AUTO MODE - Using NATS for discovery, LX for trading")
|
||||
|
||||
// Wait for discovery
|
||||
time.Sleep(2 * time.Second)
|
||||
@@ -115,37 +117,33 @@ func (n *HybridNode) autoMode(workers, traders int, rate int, duration time.Dura
|
||||
|
||||
if !hasServer {
|
||||
// No server exists - I become THE server
|
||||
log.Println("🏆 No server found - becoming THE LX SERVER")
|
||||
log.Println("[LEADER] No server found - becoming THE LX SERVER")
|
||||
go n.runZMQServer(workers)
|
||||
time.Sleep(2 * time.Second) // Let server start
|
||||
// Also run traders
|
||||
n.runZMQTrader(traders, rate, duration)
|
||||
} else {
|
||||
// Server exists - connect via LX
|
||||
log.Printf("📍 Server found at %s - connecting via LX", n.activeServer.ZMQAddr)
|
||||
log.Printf("[CONNECT] Server found at %s - connecting via LX", n.activeServer.ZMQAddr)
|
||||
n.runZMQTrader(traders, rate, duration)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *HybridNode) runZMQServer(workers int) {
|
||||
n.isServer = true
|
||||
log.Printf("🏦 Starting LX Exchange Server on %s", n.zmqAddr)
|
||||
log.Printf("[SERVER] Starting LX Exchange Server on %s", n.zmqAddr)
|
||||
|
||||
// Announce via NATS
|
||||
go n.announcer()
|
||||
|
||||
// Start actual LX server
|
||||
context, _ := zmq.NewContext()
|
||||
defer context.Term()
|
||||
|
||||
// Router socket for clients
|
||||
router, _ := context.NewSocket(zmq.ROUTER)
|
||||
defer router.Close()
|
||||
router := czmq.NewSock(czmq.Router)
|
||||
defer router.Destroy()
|
||||
router.Bind(n.zmqAddr)
|
||||
|
||||
// Dealer socket for workers
|
||||
dealer, _ := context.NewSocket(zmq.DEALER)
|
||||
defer dealer.Close()
|
||||
dealer := czmq.NewSock(czmq.Dealer)
|
||||
defer dealer.Destroy()
|
||||
dealer.Bind("inproc://workers")
|
||||
|
||||
// Start worker goroutines
|
||||
@@ -159,23 +157,20 @@ func (n *HybridNode) runZMQServer(workers int) {
|
||||
}
|
||||
|
||||
// Proxy between router and dealer
|
||||
log.Printf("✅ LX server ready on %s", n.zmqAddr)
|
||||
zmq.Proxy(router, dealer, nil)
|
||||
log.Printf("[READY] LX server ready on %s", n.zmqAddr)
|
||||
czmq.Proxy(router, dealer, nil)
|
||||
}
|
||||
|
||||
func (n *HybridNode) zmqWorker(id int) {
|
||||
context, _ := zmq.NewContext()
|
||||
defer context.Term()
|
||||
|
||||
worker, _ := context.NewSocket(zmq.REP)
|
||||
defer worker.Close()
|
||||
worker := czmq.NewSock(czmq.Rep)
|
||||
defer worker.Destroy()
|
||||
worker.Connect("inproc://workers")
|
||||
|
||||
var ordersProcessed int64
|
||||
var tradesMatched int64
|
||||
|
||||
for {
|
||||
_, err := worker.RecvBytes(0)
|
||||
_, _, err := worker.RecvFrame()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
@@ -190,7 +185,7 @@ func (n *HybridNode) zmqWorker(id int) {
|
||||
// Send response
|
||||
response := fmt.Sprintf(`{"id":%d,"status":"accepted","matched":%v}`,
|
||||
ordersProcessed, ordersProcessed%2 == 0)
|
||||
worker.SendBytes([]byte(response), 0)
|
||||
worker.SendFrame([]byte(response), 0)
|
||||
|
||||
if ordersProcessed%100000 == 0 {
|
||||
log.Printf("Worker %d: Orders=%d, Trades=%d", id, ordersProcessed, tradesMatched)
|
||||
@@ -208,7 +203,7 @@ func (n *HybridNode) runZMQTrader(traders int, rate int, duration time.Duration)
|
||||
if server != nil || n.isServer {
|
||||
break
|
||||
}
|
||||
log.Println("⏳ Waiting for server discovery...")
|
||||
log.Println("[WAIT] Waiting for server discovery...")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
@@ -220,7 +215,7 @@ func (n *HybridNode) runZMQTrader(traders int, rate int, duration time.Duration)
|
||||
}
|
||||
n.serverMu.RUnlock()
|
||||
|
||||
log.Printf("💹 Starting %d LX traders connecting to %s", traders, serverAddr)
|
||||
log.Printf("[TRADER] Starting %d LX traders connecting to %s", traders, serverAddr)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var totalOrders int64
|
||||
@@ -233,11 +228,8 @@ func (n *HybridNode) runZMQTrader(traders int, rate int, duration time.Duration)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
context, _ := zmq.NewContext()
|
||||
defer context.Term()
|
||||
|
||||
socket, _ := context.NewSocket(zmq.REQ)
|
||||
defer socket.Close()
|
||||
socket := czmq.NewSock(czmq.Req)
|
||||
defer socket.Destroy()
|
||||
|
||||
socket.Connect(serverAddr)
|
||||
|
||||
@@ -250,14 +242,14 @@ func (n *HybridNode) runZMQTrader(traders int, rate int, duration time.Duration)
|
||||
|
||||
// Send order (binary format for speed)
|
||||
order := fmt.Sprintf(`{"id":%d,"symbol":"BTC/USD","side":"buy","price":50000,"qty":1}`, orderID)
|
||||
_, err := socket.SendBytes([]byte(order), 0)
|
||||
err := socket.SendFrame([]byte(order), 0)
|
||||
if err != nil {
|
||||
atomic.AddInt64(&totalErrors, 1)
|
||||
continue
|
||||
}
|
||||
|
||||
// Receive response
|
||||
_, err = socket.RecvBytes(0)
|
||||
_, _, err = socket.RecvFrame()
|
||||
if err != nil {
|
||||
atomic.AddInt64(&totalErrors, 1)
|
||||
continue
|
||||
@@ -283,7 +275,7 @@ func (n *HybridNode) runZMQTrader(traders int, rate int, duration time.Duration)
|
||||
errors := atomic.LoadInt64(&totalErrors)
|
||||
delta := orders - lastOrders
|
||||
|
||||
log.Printf("📈 ZMQ Orders: %d | Rate: %d/sec | Errors: %d",
|
||||
log.Printf("[STATS] ZMQ Orders: %d | Rate: %d/sec | Errors: %d",
|
||||
orders, delta, errors)
|
||||
lastOrders = orders
|
||||
}
|
||||
@@ -292,7 +284,7 @@ func (n *HybridNode) runZMQTrader(traders int, rate int, duration time.Duration)
|
||||
wg.Wait()
|
||||
finalOrders := atomic.LoadInt64(&totalOrders)
|
||||
elapsed := time.Since(startTime).Seconds()
|
||||
log.Printf("✅ Trading complete: %d orders in %.1fs = %.0f orders/sec",
|
||||
log.Printf("[DONE] Trading complete: %d orders in %.1fs = %.0f orders/sec",
|
||||
finalOrders, elapsed, float64(finalOrders)/elapsed)
|
||||
}
|
||||
|
||||
@@ -312,7 +304,7 @@ func (n *HybridNode) announcer() {
|
||||
|
||||
data, _ := json.Marshal(info)
|
||||
n.nc.Publish("hybrid.server", data)
|
||||
log.Printf("📡 Announcing ZMQ server at %s", n.zmqAddr)
|
||||
log.Printf("[ANNOUNCE] Announcing ZMQ server at %s", n.zmqAddr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +317,7 @@ func (n *HybridNode) discoverer() {
|
||||
n.serverMu.Lock()
|
||||
if n.activeServer == nil || info.Timestamp.Before(n.activeServer.Timestamp) {
|
||||
n.activeServer = &info
|
||||
log.Printf("🔍 Discovered ZMQ server: %s at %s", info.ID, info.ZMQAddr)
|
||||
log.Printf("[DISCOVER] Discovered ZMQ server: %s at %s", info.ID, info.ZMQAddr)
|
||||
}
|
||||
n.serverMu.Unlock()
|
||||
}
|
||||
@@ -360,7 +352,7 @@ func discoverNATS() string {
|
||||
nc, err := nats.Connect(loc, nats.Timeout(1*time.Second))
|
||||
if err == nil {
|
||||
nc.Close()
|
||||
log.Printf("✅ Found NATS at %s", loc)
|
||||
log.Printf("[FOUND] Found NATS at %s", loc)
|
||||
return loc
|
||||
}
|
||||
}
|
||||
@@ -374,6 +366,6 @@ func startLocalNATS() {
|
||||
if err != nil {
|
||||
log.Printf("Could not start NATS server: %v", err)
|
||||
} else {
|
||||
log.Println("✅ Started local NATS server on port 4222")
|
||||
log.Println("[NATS] Started local NATS server on port 4222")
|
||||
}
|
||||
}
|
||||
|
||||
+26
-35
@@ -7,8 +7,8 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/accel/ops/dex"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/mlx"
|
||||
)
|
||||
|
||||
// Memory analysis for MLX matching engine and orderbook
|
||||
@@ -43,8 +43,8 @@ func main() {
|
||||
// Test 4: 100K orderbooks (if memory allows)
|
||||
test100KOrderbooks()
|
||||
|
||||
// Test 5: MLX Engine memory usage
|
||||
testMLXEngineMemory()
|
||||
// Test 5: Accel Engine memory usage
|
||||
testAccelEngineMemory()
|
||||
|
||||
// Extrapolate to 1M markets
|
||||
extrapolateTo1Million()
|
||||
@@ -60,9 +60,9 @@ func analyzeTheoreticalMemory() {
|
||||
fmt.Printf("Size of OrderBook struct: %d bytes\n", unsafe.Sizeof(lx.OrderBook{}))
|
||||
fmt.Printf("Size of PriceLevel struct: %d bytes\n", unsafe.Sizeof(lx.PriceLevel{}))
|
||||
|
||||
// MLX structures
|
||||
fmt.Printf("Size of MLX Order: %d bytes\n", unsafe.Sizeof(mlx.Order{}))
|
||||
fmt.Printf("Size of MLX Trade: %d bytes\n", unsafe.Sizeof(mlx.Trade{}))
|
||||
// Accel/dex structures
|
||||
fmt.Printf("Size of dex.Order: %d bytes\n", unsafe.Sizeof(dex.Order{}))
|
||||
fmt.Printf("Size of dex.Trade: %d bytes\n", unsafe.Sizeof(dex.Trade{}))
|
||||
|
||||
fmt.Println("\n--- Per-Market Memory Requirements ---")
|
||||
|
||||
@@ -277,52 +277,43 @@ func test100KOrderbooks() {
|
||||
float64(used)/(100000*1024))
|
||||
}
|
||||
|
||||
func testMLXEngineMemory() {
|
||||
fmt.Println("\n--- MLX Engine Memory Test ---")
|
||||
func testAccelEngineMemory() {
|
||||
fmt.Println("\n--- Accel/DEX Engine Memory Test ---")
|
||||
|
||||
runtime.GC()
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
before := m.Alloc
|
||||
|
||||
// Create MLX engine
|
||||
engine, err := mlx.NewEngine(mlx.Config{
|
||||
Backend: mlx.BackendCPU,
|
||||
MaxBatch: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create MLX engine: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
// Create batch of orders
|
||||
bids := make([]mlx.Order, 1000)
|
||||
asks := make([]mlx.Order, 1000)
|
||||
// Create batch of orders using accel/ops/dex
|
||||
bids := make([]dex.Order, 1000)
|
||||
asks := make([]dex.Order, 1000)
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
bids[i] = mlx.Order{
|
||||
ID: uint64(i),
|
||||
Side: 0,
|
||||
Price: 100 - float64(i%10),
|
||||
Size: 10,
|
||||
bids[i] = dex.Order{
|
||||
ID: uint64(i),
|
||||
Side: dex.Bid,
|
||||
Price: uint64(100-i%10) * 1e8,
|
||||
Quantity: 10 * 1e8,
|
||||
Remaining: 10 * 1e8,
|
||||
}
|
||||
asks[i] = mlx.Order{
|
||||
ID: uint64(i + 1000),
|
||||
Side: 1,
|
||||
Price: 101 + float64(i%10),
|
||||
Size: 10,
|
||||
asks[i] = dex.Order{
|
||||
ID: uint64(i + 1000),
|
||||
Side: dex.Ask,
|
||||
Price: uint64(101+i%10) * 1e8,
|
||||
Quantity: 10 * 1e8,
|
||||
Remaining: 10 * 1e8,
|
||||
}
|
||||
}
|
||||
|
||||
// Run batch matching
|
||||
trades := engine.BatchMatch(bids, asks)
|
||||
// Run batch matching using accel
|
||||
trades, _, _ := dex.MatchOrders(bids, asks, nil)
|
||||
|
||||
runtime.ReadMemStats(&m)
|
||||
after := m.Alloc
|
||||
used := after - before
|
||||
|
||||
fmt.Printf("MLX Engine memory overhead: %d bytes (%.2f KB)\n",
|
||||
fmt.Printf("Accel/DEX memory overhead: %d bytes (%.2f KB)\n",
|
||||
used, float64(used)/1024)
|
||||
fmt.Printf("Trades generated: %d\n", len(trades))
|
||||
}
|
||||
|
||||
+24
-30
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
// Multi-node DEX deployment test with 3+ nodes
|
||||
package main
|
||||
|
||||
@@ -10,16 +12,16 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/czmq/v4"
|
||||
"github.com/nats-io/nats.go"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
ID string
|
||||
Type NodeType
|
||||
NatsConn *nats.Conn
|
||||
ZmqPub *zmq.Socket
|
||||
ZmqSub *zmq.Socket
|
||||
ZmqPub *czmq.Sock
|
||||
ZmqSub *czmq.Sock
|
||||
Orders int64
|
||||
Trades int64
|
||||
IsLeader bool
|
||||
@@ -50,8 +52,8 @@ func main() {
|
||||
duration := flag.Duration("duration", 30*time.Second, "Test duration")
|
||||
flag.Parse()
|
||||
|
||||
log.Printf("🚀 Multi-Node DEX Test")
|
||||
log.Printf("📊 Nodes: %d | Duration: %v", *numNodes, *duration)
|
||||
log.Printf("[START] Multi-Node DEX Test")
|
||||
log.Printf("[CONFIG] Nodes: %d | Duration: %v", *numNodes, *duration)
|
||||
|
||||
cm := &ClusterManager{
|
||||
nodes: make(map[string]*Node),
|
||||
@@ -110,7 +112,7 @@ func (cm *ClusterManager) startNode(nodeID string, nodeType NodeType, nodeNum in
|
||||
// Connect to NATS
|
||||
nc, err := nats.Connect(cm.natsURL)
|
||||
if err != nil {
|
||||
log.Printf("❌ Node %s: Failed to connect to NATS: %v", nodeID, err)
|
||||
log.Printf("[ERROR] Node %s: Failed to connect to NATS: %v", nodeID, err)
|
||||
return
|
||||
}
|
||||
node.NatsConn = nc
|
||||
@@ -120,20 +122,12 @@ func (cm *ClusterManager) startNode(nodeID string, nodeType NodeType, nodeNum in
|
||||
subPort := cm.zmqSubPort + nodeNum
|
||||
|
||||
// Publisher socket for market data
|
||||
pub, err := zmq.NewSocket(zmq.PUB)
|
||||
if err != nil {
|
||||
log.Printf("❌ Node %s: Failed to create ZMQ PUB: %v", nodeID, err)
|
||||
return
|
||||
}
|
||||
pub := czmq.NewSock(czmq.Pub)
|
||||
pub.Bind(fmt.Sprintf("tcp://*:%d", pubPort))
|
||||
node.ZmqPub = pub
|
||||
|
||||
// Subscriber socket for receiving data
|
||||
sub, err := zmq.NewSocket(zmq.SUB)
|
||||
if err != nil {
|
||||
log.Printf("❌ Node %s: Failed to create ZMQ SUB: %v", nodeID, err)
|
||||
return
|
||||
}
|
||||
sub := czmq.NewSock(czmq.Sub)
|
||||
|
||||
// Subscribe to other nodes
|
||||
for i := 0; i < 10; i++ {
|
||||
@@ -158,7 +152,7 @@ func (cm *ClusterManager) startNode(nodeID string, nodeType NodeType, nodeNum in
|
||||
cm.nodes[nodeID] = node
|
||||
cm.mu.Unlock()
|
||||
|
||||
log.Printf("✅ Node %s started (Type: %v, ZMQ Pub: %d, Sub: %d)",
|
||||
log.Printf("[OK] Node %s started (Type: %v, ZMQ Pub: %d, Sub: %d)",
|
||||
nodeID, nodeType, pubPort, subPort)
|
||||
|
||||
// Start node operations
|
||||
@@ -188,7 +182,7 @@ func (node *Node) processOrders() {
|
||||
// Publish trade via ZeroMQ
|
||||
trade := fmt.Sprintf(`{"type":"trade","id":%d,"price":50000,"qty":1}`,
|
||||
atomic.LoadInt64(&node.Trades))
|
||||
node.ZmqPub.Send(trade, 0)
|
||||
node.ZmqPub.SendFrame([]byte(trade), 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,21 +206,21 @@ func (node *Node) publishMarketData() {
|
||||
atomic.LoadInt64(&node.Trades),
|
||||
time.Now().UnixNano())
|
||||
|
||||
node.ZmqPub.Send(snapshot, zmq.DONTWAIT)
|
||||
node.ZmqPub.SendFrame([]byte(snapshot), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (node *Node) subscribeToMarketData() {
|
||||
for {
|
||||
msg, err := node.ZmqSub.Recv(0)
|
||||
msg, _, err := node.ZmqSub.RecvFrame()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process market data from other nodes
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(msg), &data); err == nil {
|
||||
if err := json.Unmarshal(msg, &data); err == nil {
|
||||
if data["type"] == "trade" && !node.IsLeader {
|
||||
// Replicas track trades from leader
|
||||
atomic.AddInt64(&node.Trades, 1)
|
||||
@@ -257,7 +251,7 @@ func (cm *ClusterManager) monitorCluster() {
|
||||
for range ticker.C {
|
||||
cm.mu.RLock()
|
||||
|
||||
log.Println("\n📊 Cluster Status:")
|
||||
log.Println("\n[CLUSTER] Cluster Status:")
|
||||
log.Println("================")
|
||||
|
||||
totalOrders := int64(0)
|
||||
@@ -270,9 +264,9 @@ func (cm *ClusterManager) monitorCluster() {
|
||||
totalOrders += orders
|
||||
totalTrades += trades
|
||||
|
||||
status := "✅"
|
||||
status := "[OK]"
|
||||
if time.Since(node.LastHeartbeat) > 5*time.Second {
|
||||
status = "⚠️"
|
||||
status = "[WARN]"
|
||||
} else {
|
||||
activeNodes++
|
||||
}
|
||||
@@ -286,11 +280,11 @@ func (cm *ClusterManager) monitorCluster() {
|
||||
status, node.ID, role, orders, trades)
|
||||
}
|
||||
|
||||
log.Printf("\n📈 Total: %d orders, %d trades across %d active nodes",
|
||||
log.Printf("\n[TOTAL] Total: %d orders, %d trades across %d active nodes",
|
||||
totalOrders, totalTrades, activeNodes)
|
||||
|
||||
if totalOrders > 0 {
|
||||
log.Printf("💹 Trade Rate: %.2f%%",
|
||||
log.Printf("[RATE] Trade Rate: %.2f%%",
|
||||
float64(totalTrades)/float64(totalOrders)*100)
|
||||
}
|
||||
|
||||
@@ -302,7 +296,7 @@ func (cm *ClusterManager) runWorkload(duration time.Duration) {
|
||||
// Connect as client
|
||||
nc, err := nats.Connect(cm.natsURL)
|
||||
if err != nil {
|
||||
log.Printf("❌ Failed to connect workload generator: %v", err)
|
||||
log.Printf("[ERROR] Failed to connect workload generator: %v", err)
|
||||
return
|
||||
}
|
||||
defer nc.Close()
|
||||
@@ -336,7 +330,7 @@ func (cm *ClusterManager) printResults() {
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||||
fmt.Println("📊 MULTI-NODE TEST RESULTS")
|
||||
fmt.Println("[RESULTS] MULTI-NODE TEST RESULTS")
|
||||
fmt.Println(strings.Repeat("=", 60))
|
||||
|
||||
totalOrders := int64(0)
|
||||
@@ -379,10 +373,10 @@ func (cm *ClusterManager) shutdown() {
|
||||
node.NatsConn.Close()
|
||||
}
|
||||
if node.ZmqPub != nil {
|
||||
node.ZmqPub.Close()
|
||||
node.ZmqPub.Destroy()
|
||||
}
|
||||
if node.ZmqSub != nil {
|
||||
node.ZmqSub.Close()
|
||||
node.ZmqSub.Destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-13
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -10,13 +12,13 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/czmq/v4"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// TestClient represents a client that sends orders to the multi-node cluster
|
||||
type TestClient struct {
|
||||
dealers []*zmq.Socket
|
||||
dealers []*czmq.Sock
|
||||
nodeAddrs []string
|
||||
ordersent uint64
|
||||
responsesRcv uint64
|
||||
@@ -27,15 +29,12 @@ type TestClient struct {
|
||||
func NewTestClient(nodeAddrs []string) (*TestClient, error) {
|
||||
client := &TestClient{
|
||||
nodeAddrs: nodeAddrs,
|
||||
dealers: make([]*zmq.Socket, len(nodeAddrs)),
|
||||
dealers: make([]*czmq.Sock, len(nodeAddrs)),
|
||||
}
|
||||
|
||||
// Create DEALER socket for each node
|
||||
for i, addr := range nodeAddrs {
|
||||
dealer, err := zmq.NewSocket(zmq.DEALER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create dealer socket: %w", err)
|
||||
}
|
||||
dealer := czmq.NewSock(czmq.Dealer)
|
||||
|
||||
// Set identity for the dealer
|
||||
dealer.SetIdentity(fmt.Sprintf("client-%d-%d", time.Now().Unix(), i))
|
||||
@@ -64,8 +63,8 @@ func (c *TestClient) SendOrder(order *lx.Order) error {
|
||||
}
|
||||
|
||||
// Send to dealer (no identity needed, DEALER handles it)
|
||||
dealer.Send("", zmq.SNDMORE)
|
||||
if _, err := dealer.SendBytes(data, 0); err != nil {
|
||||
dealer.SendFrame([]byte(""), czmq.FlagMore)
|
||||
if err := dealer.SendFrame(data, 0); err != nil {
|
||||
return fmt.Errorf("failed to send order: %w", err)
|
||||
}
|
||||
|
||||
@@ -78,14 +77,15 @@ func (c *TestClient) SendOrder(order *lx.Order) error {
|
||||
}
|
||||
|
||||
// receiveResponse receives response from node
|
||||
func (c *TestClient) receiveResponse(dealer *zmq.Socket) {
|
||||
func (c *TestClient) receiveResponse(dealer *czmq.Sock) {
|
||||
// Empty delimiter
|
||||
if _, err := dealer.Recv(zmq.DONTWAIT); err != nil {
|
||||
_, _, err := dealer.RecvFrame()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Response data
|
||||
data, err := dealer.RecvBytes(zmq.DONTWAIT)
|
||||
data, _, err := dealer.RecvFrame()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (c *TestClient) RunLoadTest(duration time.Duration, ordersPerSec int) {
|
||||
func (c *TestClient) Close() {
|
||||
for _, dealer := range c.dealers {
|
||||
if dealer != nil {
|
||||
dealer.Close()
|
||||
dealer.Destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+31
-56
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -14,8 +16,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/czmq/v4"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// NodeConfig represents configuration for a single node
|
||||
@@ -33,10 +35,10 @@ type NodeConfig struct {
|
||||
type MultiNode struct {
|
||||
config *NodeConfig
|
||||
orderBook *lx.OrderBook
|
||||
pubSocket *zmq.Socket
|
||||
subSocket *zmq.Socket
|
||||
routerSocket *zmq.Socket
|
||||
dealerSocket *zmq.Socket
|
||||
pubSocket *czmq.Sock
|
||||
subSocket *czmq.Sock
|
||||
routerSocket *czmq.Sock
|
||||
dealerSocket *czmq.Sock
|
||||
|
||||
// Metrics
|
||||
ordersProcessed uint64
|
||||
@@ -82,48 +84,32 @@ func NewMultiNode(config *NodeConfig) (*MultiNode, error) {
|
||||
}
|
||||
|
||||
// Initialize ZMQ sockets
|
||||
var err error
|
||||
|
||||
// Publisher socket for broadcasting events
|
||||
node.pubSocket, err = zmq.NewSocket(zmq.PUB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create PUB socket: %w", err)
|
||||
}
|
||||
err = node.pubSocket.Bind(fmt.Sprintf("tcp://*:%d", config.PubPort))
|
||||
if err != nil {
|
||||
node.pubSocket = czmq.NewSock(czmq.Pub)
|
||||
if _, err := node.pubSocket.Bind(fmt.Sprintf("tcp://*:%d", config.PubPort)); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind PUB socket: %w", err)
|
||||
}
|
||||
|
||||
// Subscriber socket for receiving events from peers
|
||||
node.subSocket, err = zmq.NewSocket(zmq.SUB)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SUB socket: %w", err)
|
||||
}
|
||||
node.subSocket = czmq.NewSock(czmq.Sub)
|
||||
node.subSocket.SetSubscribe("")
|
||||
|
||||
// Connect to peer publishers
|
||||
for _, peer := range config.Peers {
|
||||
err = node.subSocket.Connect(peer)
|
||||
if err != nil {
|
||||
if err := node.subSocket.Connect(peer); err != nil {
|
||||
log.Printf("Failed to connect to peer %s: %v", peer, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Router socket for receiving orders (server mode)
|
||||
node.routerSocket, err = zmq.NewSocket(zmq.ROUTER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create ROUTER socket: %w", err)
|
||||
}
|
||||
err = node.routerSocket.Bind(fmt.Sprintf("tcp://*:%d", config.RouterPort))
|
||||
if err != nil {
|
||||
node.routerSocket = czmq.NewSock(czmq.Router)
|
||||
if _, err := node.routerSocket.Bind(fmt.Sprintf("tcp://*:%d", config.RouterPort)); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind ROUTER socket: %w", err)
|
||||
}
|
||||
|
||||
// Dealer socket for load balancing (client mode)
|
||||
node.dealerSocket, err = zmq.NewSocket(zmq.DEALER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create DEALER socket: %w", err)
|
||||
}
|
||||
node.dealerSocket = czmq.NewSock(czmq.Dealer)
|
||||
node.dealerSocket.SetIdentity(config.NodeID)
|
||||
|
||||
return node, nil
|
||||
@@ -150,30 +136,19 @@ func (n *MultiNode) Start() {
|
||||
|
||||
// handleSubscriptions processes messages from peer nodes
|
||||
func (n *MultiNode) handleSubscriptions() {
|
||||
poller := zmq.NewPoller()
|
||||
poller.Add(n.subSocket, zmq.POLLIN)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-n.ctx.Done():
|
||||
return
|
||||
default:
|
||||
sockets, err := poller.Poll(100 * time.Millisecond)
|
||||
msg, _, err := n.subSocket.RecvFrame()
|
||||
if err != nil {
|
||||
log.Printf("Poller error: %v", err)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, socket := range sockets {
|
||||
msg, err := socket.Socket.RecvBytes(0)
|
||||
if err != nil {
|
||||
log.Printf("Receive error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
atomic.AddUint64(&n.messagesReceived, 1)
|
||||
n.processMessage(msg)
|
||||
}
|
||||
atomic.AddUint64(&n.messagesReceived, 1)
|
||||
n.processMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,20 +161,20 @@ func (n *MultiNode) handleRouterRequests() {
|
||||
return
|
||||
default:
|
||||
// Receive multi-part message [identity, empty, data]
|
||||
identity, err := n.routerSocket.Recv(0)
|
||||
identity, _, err := n.routerSocket.RecvFrame()
|
||||
if err != nil {
|
||||
log.Printf("Router receive error: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Empty delimiter
|
||||
_, err = n.routerSocket.Recv(0)
|
||||
_, _, err = n.routerSocket.RecvFrame()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Actual message
|
||||
data, err := n.routerSocket.RecvBytes(0)
|
||||
data, _, err := n.routerSocket.RecvFrame()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -208,9 +183,9 @@ func (n *MultiNode) handleRouterRequests() {
|
||||
response := n.processOrderRequest(data)
|
||||
|
||||
// Send response back [identity, empty, response]
|
||||
n.routerSocket.Send(identity, zmq.SNDMORE)
|
||||
n.routerSocket.Send("", zmq.SNDMORE)
|
||||
n.routerSocket.SendBytes(response, 0)
|
||||
n.routerSocket.SendFrame(identity, czmq.FlagMore)
|
||||
n.routerSocket.SendFrame([]byte(""), czmq.FlagMore)
|
||||
n.routerSocket.SendFrame(response, 0)
|
||||
|
||||
atomic.AddUint64(&n.ordersProcessed, 1)
|
||||
}
|
||||
@@ -292,7 +267,7 @@ func (n *MultiNode) broadcastTrades(trades []lx.Trade) {
|
||||
return
|
||||
}
|
||||
|
||||
n.pubSocket.SendBytes(data, zmq.DONTWAIT)
|
||||
n.pubSocket.SendFrame(data, 0)
|
||||
atomic.AddUint64(&n.messagesSent, 1)
|
||||
}
|
||||
|
||||
@@ -317,7 +292,7 @@ func (n *MultiNode) heartbeatLoop() {
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(msg)
|
||||
n.pubSocket.SendBytes(data, zmq.DONTWAIT)
|
||||
n.pubSocket.SendFrame(data, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,7 +327,7 @@ func (n *MultiNode) broadcastStateSync() {
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(msg)
|
||||
n.pubSocket.SendBytes(data, zmq.DONTWAIT)
|
||||
n.pubSocket.SendFrame(data, 0)
|
||||
atomic.AddUint64(&n.messagesSent, 1)
|
||||
}
|
||||
|
||||
@@ -404,16 +379,16 @@ func (n *MultiNode) cleanup() {
|
||||
log.Printf("Node %s shutting down...", n.config.NodeID)
|
||||
|
||||
if n.pubSocket != nil {
|
||||
n.pubSocket.Close()
|
||||
n.pubSocket.Destroy()
|
||||
}
|
||||
if n.subSocket != nil {
|
||||
n.subSocket.Close()
|
||||
n.subSocket.Destroy()
|
||||
}
|
||||
if n.routerSocket != nil {
|
||||
n.routerSocket.Close()
|
||||
n.routerSocket.Destroy()
|
||||
}
|
||||
if n.dealerSocket != nil {
|
||||
n.dealerSocket.Close()
|
||||
n.dealerSocket.Destroy()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-16
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -11,13 +13,13 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/czmq/v4"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// TestClient represents a client that sends orders to the multi-node cluster
|
||||
type TestClient struct {
|
||||
dealers []*zmq.Socket
|
||||
dealers []*czmq.Sock
|
||||
nodeAddrs []string
|
||||
ordersSent uint64
|
||||
responsesRcv uint64
|
||||
@@ -28,15 +30,12 @@ type TestClient struct {
|
||||
func NewTestClient(nodeAddrs []string) (*TestClient, error) {
|
||||
client := &TestClient{
|
||||
nodeAddrs: nodeAddrs,
|
||||
dealers: make([]*zmq.Socket, len(nodeAddrs)),
|
||||
dealers: make([]*czmq.Sock, len(nodeAddrs)),
|
||||
}
|
||||
|
||||
// Create DEALER socket for each node
|
||||
for i, addr := range nodeAddrs {
|
||||
dealer, err := zmq.NewSocket(zmq.DEALER)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create dealer socket: %w", err)
|
||||
}
|
||||
dealer := czmq.NewSock(czmq.Dealer)
|
||||
|
||||
// Set identity for the dealer
|
||||
dealer.SetIdentity(fmt.Sprintf("client-%d-%d", time.Now().Unix(), i))
|
||||
@@ -65,8 +64,8 @@ func (c *TestClient) SendOrder(order *lx.Order) error {
|
||||
}
|
||||
|
||||
// Send to dealer (no identity needed, DEALER handles it)
|
||||
dealer.Send("", zmq.SNDMORE)
|
||||
if _, err := dealer.SendBytes(data, 0); err != nil {
|
||||
dealer.SendFrame([]byte(""), czmq.FlagMore)
|
||||
if err := dealer.SendFrame(data, 0); err != nil {
|
||||
return fmt.Errorf("failed to send order: %w", err)
|
||||
}
|
||||
|
||||
@@ -79,17 +78,15 @@ func (c *TestClient) SendOrder(order *lx.Order) error {
|
||||
}
|
||||
|
||||
// receiveResponse receives response from node
|
||||
func (c *TestClient) receiveResponse(dealer *zmq.Socket) {
|
||||
// Set timeout for receive
|
||||
dealer.SetRcvtimeo(100 * time.Millisecond)
|
||||
|
||||
func (c *TestClient) receiveResponse(dealer *czmq.Sock) {
|
||||
// Empty delimiter
|
||||
if _, err := dealer.Recv(0); err != nil {
|
||||
_, _, err := dealer.RecvFrame()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Response data
|
||||
data, err := dealer.RecvBytes(0)
|
||||
data, _, err := dealer.RecvFrame()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -145,7 +142,7 @@ func (c *TestClient) RunLoadTest(duration time.Duration, ordersPerSec int) {
|
||||
func (c *TestClient) Close() {
|
||||
for _, dealer := range c.dealers {
|
||||
if dealer != nil {
|
||||
dealer.Close()
|
||||
dealer.Destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
// LX Exchange Server - Receives orders via ZMQ and processes them
|
||||
package main
|
||||
|
||||
@@ -9,7 +11,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
"github.com/luxfi/czmq/v4"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
@@ -36,21 +38,19 @@ func main() {
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
fmt.Printf("🚀 ZMQ Exchange Server\n")
|
||||
fmt.Printf("[START] ZMQ Exchange Server\n")
|
||||
fmt.Printf("Binding to: %s\n", *bindAddr)
|
||||
fmt.Printf("Workers: %d\n", *workers)
|
||||
fmt.Println("----------------------------------------")
|
||||
|
||||
// Create PULL socket for receiving orders
|
||||
context, _ := zmq.NewContext()
|
||||
socket, _ := context.NewSocket(zmq.PULL)
|
||||
defer socket.Close()
|
||||
socket := czmq.NewSock(czmq.Pull)
|
||||
defer socket.Destroy()
|
||||
|
||||
// Set high water mark for better throughput
|
||||
socket.SetRcvhwm(100000)
|
||||
socket.SetRcvbuf(8 * 1024 * 1024) // 8MB buffer
|
||||
|
||||
if err := socket.Bind(*bindAddr); err != nil {
|
||||
if _, err := socket.Bind(*bindAddr); err != nil {
|
||||
log.Fatalf("Failed to bind: %v", err)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func main() {
|
||||
mbPerSec := float64(bytes) / (elapsed * 1024 * 1024)
|
||||
gbpsUsed := (float64(bytes) * 8) / (elapsed * 1e9)
|
||||
|
||||
fmt.Printf("\r📊 Orders: %d | Rate: %.0f/sec | Network: %.2f MB/s (%.3f Gbps) | Avg Size: %d bytes",
|
||||
fmt.Printf("\r[STATS] Orders: %d | Rate: %.0f/sec | Network: %.2f MB/s (%.3f Gbps) | Avg Size: %d bytes",
|
||||
orders, ordersPerSec, mbPerSec, gbpsUsed,
|
||||
func() uint64 {
|
||||
if orders > 0 {
|
||||
@@ -88,7 +88,7 @@ func main() {
|
||||
|
||||
// Main receive loop
|
||||
for {
|
||||
msg, err := socket.RecvBytes(0)
|
||||
msg, _, err := socket.RecvFrame()
|
||||
if err != nil {
|
||||
log.Printf("Receive error: %v", err)
|
||||
continue
|
||||
|
||||
+87
-120
@@ -1,4 +1,6 @@
|
||||
// ZMQ-MLX Server - GPU-accelerated order matching over 10Gbps fiber
|
||||
//go:build zmqtest
|
||||
|
||||
// ZMQ-Accel Server - GPU-accelerated order matching over 10Gbps fiber
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -14,9 +16,9 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/accel/ops/dex"
|
||||
"github.com/luxfi/czmq/v4"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/dex/pkg/mlx"
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
)
|
||||
|
||||
// Binary protocol for maximum speed (fixed 64-byte messages)
|
||||
@@ -56,18 +58,17 @@ type Stats struct {
|
||||
errors uint64
|
||||
}
|
||||
|
||||
type MLXZMQServer struct {
|
||||
mlxEngine mlx.Engine
|
||||
type AccelZMQServer struct {
|
||||
orderBooks map[uint32]*lx.OrderBook
|
||||
stats *Stats
|
||||
|
||||
// ZMQ sockets
|
||||
orderSocket *zmq.Socket // PULL socket for orders (many-to-one)
|
||||
tradeSocket *zmq.Socket // PUB socket for trades (one-to-many)
|
||||
marketSocket *zmq.Socket // PUB socket for market data
|
||||
cmdSocket *zmq.Socket // REP socket for commands
|
||||
orderSocket *czmq.Sock // PULL socket for orders (many-to-one)
|
||||
tradeSocket *czmq.Sock // PUB socket for trades (one-to-many)
|
||||
marketSocket *czmq.Sock // PUB socket for market data
|
||||
cmdSocket *czmq.Sock // REP socket for commands
|
||||
|
||||
// Batching for MLX
|
||||
// Batching for accel
|
||||
orderBatch []OrderMessage
|
||||
batchMutex sync.Mutex
|
||||
batchSize int
|
||||
@@ -80,19 +81,10 @@ type MLXZMQServer struct {
|
||||
hwm int
|
||||
}
|
||||
|
||||
func NewMLXZMQServer(config *Config) (*MLXZMQServer, error) {
|
||||
// Initialize MLX engine
|
||||
mlxEngine, err := mlx.NewEngine(mlx.Config{
|
||||
Backend: mlx.BackendAuto,
|
||||
MaxBatch: config.BatchSize,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create MLX engine: %w", err)
|
||||
}
|
||||
log.Printf("MLX Engine initialized: Backend=%s Device=%s", mlxEngine.Backend(), mlxEngine.Device())
|
||||
func NewAccelZMQServer(config *Config) (*AccelZMQServer, error) {
|
||||
log.Printf("Accel Engine initialized with GPU acceleration via luxfi/accel")
|
||||
|
||||
server := &MLXZMQServer{
|
||||
mlxEngine: mlxEngine,
|
||||
server := &AccelZMQServer{
|
||||
orderBooks: make(map[uint32]*lx.OrderBook),
|
||||
stats: &Stats{},
|
||||
orderBatch: make([]OrderMessage, 0, config.BatchSize),
|
||||
@@ -103,69 +95,44 @@ func NewMLXZMQServer(config *Config) (*MLXZMQServer, error) {
|
||||
tcpNoDelay: config.TCPNoDelay,
|
||||
}
|
||||
|
||||
// Setup ZMQ context with optimizations
|
||||
zmq.SetMaxSockets(100000)
|
||||
|
||||
// Order receiver (PULL) - many traders can connect
|
||||
orderSocket, err := zmq.NewSocket(zmq.PULL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderSocket := czmq.NewSock(czmq.Pull)
|
||||
|
||||
// Set socket options for 10Gbps throughput
|
||||
orderSocket.SetRcvhwm(server.hwm)
|
||||
orderSocket.SetRcvbuf(128 * 1024 * 1024) // 128MB receive buffer
|
||||
orderSocket.SetTcpKeepalive(1)
|
||||
orderSocket.SetTcpKeepaliveIdle(300)
|
||||
orderSocket.SetImmediate(false) // Allow batching
|
||||
|
||||
if server.tcpNoDelay {
|
||||
// TCP_NODELAY not directly available in pebbe/zmq4
|
||||
// Would need to use SetSockoptInt with proper constant
|
||||
}
|
||||
|
||||
// Bind to order port
|
||||
endpoint := fmt.Sprintf("tcp://*:%d", config.OrderPort)
|
||||
if err := orderSocket.Bind(endpoint); err != nil {
|
||||
if _, err := orderSocket.Bind(endpoint); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind order socket: %w", err)
|
||||
}
|
||||
log.Printf("Order socket listening on %s", endpoint)
|
||||
|
||||
// Trade publisher (PUB)
|
||||
tradeSocket, err := zmq.NewSocket(zmq.PUB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tradeSocket := czmq.NewSock(czmq.Pub)
|
||||
|
||||
tradeSocket.SetSndhwm(server.hwm)
|
||||
tradeSocket.SetSndbuf(128 * 1024 * 1024)
|
||||
|
||||
endpoint = fmt.Sprintf("tcp://*:%d", config.TradePort)
|
||||
if err := tradeSocket.Bind(endpoint); err != nil {
|
||||
if _, err := tradeSocket.Bind(endpoint); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind trade socket: %w", err)
|
||||
}
|
||||
log.Printf("Trade socket publishing on %s", endpoint)
|
||||
|
||||
// Market data publisher (PUB)
|
||||
marketSocket, err := zmq.NewSocket(zmq.PUB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
marketSocket := czmq.NewSock(czmq.Pub)
|
||||
|
||||
endpoint = fmt.Sprintf("tcp://*:%d", config.MarketPort)
|
||||
if err := marketSocket.Bind(endpoint); err != nil {
|
||||
if _, err := marketSocket.Bind(endpoint); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind market socket: %w", err)
|
||||
}
|
||||
log.Printf("Market data socket publishing on %s", endpoint)
|
||||
|
||||
// Command socket (REP)
|
||||
cmdSocket, err := zmq.NewSocket(zmq.REP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmdSocket := czmq.NewSock(czmq.Rep)
|
||||
|
||||
endpoint = fmt.Sprintf("tcp://*:%d", config.CmdPort)
|
||||
if err := cmdSocket.Bind(endpoint); err != nil {
|
||||
if _, err := cmdSocket.Bind(endpoint); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind command socket: %w", err)
|
||||
}
|
||||
log.Printf("Command socket listening on %s", endpoint)
|
||||
@@ -181,7 +148,7 @@ func NewMLXZMQServer(config *Config) (*MLXZMQServer, error) {
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) initOrderBooks() {
|
||||
func (s *AccelZMQServer) initOrderBooks() {
|
||||
// Pre-create order books for known symbols
|
||||
symbols := []uint32{
|
||||
1, // BTC-USD
|
||||
@@ -190,7 +157,6 @@ func (s *MLXZMQServer) initOrderBooks() {
|
||||
4, // AVAX-USD
|
||||
5, // MATIC-USD
|
||||
10, // BTC-ETH
|
||||
// Add more as needed
|
||||
}
|
||||
|
||||
for _, sym := range symbols {
|
||||
@@ -200,12 +166,11 @@ func (s *MLXZMQServer) initOrderBooks() {
|
||||
log.Printf("Initialized %d order books", len(s.orderBooks))
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) Run() error {
|
||||
log.Println("Starting MLX-ZMQ server...")
|
||||
func (s *AccelZMQServer) Run() error {
|
||||
log.Println("Starting Accel-ZMQ server...")
|
||||
|
||||
// Set CPU affinity for maximum performance
|
||||
if len(s.cpuAffinity) > 0 {
|
||||
// TODO: Set CPU affinity using syscalls
|
||||
log.Printf("CPU affinity: %v", s.cpuAffinity)
|
||||
}
|
||||
|
||||
@@ -216,7 +181,7 @@ func (s *MLXZMQServer) Run() error {
|
||||
wg.Add(1)
|
||||
go s.orderReceiver(&wg)
|
||||
|
||||
// Batch processor (MLX GPU processing)
|
||||
// Batch processor (GPU processing)
|
||||
wg.Add(1)
|
||||
go s.batchProcessor(&wg)
|
||||
|
||||
@@ -241,17 +206,13 @@ func (s *MLXZMQServer) Run() error {
|
||||
}
|
||||
|
||||
// Hot path - receives orders at maximum speed
|
||||
func (s *MLXZMQServer) orderReceiver(wg *sync.WaitGroup) {
|
||||
func (s *AccelZMQServer) orderReceiver(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
// Use zero-copy receive if available
|
||||
for {
|
||||
// Receive binary message
|
||||
msg, err := s.orderSocket.RecvBytes(0)
|
||||
msg, _, err := s.orderSocket.RecvFrame()
|
||||
if err != nil {
|
||||
if zmq.AsErrno(err) == zmq.ETERM {
|
||||
break
|
||||
}
|
||||
atomic.AddUint64(&s.stats.errors, 1)
|
||||
continue
|
||||
}
|
||||
@@ -288,23 +249,20 @@ func (s *MLXZMQServer) orderReceiver(wg *sync.WaitGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
// Process batches using MLX GPU acceleration
|
||||
func (s *MLXZMQServer) batchProcessor(wg *sync.WaitGroup) {
|
||||
// Process batches using Accel GPU acceleration
|
||||
func (s *AccelZMQServer) batchProcessor(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(s.batchTimeout)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// Process any pending orders on timeout
|
||||
s.processBatch()
|
||||
}
|
||||
for range ticker.C {
|
||||
// Process any pending orders on timeout
|
||||
s.processBatch()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) processBatch() {
|
||||
func (s *AccelZMQServer) processBatch() {
|
||||
s.batchMutex.Lock()
|
||||
if len(s.orderBatch) == 0 {
|
||||
s.batchMutex.Unlock()
|
||||
@@ -318,34 +276,40 @@ func (s *MLXZMQServer) processBatch() {
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
// Group orders by symbol for MLX processing
|
||||
// Group orders by symbol for processing
|
||||
symbolOrders := make(map[uint32][]OrderMessage)
|
||||
for _, order := range batch {
|
||||
symbolOrders[order.Symbol] = append(symbolOrders[order.Symbol], order)
|
||||
}
|
||||
|
||||
// Process each symbol's orders
|
||||
// Process each symbol's orders using accel/ops/dex
|
||||
for symbol, orders := range symbolOrders {
|
||||
// Separate buy and sell orders
|
||||
var bids, asks []mlx.Order
|
||||
var bids, asks []dex.Order
|
||||
|
||||
for _, o := range orders {
|
||||
mlxOrder := mlx.Order{
|
||||
ID: o.OrderID,
|
||||
Price: o.Price,
|
||||
Size: o.Size,
|
||||
Side: int(o.Side),
|
||||
accelOrder := dex.Order{
|
||||
ID: o.OrderID,
|
||||
UserID: o.UserID,
|
||||
Price: uint64(o.Price * 1e8), // Convert to fixed-point
|
||||
Quantity: uint64(o.Size * 1e8),
|
||||
Remaining: uint64(o.Size * 1e8),
|
||||
Side: dex.Side(o.Side),
|
||||
Type: dex.OrderType(o.Type),
|
||||
}
|
||||
|
||||
if o.Side == 0 { // Buy
|
||||
bids = append(bids, mlxOrder)
|
||||
bids = append(bids, accelOrder)
|
||||
} else { // Sell
|
||||
asks = append(asks, mlxOrder)
|
||||
asks = append(asks, accelOrder)
|
||||
}
|
||||
}
|
||||
|
||||
// Use MLX GPU engine for matching
|
||||
trades := s.mlxEngine.BatchMatch(bids, asks)
|
||||
// Use accel for GPU-accelerated matching
|
||||
trades, _, err := dex.MatchOrders(bids, asks, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Publish trades
|
||||
for _, trade := range trades {
|
||||
@@ -378,14 +342,20 @@ func (s *MLXZMQServer) processBatch() {
|
||||
atomic.StoreUint64(&s.stats.latencyNanos, uint64(latency))
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) publishTrade(symbol uint32, trade mlx.Trade) {
|
||||
func (s *AccelZMQServer) publishTrade(symbol uint32, trade dex.Trade) {
|
||||
// Determine buy/sell order IDs based on taker side
|
||||
buyID, sellID := trade.MakerID, trade.TakerID
|
||||
if trade.TakerSide == dex.Ask {
|
||||
buyID, sellID = trade.TakerID, trade.MakerID
|
||||
}
|
||||
|
||||
msg := TradeMessage{
|
||||
Magic: 0xBEEFDEAD,
|
||||
TradeID: trade.ID,
|
||||
BuyOrderID: trade.BuyOrderID,
|
||||
SellOrderID: trade.SellOrderID,
|
||||
Price: trade.Price,
|
||||
Size: trade.Size,
|
||||
BuyOrderID: buyID,
|
||||
SellOrderID: sellID,
|
||||
Price: float64(trade.Price) / 1e8,
|
||||
Size: float64(trade.Quantity) / 1e8,
|
||||
Timestamp: uint64(time.Now().UnixNano()),
|
||||
}
|
||||
|
||||
@@ -394,11 +364,11 @@ func (s *MLXZMQServer) publishTrade(symbol uint32, trade mlx.Trade) {
|
||||
|
||||
// Publish with topic
|
||||
topic := fmt.Sprintf("TRADE.%d", symbol)
|
||||
s.tradeSocket.Send(topic, zmq.SNDMORE)
|
||||
s.tradeSocket.SendBytes(buf[:], 0)
|
||||
s.tradeSocket.SendFrame([]byte(topic), czmq.FlagMore)
|
||||
s.tradeSocket.SendFrame(buf[:], 0)
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) publishMarketData(symbol uint32, ob *lx.OrderBook) {
|
||||
func (s *AccelZMQServer) publishMarketData(symbol uint32, ob *lx.OrderBook) {
|
||||
// Get best bid/ask
|
||||
bestBid := ob.GetBestBid()
|
||||
bestAsk := ob.GetBestAsk()
|
||||
@@ -423,11 +393,11 @@ func (s *MLXZMQServer) publishMarketData(symbol uint32, ob *lx.OrderBook) {
|
||||
buf := (*[48]byte)(unsafe.Pointer(&md))
|
||||
|
||||
topic := fmt.Sprintf("MD.%d", symbol)
|
||||
s.marketSocket.Send(topic, zmq.SNDMORE)
|
||||
s.marketSocket.SendBytes(buf[:], 0)
|
||||
s.marketSocket.SendFrame([]byte(topic), czmq.FlagMore)
|
||||
s.marketSocket.SendFrame(buf[:], 0)
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) statsReporter(wg *sync.WaitGroup) {
|
||||
func (s *AccelZMQServer) statsReporter(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
@@ -450,12 +420,12 @@ func (s *MLXZMQServer) statsReporter(wg *sync.WaitGroup) {
|
||||
bytesPerSec := float64(bytes-lastBytes) / elapsed
|
||||
gbps := (bytesPerSec * 8) / 1e9
|
||||
|
||||
log.Printf("Stats: %.0f orders/sec | %.2f Gbps | %d trades | %.0f μs latency | %d errors",
|
||||
log.Printf("Stats: %.0f orders/sec | %.2f Gbps | %d trades | %.0f us latency | %d errors",
|
||||
ordersPerSec, gbps, trades, float64(latency)/1000, errors)
|
||||
|
||||
// Check if we're saturating 10Gbps
|
||||
if gbps > 9.0 {
|
||||
log.Printf("🚀 SATURATING 10Gbps FIBER! %.2f Gbps achieved!", gbps)
|
||||
log.Printf("[PERF] SATURATING 10Gbps FIBER! %.2f Gbps achieved!", gbps)
|
||||
}
|
||||
|
||||
lastOrders = orders
|
||||
@@ -464,41 +434,38 @@ func (s *MLXZMQServer) statsReporter(wg *sync.WaitGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) commandHandler(wg *sync.WaitGroup) {
|
||||
func (s *AccelZMQServer) commandHandler(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
msg, err := s.cmdSocket.Recv(0)
|
||||
msg, _, err := s.cmdSocket.RecvFrame()
|
||||
if err != nil {
|
||||
if zmq.AsErrno(err) == zmq.ETERM {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle commands
|
||||
switch msg {
|
||||
switch string(msg) {
|
||||
case "STATS":
|
||||
stats := fmt.Sprintf("orders:%d,trades:%d,errors:%d",
|
||||
atomic.LoadUint64(&s.stats.ordersReceived),
|
||||
atomic.LoadUint64(&s.stats.tradesExecuted),
|
||||
atomic.LoadUint64(&s.stats.errors))
|
||||
s.cmdSocket.Send(stats, 0)
|
||||
s.cmdSocket.SendFrame([]byte(stats), 0)
|
||||
|
||||
case "PING":
|
||||
s.cmdSocket.Send("PONG", 0)
|
||||
s.cmdSocket.SendFrame([]byte("PONG"), 0)
|
||||
|
||||
default:
|
||||
s.cmdSocket.Send("ERROR: Unknown command", 0)
|
||||
s.cmdSocket.SendFrame([]byte("ERROR: Unknown command"), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MLXZMQServer) shutdown() {
|
||||
s.orderSocket.Close()
|
||||
s.tradeSocket.Close()
|
||||
s.marketSocket.Close()
|
||||
s.cmdSocket.Close()
|
||||
func (s *AccelZMQServer) shutdown() {
|
||||
s.orderSocket.Destroy()
|
||||
s.tradeSocket.Destroy()
|
||||
s.marketSocket.Destroy()
|
||||
s.cmdSocket.Destroy()
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -520,7 +487,7 @@ func main() {
|
||||
flag.IntVar(&config.TradePort, "trade-port", 5556, "Trade publisher port")
|
||||
flag.IntVar(&config.MarketPort, "market-port", 5557, "Market data port")
|
||||
flag.IntVar(&config.CmdPort, "cmd-port", 5558, "Command port")
|
||||
flag.IntVar(&config.BatchSize, "batch", 1000, "Batch size for MLX processing")
|
||||
flag.IntVar(&config.BatchSize, "batch", 1000, "Batch size for processing")
|
||||
flag.DurationVar(&config.BatchTimeout, "batch-timeout", 10*time.Millisecond, "Batch timeout")
|
||||
flag.IntVar(&config.HWM, "hwm", 100000, "High water mark")
|
||||
flag.BoolVar(&config.ZeroCopy, "zero-copy", true, "Use zero-copy")
|
||||
@@ -530,11 +497,11 @@ func main() {
|
||||
// Set runtime optimizations
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
|
||||
log.Println("═══════════════════════════════════════════════")
|
||||
log.Println(" LX MLX-ZMQ Server - 10Gbps Fiber Edition")
|
||||
log.Println("═══════════════════════════════════════════════")
|
||||
log.Println("===============================================")
|
||||
log.Println(" LX Accel-ZMQ Server - 10Gbps Fiber Edition")
|
||||
log.Println("===============================================")
|
||||
|
||||
server, err := NewMLXZMQServer(config)
|
||||
server, err := NewAccelZMQServer(config)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -7,7 +9,7 @@ import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
"github.com/luxfi/czmq/v4"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -16,13 +18,10 @@ func main() {
|
||||
flag.Parse()
|
||||
|
||||
// Connect to X-Chain order processor
|
||||
socket, err := zmq.NewSocket(zmq.PUSH)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create socket: %v", err)
|
||||
}
|
||||
defer socket.Close()
|
||||
socket := czmq.NewSock(czmq.Push)
|
||||
defer socket.Destroy()
|
||||
|
||||
err = socket.Connect(*endpoint)
|
||||
err := socket.Connect(*endpoint)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
@@ -73,7 +72,7 @@ func main() {
|
||||
// Remaining bytes are padding
|
||||
|
||||
// Send the order
|
||||
_, err := socket.SendBytes(order, 0)
|
||||
err := socket.SendFrame(order, 0)
|
||||
if err != nil {
|
||||
log.Printf("Failed to send order %d: %v", i, err)
|
||||
continue
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build zmqtest
|
||||
|
||||
// LX Trader - Sends orders to exchange via ZMQ
|
||||
package main
|
||||
|
||||
@@ -11,7 +13,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
zmq "github.com/pebbe/zmq4"
|
||||
"github.com/luxfi/czmq/v4"
|
||||
)
|
||||
|
||||
type Order struct {
|
||||
@@ -33,13 +35,11 @@ type TraderStats struct {
|
||||
func runTrader(id int, serverAddr string, ordersPerSec int, stats *TraderStats, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
context, _ := zmq.NewContext()
|
||||
socket, _ := context.NewSocket(zmq.PUSH)
|
||||
defer socket.Close()
|
||||
socket := czmq.NewSock(czmq.Push)
|
||||
defer socket.Destroy()
|
||||
|
||||
// Set high water mark
|
||||
socket.SetSndhwm(100000)
|
||||
socket.SetSndbuf(8 * 1024 * 1024) // 8MB buffer
|
||||
|
||||
if err := socket.Connect(serverAddr); err != nil {
|
||||
log.Printf("Trader %d failed to connect: %v", id, err)
|
||||
@@ -70,7 +70,7 @@ func runTrader(id int, serverAddr string, ordersPerSec int, stats *TraderStats,
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := socket.SendBytes(data, zmq.DONTWAIT); err != nil {
|
||||
if err := socket.SendFrame(data, 0); err != nil {
|
||||
atomic.AddUint64(&stats.Errors, 1)
|
||||
} else {
|
||||
atomic.AddUint64(&stats.OrdersSent, 1)
|
||||
@@ -91,7 +91,7 @@ func main() {
|
||||
|
||||
totalRate := *traders * *ordersPerSec
|
||||
|
||||
fmt.Printf("🚀 ZMQ Trader Client\n")
|
||||
fmt.Printf("[START] ZMQ Trader Client\n")
|
||||
fmt.Printf("Server: %s\n", *serverAddr)
|
||||
fmt.Printf("Traders: %d\n", *traders)
|
||||
fmt.Printf("Rate per trader: %d orders/sec\n", *ordersPerSec)
|
||||
@@ -128,7 +128,7 @@ func main() {
|
||||
gbpsUsed := (float64(bytes) * 8) / (elapsed * 1e9)
|
||||
efficiency := (ordersPerSec / float64(totalRate)) * 100
|
||||
|
||||
fmt.Printf("\r📈 Sent: %d | Rate: %.0f/sec (%.1f%% efficiency) | Network: %.2f MB/s (%.3f Gbps) | Errors: %d",
|
||||
fmt.Printf("\r[STATS] Sent: %d | Rate: %.0f/sec (%.1f%% efficiency) | Network: %.2f MB/s (%.3f Gbps) | Errors: %d",
|
||||
sent, ordersPerSec, efficiency, mbPerSec, gbpsUsed, errors)
|
||||
|
||||
if *verbose && sent%10000 == 0 {
|
||||
|
||||
@@ -5,48 +5,72 @@ go 1.25.5
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/luxfi/accel v1.0.1
|
||||
github.com/luxfi/czmq/v4 v4.2.2
|
||||
github.com/luxfi/database v1.2.15
|
||||
github.com/luxfi/log v1.1.26
|
||||
github.com/luxfi/mlx v0.2.0
|
||||
github.com/nats-io/nats.go v1.44.0
|
||||
github.com/pebbe/zmq4 v1.4.0
|
||||
github.com/luxfi/geth v1.16.64
|
||||
github.com/luxfi/log v1.2.5
|
||||
github.com/luxfi/rpc v1.0.0
|
||||
github.com/nats-io/nats.go v1.48.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
google.golang.org/grpc v1.75.1
|
||||
google.golang.org/grpc v1.78.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/consensys/gnark-crypto v0.19.2 // indirect
|
||||
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dgraph-io/badger/v4 v4.9.0 // indirect
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
|
||||
github.com/emicklei/dot v1.10.0 // indirect
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
|
||||
github.com/ethereum/go-verkle v0.2.2 // indirect
|
||||
github.com/getsentry/sentry-go v0.40.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/google/flatbuffers v25.12.19+incompatible // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/hashicorp/go-bexpr v0.1.15 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/luxfi/cache v1.0.0 // indirect
|
||||
github.com/luxfi/crypto v1.17.26 // indirect
|
||||
github.com/luxfi/geth v1.16.64 // indirect
|
||||
github.com/luxfi/ids v1.2.5 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.11 // indirect
|
||||
github.com/nats-io/nkeys v0.4.14 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/supranational/blst v0.3.16 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/net v0.49.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,59 +1,88 @@
|
||||
github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=
|
||||
github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f h1:a5PUgHGinaD6XrLmIDLQmGHocjIjBsBAcR5gALjZvMU=
|
||||
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251230134950-44c893854e3f/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
|
||||
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
|
||||
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
|
||||
github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE=
|
||||
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cockroachdb/errors v1.12.0 h1:d7oCs6vuIMUQRVbi6jWWWEJZahLCfJpnJSVobd1/sUo=
|
||||
github.com/cockroachdb/errors v1.12.0/go.mod h1:SvzfYNNBshAVbZ8wzNc/UPK3w1vf0dKDUP41ucAIf7g=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0 h1:pU88SPhIFid6/k0egdR5V6eALQYq2qbSmukrkgIh/0A=
|
||||
github.com/cockroachdb/fifo v0.0.0-20240816210425-c5d0cb0b6fc0/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M=
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506 h1:ASDL+UJcILMqgNeV5jiqR4j+sTuvQNHdf2chuKj1M5k=
|
||||
github.com/cockroachdb/logtags v0.0.0-20241215232642-bb51bb14a506/go.mod h1:Mw7HqKr2kdtu6aYGn3tPmAftiP3QPX63LdK/zcariIo=
|
||||
github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw=
|
||||
github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo=
|
||||
github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30=
|
||||
github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/cockroachdb/redact v1.1.6 h1:zXJBwDZ84xJNlHl1rMyCojqyIxv+7YUpQiJLQ7n4314=
|
||||
github.com/cockroachdb/redact v1.1.6/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
|
||||
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
|
||||
github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80=
|
||||
github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
|
||||
github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
|
||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
|
||||
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
|
||||
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
|
||||
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
|
||||
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
|
||||
github.com/dgraph-io/badger/v4 v4.9.0 h1:tpqWb0NewSrCYqTvywbcXOhQdWcqephkVkbBmaaqHzc=
|
||||
github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
|
||||
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/dot v1.10.0 h1:z17n0ce/FBMz3QbShSzVGhiW447Qhu7fljzvp3Gs6ig=
|
||||
github.com/emicklei/dot v1.10.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
|
||||
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
|
||||
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk=
|
||||
github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8=
|
||||
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
|
||||
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
|
||||
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
|
||||
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
|
||||
github.com/ferranbt/fastssz v1.0.0 h1:9EXXYsracSqQRBQiHeaVsG/KQeYblPf40hsQPb9Dzk8=
|
||||
github.com/ferranbt/fastssz v1.0.0/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
|
||||
github.com/getsentry/sentry-go v0.40.0 h1:VTJMN9zbTvqDqPwheRVLcp0qcUcM+8eFivvGocAaSbo=
|
||||
github.com/getsentry/sentry-go v0.40.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
|
||||
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
|
||||
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -62,112 +91,234 @@ github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
|
||||
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/hashicorp/go-bexpr v0.1.15 h1:flTYJAqZAlK+t8ezezb6WQGlRO1D4+GEF/HmH+xZo5k=
|
||||
github.com/hashicorp/go-bexpr v0.1.15/go.mod h1:HGKbAByHn2aJWUV47gL7+IjLK79iU3EZIbOwCXJZLoE=
|
||||
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330=
|
||||
github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg=
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
|
||||
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
|
||||
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
|
||||
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
|
||||
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
|
||||
github.com/luxfi/accel v1.0.1 h1:JtkOS5tSjdrVRGDHiuXxR6AIBaiSN1NpQc4FobgSl5w=
|
||||
github.com/luxfi/accel v1.0.1/go.mod h1:j5NCg8eKiQwhbRuJqa5G1oozcKp8HxJWMnrNA/CocWk=
|
||||
github.com/luxfi/cache v1.0.0 h1:OcqGi/qs9ky+7O8fR6WMZtFsunvA40LBTg3QRQqJurs=
|
||||
github.com/luxfi/cache v1.0.0/go.mod h1:BEvpffvV3SXs/KSa9nUC1TszxKwUdIHwIaoXpeiYU+Y=
|
||||
github.com/luxfi/crypto v1.3.2 h1:3qKhhjzFYdT6FzbEuOp+6/vK2qdZ4EVAEd3vW8BRQks=
|
||||
github.com/luxfi/crypto v1.3.2/go.mod h1:dhshhrUIuwlrfgYBgYQS6h55EFD3yZex6vlPT1sXKZ4=
|
||||
github.com/luxfi/consensus v1.22.44 h1:GRzJUUG3ZEYcn3/Ve5GAJKNcOKgvFAIR4HErvsLbmLs=
|
||||
github.com/luxfi/consensus v1.22.44/go.mod h1:mNUPWj7hy3ZI0vmhPQvTKaeRci3SpEqKhqosOUYU4b8=
|
||||
github.com/luxfi/crypto v1.17.26 h1:B4hebyl2whu9Og+mb7QoQavD4LM1cUpwWQEnwi3H6fU=
|
||||
github.com/luxfi/crypto v1.17.26/go.mod h1:v6eaW5ejuzW2gecChWKQqx4CtVhoM1b+e40ro/6WVPg=
|
||||
github.com/luxfi/database v1.1.13 h1:J0iNaejPDFfvevvX/TDr61pcfEciPRSKK1czoDbg+n0=
|
||||
github.com/luxfi/database v1.1.13/go.mod h1:EgiZIOrM9gCqGBy6d9IrnIESOW+/y4MdXRfFUan6BTY=
|
||||
github.com/luxfi/czmq/v4 v4.2.2 h1:D4QDl99OfGoi8Gk3RI8GZ6pqt+040rolj+h36scSj2M=
|
||||
github.com/luxfi/czmq/v4 v4.2.2/go.mod h1:WFY9ldUbeU900P8bcIRr7Jim23veOhJL5A5zSpTogdU=
|
||||
github.com/luxfi/database v1.2.15 h1:tQsOAucLyWL3d+/orSpCVF8WVrqz/IsTDPlXCBhFmjI=
|
||||
github.com/luxfi/database v1.2.15/go.mod h1:lSmpKmL4e9ajUir89Y7Be1IOzK56fPr8D3ZDXh+y5T8=
|
||||
github.com/luxfi/geth v1.16.34 h1:61IMMZMWfi4bM4SASVBvp3x+wYmpcTpIIHDiKVVPDiQ=
|
||||
github.com/luxfi/geth v1.16.34/go.mod h1:kjF/Vwzd9Iz6+9PpHyF7ZtfVJYsJdBYGhnoXLDxx7b8=
|
||||
github.com/luxfi/geth v1.16.64 h1:kfuVA6FkgHOsOKiz75e+h6UG4eHLOInqXmJEhx9NKTs=
|
||||
github.com/luxfi/geth v1.16.64/go.mod h1:7lQ25do4pQizMtrVKdepji2Pl/pL+8DUo5Q0SbSuWpE=
|
||||
github.com/luxfi/ids v1.0.2 h1:OnbCWBgdmuBPZat1r1vRikk1FFrdHMfSknpfKmX0KBg=
|
||||
github.com/luxfi/ids v1.0.2/go.mod h1:cMbto3Q8N3IaBxdz4Lzaq9wYl33coGXUdlr2+YwXeUw=
|
||||
github.com/luxfi/ids v1.2.5 h1:w4YSj3S6LQiUXhiChUmcbrMCRMfkCaBFIGn0Eo4ZSxk=
|
||||
github.com/luxfi/ids v1.2.5/go.mod h1:vZBjJWSmqMIZuodgnoszvTDvXIwsbnhM2pkktg6FGrg=
|
||||
github.com/luxfi/log v1.1.22 h1:E+jX+ZZS4JX2HjuGdDTpLfrgFKEDD8byNYTa0m6HT1o=
|
||||
github.com/luxfi/log v1.1.22/go.mod h1:Q2eeOT4alCF+/B6+k/eWtVZQ2HncDlpd9vUvkTF0Suc=
|
||||
github.com/luxfi/log v1.1.26/go.mod h1:ACN1FtMlJ/NRuWROYH2TtiFmJuUbSG+OxINlRzJM/38=
|
||||
github.com/luxfi/metric v1.3.0 h1:l5T6sTRHR2/FBLdxZJpEPV+WxtQSqL2BfDX5OEj/pFE=
|
||||
github.com/luxfi/metric v1.3.0/go.mod h1:4emefSEkjDieChAET/gpYWk9wZcOSFrxTvcMQ9dNAi0=
|
||||
github.com/luxfi/mlx v0.2.0 h1:VCyD0DMffHX4DMp1HPHf8jXYixwCLuxkILP6LUImM6Y=
|
||||
github.com/luxfi/mlx v0.2.0/go.mod h1:kEVRkGQ9iQkg+fJtEE3uZr4ANnoI2xvdhsJKk+DAEsI=
|
||||
github.com/luxfi/node v1.13.4-lux h1:wg2OfXtf7/vuk2LJDFQejYCFJepjY/hvzT7D0t3R9ro=
|
||||
github.com/luxfi/node v1.13.4-lux/go.mod h1:dCyMoguHUu3cR1Iv8rtXmWcAvBSU3v10trGzJZt8P74=
|
||||
github.com/luxfi/log v1.2.5 h1:i4yQhf3De8Db11SmNkrko34SX935JfEcBubDYAEFPJI=
|
||||
github.com/luxfi/log v1.2.5/go.mod h1:n3KqtaehrICqZLUuze+WKLgDEYUom7AaHQXuw98+8uM=
|
||||
github.com/luxfi/math v1.0.5 h1:Dbnaq2mnvIKFd79bb2jPop4to2bQMOc3wep+zIocECc=
|
||||
github.com/luxfi/math v1.0.5/go.mod h1:xe+irMomv37VxvizdwF1LLkLa9/7qiQOJ5aJCNOESc0=
|
||||
github.com/luxfi/metric v1.4.8 h1:mMbcJW+E1z6qQxn34+BE109HlvT6ciiDXuc10qEBJRs=
|
||||
github.com/luxfi/metric v1.4.8/go.mod h1:AUQ7NSNz9WndAcr/SKnOkP7XSFFnBXOa+ihtJYfDaQY=
|
||||
github.com/luxfi/p2p v1.18.1 h1:AZTaf8zBjFNjue+WkvNKIu3+ZXZ4TNdHD8WqvZwF+ig=
|
||||
github.com/luxfi/p2p v1.18.1/go.mod h1:knTRKfWiyma8MjOD9kGunqQGz9+zpd4GhzTyjNF6JtA=
|
||||
github.com/luxfi/precompiles v0.1.2 h1:3BouGKBI6ZrJPrrx9rM0PvoYXAjJtqJLC8DTjt8gIr8=
|
||||
github.com/luxfi/precompiles v0.1.2/go.mod h1:X/lQTeT3AyNyIfOfRVK88YWcrKh3B75bwVjZCo5t9wA=
|
||||
github.com/luxfi/rpc v1.0.0 h1:G8o89gT+CT3SOj08A5FS5G92fovKrWxs941FmUz28CA=
|
||||
github.com/luxfi/rpc v1.0.0/go.mod h1:dhxzM5rFqNpEOOHB7+8YBQ9d/V4haa6/id9AQTIIJvQ=
|
||||
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
|
||||
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
|
||||
github.com/luxfi/utils v1.0.3 h1:k7ahbu4phMiPMjKoM5YmBTwpXavPpHlCWTWn2chac/s=
|
||||
github.com/luxfi/utils v1.0.3/go.mod h1:1C5pLQhLiRj2OxZV261U8/rj/KRlEJeOMLCH/ecT2c4=
|
||||
github.com/luxfi/vm v1.0.1 h1:eZqrp6mEFPkhiv5kf5ZXmYAbXXjpS9axpUQG3cd3yts=
|
||||
github.com/luxfi/vm v1.0.1/go.mod h1:PJinuF4McLaHN57iKf8p7h89wIQhQT//LD/fac+/Dk8=
|
||||
github.com/luxfi/warp v1.18.0 h1:4FRpB+mnrQeOhPvcKUWOdQbNODV1bY67ll4FKi1mYKc=
|
||||
github.com/luxfi/warp v1.18.0/go.mod h1:8kjDhuE5UKdd0IUGvoD2rnNphLuATCBOddyriz91pTM=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQhoHZAz4x7TIw=
|
||||
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
|
||||
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
|
||||
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.14 h1:ofx8UiyHP5S4Q52/THHucCJsMWu6zhf4DLh0U2593HE=
|
||||
github.com/nats-io/nkeys v0.4.14/go.mod h1:seG5UKwYdZXb7M1y1vvu53mNh3xq2B6um/XUgYAgvkM=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/pebbe/zmq4 v1.4.0 h1:gO5P92Ayl8GXpPZdYcD62Cwbq0slSBVVQRIXwGSJ6eQ=
|
||||
github.com/pebbe/zmq4 v1.4.0/go.mod h1:nqnPueOapVhE2wItZ0uOErngczsJdLOGkebMxaO8r48=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
|
||||
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
|
||||
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI=
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
github.com/tilinna/z85 v1.0.0 h1:uqFnJBlD01dosSeo5sK1G1YGbPuwqVHqR+12OJDRjUw=
|
||||
github.com/tilinna/z85 v1.0.0/go.mod h1:EfpFU/DUY4ddEy6CRvk2l+UQNEzHbh+bqBQS+04Nkxs=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
|
||||
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -175,5 +326,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -3039,7 +3039,7 @@ func TestHandleVaultDepositInvalidDepositData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for handleVaultWithdraw additional coverage
|
||||
// Tests for handleVaultWithdraw additional coverage
|
||||
func TestHandleVaultWithdrawNotAuthenticated(t *testing.T) {
|
||||
server := newTestWebSocketServer()
|
||||
client := &Client{
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/luxfi/dex/pkg/log"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/rpc"
|
||||
)
|
||||
|
||||
// ZAP wire message types for DEX (zero-copy binary protocol)
|
||||
const (
|
||||
MsgPlaceOrder uint8 = iota + 1
|
||||
MsgCancelOrder
|
||||
MsgModifyOrder
|
||||
MsgGetBestBid
|
||||
MsgGetBestAsk
|
||||
MsgGetOrderBook
|
||||
MsgGetOrder
|
||||
MsgOrderAck
|
||||
MsgOrderReject
|
||||
MsgTrade
|
||||
MsgMarketData
|
||||
)
|
||||
|
||||
// Wire sizes for fixed-format messages (cache-aligned)
|
||||
const (
|
||||
OrderWireSize = 64 // Order: symbol(8) + id(8) + price(8) + size(8) + side(1) + type(1) + flags(2) + ts(8) + user(16) + pad(4)
|
||||
CancelWireSize = 32 // Cancel: order_id(8) + user(16) + pad(8)
|
||||
AckWireSize = 24 // Ack: order_id(8) + status(1) + seq(8) + pad(7)
|
||||
QuoteWireSize = 24 // Quote: price(8) + size(8) + count(4) + pad(4)
|
||||
TradeWireSize = 48 // Trade: id(8) + price(8) + size(8) + buyer(8) + seller(8) + ts(8)
|
||||
)
|
||||
|
||||
// ZAPServer provides ultra-low-latency order handling for HFT
|
||||
type ZAPServer struct {
|
||||
orderBook *lx.OrderBook
|
||||
server rpc.Server
|
||||
logger log.Logger
|
||||
addr string
|
||||
|
||||
// Statistics
|
||||
ordersProcessed atomic.Uint64
|
||||
tradesExecuted atomic.Uint64
|
||||
cancelProcessed atomic.Uint64
|
||||
|
||||
// Pre-allocated response buffers (per-connection pools)
|
||||
bufferPool sync.Pool
|
||||
|
||||
// Sequence counter for order acks
|
||||
sequence atomic.Uint64
|
||||
}
|
||||
|
||||
// NewZAPServer creates a new ZAP server for ultra-low-latency trading
|
||||
func NewZAPServer(orderBook *lx.OrderBook, addr string, logger log.Logger) *ZAPServer {
|
||||
return &ZAPServer{
|
||||
orderBook: orderBook,
|
||||
addr: addr,
|
||||
logger: logger,
|
||||
bufferPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Pre-allocate 4KB buffer for responses
|
||||
return make([]byte, 4096)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the ZAP server
|
||||
func (s *ZAPServer) Start(ctx context.Context) error {
|
||||
server, err := rpc.Listen(s.addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start ZAP server: %w", err)
|
||||
}
|
||||
s.server = server
|
||||
|
||||
// Register raw handlers for zero-copy processing
|
||||
s.server.RegisterRaw("place_order", s.handlePlaceOrder)
|
||||
s.server.RegisterRaw("cancel_order", s.handleCancelOrder)
|
||||
s.server.RegisterRaw("modify_order", s.handleModifyOrder)
|
||||
s.server.RegisterRaw("best_bid", s.handleGetBestBid)
|
||||
s.server.RegisterRaw("best_ask", s.handleGetBestAsk)
|
||||
s.server.RegisterRaw("orderbook", s.handleGetOrderBook)
|
||||
s.server.RegisterRaw("order", s.handleGetOrder)
|
||||
|
||||
s.logger.Info("ZAP server started", "addr", s.server.Addr())
|
||||
|
||||
go s.server.Serve(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the ZAP server
|
||||
func (s *ZAPServer) Stop() error {
|
||||
if s.server != nil {
|
||||
return s.server.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Addr returns the server address
|
||||
func (s *ZAPServer) Addr() string {
|
||||
if s.server != nil {
|
||||
return s.server.Addr()
|
||||
}
|
||||
return s.addr
|
||||
}
|
||||
|
||||
// Stats returns server statistics
|
||||
func (s *ZAPServer) Stats() (orders, trades, cancels uint64) {
|
||||
return s.ordersProcessed.Load(), s.tradesExecuted.Load(), s.cancelProcessed.Load()
|
||||
}
|
||||
|
||||
// handlePlaceOrder handles order placement with zero-copy
|
||||
// Wire format (64 bytes):
|
||||
//
|
||||
// [0:8] symbol (8 bytes, null-padded)
|
||||
// [8:16] order_id (uint64, network byte order)
|
||||
// [16:24] price (float64, IEEE 754)
|
||||
// [24:32] size (float64, IEEE 754)
|
||||
// [32] side (uint8: 0=buy, 1=sell)
|
||||
// [33] order_type (uint8: 0=limit, 1=market, 2=stop)
|
||||
// [34:36] flags (uint16: post-only, reduce-only, STP)
|
||||
// [36:44] timestamp (uint64, unix nanos)
|
||||
// [44:60] user_id (16 bytes, null-padded)
|
||||
// [60:64] padding
|
||||
func (s *ZAPServer) handlePlaceOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < OrderWireSize {
|
||||
return s.encodeReject(0, "invalid message size"), nil
|
||||
}
|
||||
|
||||
// Zero-copy decode order from wire format
|
||||
order := s.decodeOrder(payload)
|
||||
if order == nil {
|
||||
return s.encodeReject(0, "invalid order data"), nil
|
||||
}
|
||||
|
||||
// Validate order
|
||||
if order.Price <= 0 || order.Size <= 0 {
|
||||
return s.encodeReject(order.ID, "invalid price or size"), nil
|
||||
}
|
||||
|
||||
// Process order through matching engine
|
||||
// AddOrder returns the order ID (0 if rejected)
|
||||
orderID := s.orderBook.AddOrder(order)
|
||||
if orderID == 0 {
|
||||
return s.encodeReject(order.ID, "order rejected"), nil
|
||||
}
|
||||
|
||||
s.ordersProcessed.Add(1)
|
||||
|
||||
// Return ack with sequence number
|
||||
return s.encodeAck(orderID, 0, s.sequence.Add(1)), nil
|
||||
}
|
||||
|
||||
// handleCancelOrder handles order cancellation
|
||||
// Wire format (32 bytes):
|
||||
//
|
||||
// [0:8] order_id (uint64)
|
||||
// [8:24] user_id (16 bytes)
|
||||
// [24:32] padding
|
||||
func (s *ZAPServer) handleCancelOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < CancelWireSize {
|
||||
return s.encodeReject(0, "invalid message size"), nil
|
||||
}
|
||||
|
||||
orderID := binary.BigEndian.Uint64(payload[0:8])
|
||||
|
||||
// Cancel the order
|
||||
err := s.orderBook.CancelOrder(orderID)
|
||||
if err != nil {
|
||||
return s.encodeReject(orderID, err.Error()), nil
|
||||
}
|
||||
|
||||
s.cancelProcessed.Add(1)
|
||||
return s.encodeAck(orderID, 1, s.sequence.Add(1)), nil
|
||||
}
|
||||
|
||||
// handleModifyOrder handles order modification
|
||||
func (s *ZAPServer) handleModifyOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < OrderWireSize {
|
||||
return s.encodeReject(0, "invalid message size"), nil
|
||||
}
|
||||
|
||||
order := s.decodeOrder(payload)
|
||||
if order == nil {
|
||||
return s.encodeReject(0, "invalid order data"), nil
|
||||
}
|
||||
|
||||
// Modify is cancel + add
|
||||
_ = s.orderBook.CancelOrder(order.ID)
|
||||
orderID := s.orderBook.AddOrder(order)
|
||||
if orderID == 0 {
|
||||
return s.encodeReject(order.ID, "order rejected"), nil
|
||||
}
|
||||
|
||||
return s.encodeAck(orderID, 0, s.sequence.Add(1)), nil
|
||||
}
|
||||
|
||||
// handleGetBestBid returns the best bid price/size
|
||||
func (s *ZAPServer) handleGetBestBid(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
price := s.orderBook.GetBestBid()
|
||||
if price == 0 {
|
||||
return s.encodeQuote(0, 0, 0), nil
|
||||
}
|
||||
// Get depth for size/count info
|
||||
depth := s.orderBook.GetDepth(1)
|
||||
if depth != nil && len(depth.Bids) > 0 {
|
||||
return s.encodeQuote(depth.Bids[0].Price, depth.Bids[0].Size, depth.Bids[0].Count), nil
|
||||
}
|
||||
return s.encodeQuote(price, 0, 0), nil
|
||||
}
|
||||
|
||||
// handleGetBestAsk returns the best ask price/size
|
||||
func (s *ZAPServer) handleGetBestAsk(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
price := s.orderBook.GetBestAsk()
|
||||
if price == 0 {
|
||||
return s.encodeQuote(0, 0, 0), nil
|
||||
}
|
||||
// Get depth for size/count info
|
||||
depth := s.orderBook.GetDepth(1)
|
||||
if depth != nil && len(depth.Asks) > 0 {
|
||||
return s.encodeQuote(depth.Asks[0].Price, depth.Asks[0].Size, depth.Asks[0].Count), nil
|
||||
}
|
||||
return s.encodeQuote(price, 0, 0), nil
|
||||
}
|
||||
|
||||
// handleGetOrderBook returns top N levels
|
||||
func (s *ZAPServer) handleGetOrderBook(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
levels := 10
|
||||
if len(payload) >= 4 {
|
||||
levels = int(binary.BigEndian.Uint32(payload[0:4]))
|
||||
if levels > 100 {
|
||||
levels = 100
|
||||
}
|
||||
}
|
||||
|
||||
// Get orderbook depth
|
||||
depth := s.orderBook.GetDepth(levels)
|
||||
if depth == nil {
|
||||
return s.encodeQuote(0, 0, 0), nil
|
||||
}
|
||||
|
||||
// Encode response: [4 bytes: bid count][4 bytes: ask count][bids...][asks...]
|
||||
bidCount := len(depth.Bids)
|
||||
askCount := len(depth.Asks)
|
||||
size := 8 + bidCount*QuoteWireSize + askCount*QuoteWireSize
|
||||
resp := s.getBuffer()[:size]
|
||||
|
||||
binary.BigEndian.PutUint32(resp[0:4], uint32(bidCount))
|
||||
binary.BigEndian.PutUint32(resp[4:8], uint32(askCount))
|
||||
|
||||
offset := 8
|
||||
|
||||
// Encode bids
|
||||
for _, level := range depth.Bids {
|
||||
s.encodeQuoteAt(resp[offset:], level.Price, level.Size, level.Count)
|
||||
offset += QuoteWireSize
|
||||
}
|
||||
|
||||
// Encode asks
|
||||
for _, level := range depth.Asks {
|
||||
s.encodeQuoteAt(resp[offset:], level.Price, level.Size, level.Count)
|
||||
offset += QuoteWireSize
|
||||
}
|
||||
|
||||
return resp[:offset], nil
|
||||
}
|
||||
|
||||
// handleGetOrder returns a specific order
|
||||
func (s *ZAPServer) handleGetOrder(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
if len(payload) < 8 {
|
||||
return nil, fmt.Errorf("invalid order id")
|
||||
}
|
||||
|
||||
orderID := binary.BigEndian.Uint64(payload[0:8])
|
||||
order := s.orderBook.GetOrder(orderID)
|
||||
if order == nil {
|
||||
return s.encodeReject(orderID, "order not found"), nil
|
||||
}
|
||||
|
||||
return s.encodeOrderResponse(order), nil
|
||||
}
|
||||
|
||||
// decodeOrder decodes order from wire format (zero-copy)
|
||||
func (s *ZAPServer) decodeOrder(data []byte) *lx.Order {
|
||||
if len(data) < OrderWireSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract symbol (8 bytes, trim null padding)
|
||||
symbol := trimNull(data[0:8])
|
||||
|
||||
order := &lx.Order{
|
||||
ID: binary.BigEndian.Uint64(data[8:16]),
|
||||
Price: decodeFloat64(data[16:24]),
|
||||
Size: decodeFloat64(data[24:32]),
|
||||
Side: lx.Side(data[32]),
|
||||
Type: lx.OrderType(data[33]),
|
||||
Flags: lx.OrderFlags(binary.BigEndian.Uint16(data[34:36])),
|
||||
Timestamp: time.Unix(0, int64(binary.BigEndian.Uint64(data[36:44]))),
|
||||
UserID: string(trimNull(data[44:60])),
|
||||
Symbol: string(symbol),
|
||||
}
|
||||
|
||||
// Validate basic fields
|
||||
if order.Price < 0 || order.Size < 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
// encodeAck encodes an order acknowledgment
|
||||
func (s *ZAPServer) encodeAck(orderID uint64, status uint8, seq uint64) []byte {
|
||||
resp := s.getBuffer()[:AckWireSize]
|
||||
binary.BigEndian.PutUint64(resp[0:8], orderID)
|
||||
resp[8] = status
|
||||
binary.BigEndian.PutUint64(resp[9:17], seq)
|
||||
return resp[:AckWireSize]
|
||||
}
|
||||
|
||||
// encodeReject encodes an order rejection
|
||||
func (s *ZAPServer) encodeReject(orderID uint64, reason string) []byte {
|
||||
// Rejection: order_id(8) + status(1=rejected) + reason_len(2) + reason(variable)
|
||||
reasonBytes := []byte(reason)
|
||||
if len(reasonBytes) > 256 {
|
||||
reasonBytes = reasonBytes[:256]
|
||||
}
|
||||
|
||||
size := 11 + len(reasonBytes)
|
||||
resp := s.getBuffer()[:size]
|
||||
binary.BigEndian.PutUint64(resp[0:8], orderID)
|
||||
resp[8] = 2 // Rejected status
|
||||
binary.BigEndian.PutUint16(resp[9:11], uint16(len(reasonBytes)))
|
||||
copy(resp[11:], reasonBytes)
|
||||
return resp[:size]
|
||||
}
|
||||
|
||||
// encodeQuote encodes a price level quote
|
||||
func (s *ZAPServer) encodeQuote(price, size float64, count int) []byte {
|
||||
resp := s.getBuffer()[:QuoteWireSize]
|
||||
s.encodeQuoteAt(resp, price, size, count)
|
||||
return resp[:QuoteWireSize]
|
||||
}
|
||||
|
||||
// encodeQuoteAt encodes a quote at a specific buffer position
|
||||
func (s *ZAPServer) encodeQuoteAt(buf []byte, price, size float64, count int) {
|
||||
encodeFloat64(buf[0:8], price)
|
||||
encodeFloat64(buf[8:16], size)
|
||||
binary.BigEndian.PutUint32(buf[16:20], uint32(count))
|
||||
}
|
||||
|
||||
// encodeOrderResponse encodes a full order response
|
||||
func (s *ZAPServer) encodeOrderResponse(order *lx.Order) []byte {
|
||||
resp := s.getBuffer()[:OrderWireSize]
|
||||
|
||||
// Symbol (8 bytes, null-padded)
|
||||
copy(resp[0:8], padNull([]byte(order.Symbol), 8))
|
||||
binary.BigEndian.PutUint64(resp[8:16], order.ID)
|
||||
encodeFloat64(resp[16:24], order.Price)
|
||||
encodeFloat64(resp[24:32], order.Size)
|
||||
resp[32] = byte(order.Side)
|
||||
resp[33] = byte(order.Type)
|
||||
binary.BigEndian.PutUint16(resp[34:36], uint16(order.Flags))
|
||||
binary.BigEndian.PutUint64(resp[36:44], uint64(order.Timestamp.UnixNano()))
|
||||
copy(resp[44:60], padNull([]byte(order.UserID), 16))
|
||||
|
||||
return resp[:OrderWireSize]
|
||||
}
|
||||
|
||||
// getBuffer gets a buffer from the pool
|
||||
func (s *ZAPServer) getBuffer() []byte {
|
||||
return s.bufferPool.Get().([]byte)
|
||||
}
|
||||
|
||||
// Helper functions for binary encoding
|
||||
|
||||
func decodeFloat64(b []byte) float64 {
|
||||
bits := binary.BigEndian.Uint64(b)
|
||||
return *(*float64)(unsafe.Pointer(&bits))
|
||||
}
|
||||
|
||||
func encodeFloat64(b []byte, f float64) {
|
||||
bits := *(*uint64)(unsafe.Pointer(&f))
|
||||
binary.BigEndian.PutUint64(b, bits)
|
||||
}
|
||||
|
||||
func trimNull(b []byte) []byte {
|
||||
for i := len(b) - 1; i >= 0; i-- {
|
||||
if b[i] != 0 {
|
||||
return b[:i+1]
|
||||
}
|
||||
}
|
||||
return b[:0]
|
||||
}
|
||||
|
||||
func padNull(b []byte, size int) []byte {
|
||||
if len(b) >= size {
|
||||
return b[:size]
|
||||
}
|
||||
result := make([]byte, size)
|
||||
copy(result, b)
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/log"
|
||||
"github.com/luxfi/dex/pkg/lx"
|
||||
"github.com/luxfi/rpc"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// nopLogger is a no-op logger for testing
|
||||
type nopLogger struct{}
|
||||
|
||||
func (nopLogger) Info(msg string, args ...interface{}) {}
|
||||
func (nopLogger) Error(msg string, args ...interface{}) {}
|
||||
func (nopLogger) Warn(msg string, args ...interface{}) {}
|
||||
func (nopLogger) Debug(msg string, args ...interface{}) {}
|
||||
func (nopLogger) Fatal(msg string, args ...interface{}) {}
|
||||
func (nopLogger) WithField(key string, value interface{}) log.Logger { return nopLogger{} }
|
||||
|
||||
func TestZAPServerBasic(t *testing.T) {
|
||||
// Create orderbook
|
||||
ob := lx.NewOrderBook("BTC-USD")
|
||||
|
||||
// Create and start ZAP server
|
||||
logger := nopLogger{}
|
||||
server := NewZAPServer(ob, ":0", logger)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := server.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
defer server.Stop()
|
||||
|
||||
// Give server time to start
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Connect client
|
||||
client, err := rpc.Dial(ctx, server.Addr())
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
// Test place order
|
||||
orderPayload := encodeTestOrder(1, 50000.0, 1.0, lx.Buy, lx.Limit, "user1")
|
||||
resp, err := client.CallRaw(ctx, "place_order", orderPayload)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
// Verify ack
|
||||
assert.GreaterOrEqual(t, len(resp), 8)
|
||||
orderID := binary.BigEndian.Uint64(resp[0:8])
|
||||
assert.Greater(t, orderID, uint64(0))
|
||||
|
||||
// Check stats
|
||||
orders, trades, cancels := server.Stats()
|
||||
assert.Equal(t, uint64(1), orders)
|
||||
assert.Equal(t, uint64(0), trades)
|
||||
assert.Equal(t, uint64(0), cancels)
|
||||
}
|
||||
|
||||
func TestZAPServerOrderBook(t *testing.T) {
|
||||
ob := lx.NewOrderBook("BTC-USD")
|
||||
logger := nopLogger{}
|
||||
server := NewZAPServer(ob, ":0", logger)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := server.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
defer server.Stop()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
client, err := rpc.Dial(ctx, server.Addr())
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
// Place some orders
|
||||
for i := 0; i < 5; i++ {
|
||||
price := 50000.0 - float64(i)*100
|
||||
orderPayload := encodeTestOrder(uint64(i+1), price, 1.0, lx.Buy, lx.Limit, "user1")
|
||||
_, err := client.CallRaw(ctx, "place_order", orderPayload)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
price := 50100.0 + float64(i)*100
|
||||
orderPayload := encodeTestOrder(uint64(i+100), price, 1.0, lx.Sell, lx.Limit, "user2")
|
||||
_, err := client.CallRaw(ctx, "place_order", orderPayload)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Get best bid
|
||||
resp, err := client.CallRaw(ctx, "best_bid", nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(resp), QuoteWireSize)
|
||||
|
||||
price := decodeFloat64(resp[0:8])
|
||||
assert.Equal(t, 50000.0, price)
|
||||
|
||||
// Get best ask
|
||||
resp, err = client.CallRaw(ctx, "best_ask", nil)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(resp), QuoteWireSize)
|
||||
|
||||
price = decodeFloat64(resp[0:8])
|
||||
assert.Equal(t, 50100.0, price)
|
||||
|
||||
// Get orderbook
|
||||
depthReq := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(depthReq, 10)
|
||||
resp, err = client.CallRaw(ctx, "orderbook", depthReq)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(resp), 8)
|
||||
|
||||
bidCount := binary.BigEndian.Uint32(resp[0:4])
|
||||
askCount := binary.BigEndian.Uint32(resp[4:8])
|
||||
assert.Equal(t, uint32(5), bidCount)
|
||||
assert.Equal(t, uint32(5), askCount)
|
||||
}
|
||||
|
||||
func TestZAPServerCancelOrder(t *testing.T) {
|
||||
ob := lx.NewOrderBook("BTC-USD")
|
||||
logger := nopLogger{}
|
||||
server := NewZAPServer(ob, ":0", logger)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := server.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
defer server.Stop()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
client, err := rpc.Dial(ctx, server.Addr())
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
// Place order
|
||||
orderPayload := encodeTestOrder(1, 50000.0, 1.0, lx.Buy, lx.Limit, "user1")
|
||||
resp, err := client.CallRaw(ctx, "place_order", orderPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
orderID := binary.BigEndian.Uint64(resp[0:8])
|
||||
|
||||
// Cancel order
|
||||
cancelPayload := make([]byte, CancelWireSize)
|
||||
binary.BigEndian.PutUint64(cancelPayload[0:8], orderID)
|
||||
copy(cancelPayload[8:24], padNull([]byte("user1"), 16))
|
||||
|
||||
resp, err = client.CallRaw(ctx, "cancel_order", cancelPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify ack with cancel status
|
||||
assert.GreaterOrEqual(t, len(resp), 9)
|
||||
respOrderID := binary.BigEndian.Uint64(resp[0:8])
|
||||
assert.Equal(t, orderID, respOrderID)
|
||||
assert.Equal(t, uint8(1), resp[8]) // Cancel status
|
||||
|
||||
// Check stats
|
||||
_, _, cancels := server.Stats()
|
||||
assert.Equal(t, uint64(1), cancels)
|
||||
}
|
||||
|
||||
func TestZAPServerGetOrder(t *testing.T) {
|
||||
ob := lx.NewOrderBook("BTC-USD")
|
||||
logger := nopLogger{}
|
||||
server := NewZAPServer(ob, ":0", logger)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := server.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
defer server.Stop()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
client, err := rpc.Dial(ctx, server.Addr())
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
// Place order
|
||||
orderPayload := encodeTestOrder(1, 50000.0, 2.5, lx.Buy, lx.Limit, "user1")
|
||||
resp, err := client.CallRaw(ctx, "place_order", orderPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
orderID := binary.BigEndian.Uint64(resp[0:8])
|
||||
|
||||
// Get order
|
||||
getReq := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(getReq, orderID)
|
||||
|
||||
resp, err = client.CallRaw(ctx, "order", getReq)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(resp), OrderWireSize)
|
||||
|
||||
// Decode order response
|
||||
respOrderID := binary.BigEndian.Uint64(resp[8:16])
|
||||
respPrice := decodeFloat64(resp[16:24])
|
||||
respSize := decodeFloat64(resp[24:32])
|
||||
respSide := lx.Side(resp[32])
|
||||
|
||||
assert.Equal(t, orderID, respOrderID)
|
||||
assert.Equal(t, 50000.0, respPrice)
|
||||
assert.Equal(t, 2.5, respSize)
|
||||
assert.Equal(t, lx.Buy, respSide)
|
||||
}
|
||||
|
||||
func BenchmarkZAPOrderPlacement(b *testing.B) {
|
||||
ob := lx.NewOrderBook("BTC-USD")
|
||||
logger := nopLogger{}
|
||||
server := NewZAPServer(ob, ":0", logger)
|
||||
|
||||
ctx := context.Background()
|
||||
err := server.Start(ctx)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
client, err := rpc.Dial(ctx, server.Addr())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
orderPayload := encodeTestOrder(0, 50000.0, 1.0, lx.Buy, lx.Limit, "user1")
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Update order ID
|
||||
binary.BigEndian.PutUint64(orderPayload[8:16], uint64(i+1))
|
||||
|
||||
_, err := client.CallRaw(ctx, "place_order", orderPayload)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkZAPBestBid(b *testing.B) {
|
||||
ob := lx.NewOrderBook("BTC-USD")
|
||||
|
||||
// Add some orders
|
||||
for i := 0; i < 100; i++ {
|
||||
order := &lx.Order{
|
||||
ID: uint64(i + 1),
|
||||
Price: 50000.0 - float64(i),
|
||||
Size: 1.0,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
UserID: "user1",
|
||||
}
|
||||
ob.AddOrder(order)
|
||||
}
|
||||
|
||||
logger := nopLogger{}
|
||||
server := NewZAPServer(ob, ":0", logger)
|
||||
|
||||
ctx := context.Background()
|
||||
err := server.Start(ctx)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer server.Stop()
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
client, err := rpc.Dial(ctx, server.Addr())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := client.CallRaw(ctx, "best_bid", nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to encode a test order
|
||||
func encodeTestOrder(id uint64, price, size float64, side lx.Side, orderType lx.OrderType, userID string) []byte {
|
||||
buf := make([]byte, OrderWireSize)
|
||||
|
||||
// Symbol (8 bytes)
|
||||
copy(buf[0:8], padNull([]byte("BTC-USD"), 8))
|
||||
|
||||
// Order ID
|
||||
binary.BigEndian.PutUint64(buf[8:16], id)
|
||||
|
||||
// Price
|
||||
encodeFloat64(buf[16:24], price)
|
||||
|
||||
// Size
|
||||
encodeFloat64(buf[24:32], size)
|
||||
|
||||
// Side
|
||||
buf[32] = byte(side)
|
||||
|
||||
// Type
|
||||
buf[33] = byte(orderType)
|
||||
|
||||
// Flags (none)
|
||||
binary.BigEndian.PutUint16(buf[34:36], 0)
|
||||
|
||||
// Timestamp
|
||||
binary.BigEndian.PutUint64(buf[36:44], uint64(time.Now().UnixNano()))
|
||||
|
||||
// User ID
|
||||
copy(buf[44:60], padNull([]byte(userID), 16))
|
||||
|
||||
return buf
|
||||
}
|
||||
+40
-40
@@ -470,11 +470,11 @@ func TestDAGOrderBookAddOrder(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
order := &lx.Order{
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
}
|
||||
|
||||
vertex, err := dob.AddOrder(order)
|
||||
@@ -491,11 +491,11 @@ func TestDAGOrderBookAddMultipleOrders(t *testing.T) {
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
order := &lx.Order{
|
||||
ID: uint64(i),
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0 + float64(i*100),
|
||||
Size: 1.0,
|
||||
ID: uint64(i),
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0 + float64(i*100),
|
||||
Size: 1.0,
|
||||
}
|
||||
vertex, err := dob.AddOrder(order)
|
||||
require.NoError(t, err)
|
||||
@@ -526,11 +526,11 @@ func TestLuxDAGOrderBookAddOrder(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
order := &lx.Order{
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
}
|
||||
|
||||
vertex, err := lux.AddOrder(order)
|
||||
@@ -564,11 +564,11 @@ func TestLuxDAGOrderBookRunFPCRound(t *testing.T) {
|
||||
|
||||
// Add an order
|
||||
order := &lx.Order{
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
}
|
||||
_, err = lux.AddOrder(order)
|
||||
require.NoError(t, err)
|
||||
@@ -671,11 +671,11 @@ func TestLuxDAGOrderBookGenerateQuantumCertificate(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
order := &lx.Order{
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
}
|
||||
|
||||
vertex, err := lux.AddOrder(order)
|
||||
@@ -715,11 +715,11 @@ func TestLuxDAGOrderBookProcessQuasarCertificates(t *testing.T) {
|
||||
|
||||
// Add an order which tracks in quasar
|
||||
order := &lx.Order{
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
ID: 1,
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
}
|
||||
vertex, err := lux.AddOrder(order)
|
||||
require.NoError(t, err)
|
||||
@@ -912,11 +912,11 @@ func BenchmarkDAGOrderBookAddOrder(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
order := &lx.Order{
|
||||
ID: uint64(i),
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
ID: uint64(i),
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
}
|
||||
_, _ = dob.AddOrder(order)
|
||||
}
|
||||
@@ -928,11 +928,11 @@ func BenchmarkLuxDAGOrderBookAddOrder(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
order := &lx.Order{
|
||||
ID: uint64(i),
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
ID: uint64(i),
|
||||
Side: lx.Buy,
|
||||
Type: lx.Limit,
|
||||
Price: 50000.0,
|
||||
Size: 1.0,
|
||||
}
|
||||
_, _ = lux.AddOrder(order)
|
||||
}
|
||||
|
||||
+17
-17
@@ -22,7 +22,7 @@ type Gateway struct {
|
||||
func New(config GatewayConfig) *Gateway {
|
||||
registry := NewRegistry()
|
||||
router := NewRouter(registry, config.EnableFallback)
|
||||
|
||||
|
||||
return &Gateway{
|
||||
registry: registry,
|
||||
router: router,
|
||||
@@ -54,15 +54,15 @@ func (g *Gateway) GetRegistry() *Registry {
|
||||
func (g *Gateway) Start(addr string) error {
|
||||
cfg := DefaultServerConfig()
|
||||
cfg.Addr = addr
|
||||
|
||||
|
||||
g.server = NewServer(g.router, cfg)
|
||||
|
||||
|
||||
log.Printf("Starting gateway server on %s", addr)
|
||||
log.Printf("Registered providers: %d", len(g.registry.ListProviders()))
|
||||
for _, info := range g.registry.ListProviders() {
|
||||
log.Printf(" - %s (priority: %d, chains: %v)", info.Name, info.Priority, info.SupportedChains)
|
||||
}
|
||||
|
||||
|
||||
return g.server.Start()
|
||||
}
|
||||
|
||||
@@ -70,41 +70,41 @@ func (g *Gateway) Start(addr string) error {
|
||||
func (g *Gateway) StartWithGracefulShutdown(addr string) error {
|
||||
cfg := DefaultServerConfig()
|
||||
cfg.Addr = addr
|
||||
|
||||
|
||||
g.server = NewServer(g.router, cfg)
|
||||
|
||||
|
||||
// Channel to receive OS signals
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
|
||||
// Channel to receive server errors
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
|
||||
go func() {
|
||||
log.Printf("Starting gateway server on %s", addr)
|
||||
log.Printf("Registered providers: %d", len(g.registry.ListProviders()))
|
||||
for _, info := range g.registry.ListProviders() {
|
||||
log.Printf(" - %s (priority: %d, chains: %v)", info.Name, info.Priority, info.SupportedChains)
|
||||
}
|
||||
|
||||
|
||||
if err := g.server.Start(); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case sig := <-quit:
|
||||
log.Printf("Received signal %v, shutting down...", sig)
|
||||
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
|
||||
if err := g.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("shutdown error: %w", err)
|
||||
}
|
||||
|
||||
|
||||
log.Println("Server gracefully stopped")
|
||||
return nil
|
||||
}
|
||||
@@ -113,23 +113,23 @@ func (g *Gateway) StartWithGracefulShutdown(addr string) error {
|
||||
// Shutdown gracefully shuts down the gateway
|
||||
func (g *Gateway) Shutdown(ctx context.Context) error {
|
||||
var errs []error
|
||||
|
||||
|
||||
// Shutdown HTTP server
|
||||
if g.server != nil {
|
||||
if err := g.server.Shutdown(ctx); err != nil {
|
||||
errs = append(errs, fmt.Errorf("server shutdown: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Close all providers
|
||||
if err := g.registry.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("registry close: %w", err))
|
||||
}
|
||||
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("shutdown errors: %v", errs)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+45
-45
@@ -74,14 +74,14 @@ func (m *MockProvider) GetQuotes(ctx context.Context, req QuoteRequest) ([]SwapQ
|
||||
|
||||
func TestRegistryRegisterProvider(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
|
||||
|
||||
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
|
||||
|
||||
|
||||
// Register should succeed
|
||||
if err := registry.RegisterProvider(provider); err != nil {
|
||||
t.Fatalf("Failed to register provider: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Registering same provider should fail
|
||||
if err := registry.RegisterProvider(provider); err == nil {
|
||||
t.Fatal("Expected error when registering duplicate provider")
|
||||
@@ -91,21 +91,21 @@ func TestRegistryRegisterProvider(t *testing.T) {
|
||||
func TestRegistryGetProvider(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
|
||||
|
||||
|
||||
if err := registry.RegisterProvider(provider); err != nil {
|
||||
t.Fatalf("Failed to register provider: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Get should succeed
|
||||
got, err := registry.GetProvider("test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get provider: %v", err)
|
||||
}
|
||||
|
||||
|
||||
if got.Info().Name != "test" {
|
||||
t.Errorf("Expected provider name 'test', got %s", got.Info().Name)
|
||||
}
|
||||
|
||||
|
||||
// Get non-existent should fail
|
||||
_, err = registry.GetProvider("nonexistent")
|
||||
if err == nil {
|
||||
@@ -115,22 +115,22 @@ func TestRegistryGetProvider(t *testing.T) {
|
||||
|
||||
func TestRegistryListProviders(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
|
||||
|
||||
// Register providers with different priorities
|
||||
p1 := NewMockProvider("high", 10, []ChainID{ChainIDLux})
|
||||
p2 := NewMockProvider("low", 100, []ChainID{ChainIDLux})
|
||||
p3 := NewMockProvider("medium", 50, []ChainID{ChainIDLux})
|
||||
|
||||
|
||||
registry.RegisterProvider(p1)
|
||||
registry.RegisterProvider(p2)
|
||||
registry.RegisterProvider(p3)
|
||||
|
||||
|
||||
providers := registry.ListProviders()
|
||||
|
||||
|
||||
if len(providers) != 3 {
|
||||
t.Fatalf("Expected 3 providers, got %d", len(providers))
|
||||
}
|
||||
|
||||
|
||||
// Should be sorted by priority
|
||||
if providers[0].Name != "high" {
|
||||
t.Errorf("Expected first provider to be 'high', got %s", providers[0].Name)
|
||||
@@ -146,22 +146,22 @@ func TestRegistryListProviders(t *testing.T) {
|
||||
func TestRegistryUnregisterProvider(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
|
||||
|
||||
|
||||
if err := registry.RegisterProvider(provider); err != nil {
|
||||
t.Fatalf("Failed to register provider: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Unregister should succeed
|
||||
if err := registry.UnregisterProvider("test"); err != nil {
|
||||
t.Fatalf("Failed to unregister provider: %v", err)
|
||||
}
|
||||
|
||||
|
||||
// Provider should no longer exist
|
||||
_, err := registry.GetProvider("test")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when getting unregistered provider")
|
||||
}
|
||||
|
||||
|
||||
// Unregister non-existent should fail
|
||||
if err := registry.UnregisterProvider("nonexistent"); err == nil {
|
||||
t.Fatal("Expected error when unregistering non-existent provider")
|
||||
@@ -170,16 +170,16 @@ func TestRegistryUnregisterProvider(t *testing.T) {
|
||||
|
||||
func TestRouterGetBestQuote(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
|
||||
|
||||
// Create two mock providers that implement QuoteProvider
|
||||
p1 := NewMockProvider("fast", 10, []ChainID{ChainIDLux})
|
||||
p2 := NewMockProvider("slow", 100, []ChainID{ChainIDLux})
|
||||
|
||||
|
||||
registry.RegisterProvider(p1)
|
||||
registry.RegisterProvider(p2)
|
||||
|
||||
|
||||
router := NewRouter(registry, true)
|
||||
|
||||
|
||||
req := QuoteRequest{
|
||||
TokenIn: Token{
|
||||
Address: "0x1111111111111111111111111111111111111111",
|
||||
@@ -193,16 +193,16 @@ func TestRouterGetBestQuote(t *testing.T) {
|
||||
IsExactIn: true,
|
||||
ChainID: ChainIDLux,
|
||||
}
|
||||
|
||||
|
||||
quote, err := router.GetBestQuote(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get quote: %v", err)
|
||||
}
|
||||
|
||||
|
||||
if quote == nil {
|
||||
t.Fatal("Expected quote, got nil")
|
||||
}
|
||||
|
||||
|
||||
// Should return the quote with highest output amount
|
||||
expectedOutput := new(big.Int).Mul(req.Amount, big.NewInt(2))
|
||||
if quote.TokenOut.Amount.Cmp(expectedOutput) != 0 {
|
||||
@@ -213,7 +213,7 @@ func TestRouterGetBestQuote(t *testing.T) {
|
||||
func TestRouterNoProviders(t *testing.T) {
|
||||
registry := NewRegistry()
|
||||
router := NewRouter(registry, true)
|
||||
|
||||
|
||||
req := QuoteRequest{
|
||||
TokenIn: Token{
|
||||
Address: "0x1111111111111111111111111111111111111111",
|
||||
@@ -227,12 +227,12 @@ func TestRouterNoProviders(t *testing.T) {
|
||||
IsExactIn: true,
|
||||
ChainID: ChainIDLux,
|
||||
}
|
||||
|
||||
|
||||
_, err := router.GetBestQuote(context.Background(), req)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error when no providers available")
|
||||
}
|
||||
|
||||
|
||||
if err != ErrNoProvidersAvailable {
|
||||
t.Errorf("Expected ErrNoProvidersAvailable, got %v", err)
|
||||
}
|
||||
@@ -241,15 +241,15 @@ func TestRouterNoProviders(t *testing.T) {
|
||||
func TestGatewayNew(t *testing.T) {
|
||||
config := DefaultGatewayConfig()
|
||||
gw := New(config)
|
||||
|
||||
|
||||
if gw == nil {
|
||||
t.Fatal("Expected gateway, got nil")
|
||||
}
|
||||
|
||||
|
||||
if gw.router == nil {
|
||||
t.Fatal("Expected router, got nil")
|
||||
}
|
||||
|
||||
|
||||
if gw.registry == nil {
|
||||
t.Fatal("Expected registry, got nil")
|
||||
}
|
||||
@@ -258,18 +258,18 @@ func TestGatewayNew(t *testing.T) {
|
||||
func TestGatewayRegisterProvider(t *testing.T) {
|
||||
config := DefaultGatewayConfig()
|
||||
gw := New(config)
|
||||
|
||||
|
||||
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
|
||||
|
||||
|
||||
if err := gw.RegisterProvider(provider); err != nil {
|
||||
t.Fatalf("Failed to register provider: %v", err)
|
||||
}
|
||||
|
||||
|
||||
providers := gw.ListProviders()
|
||||
if len(providers) != 1 {
|
||||
t.Fatalf("Expected 1 provider, got %d", len(providers))
|
||||
}
|
||||
|
||||
|
||||
if providers[0].Name != "test" {
|
||||
t.Errorf("Expected provider name 'test', got %s", providers[0].Name)
|
||||
}
|
||||
@@ -278,20 +278,20 @@ func TestGatewayRegisterProvider(t *testing.T) {
|
||||
func TestGatewayHealthCheck(t *testing.T) {
|
||||
config := DefaultGatewayConfig()
|
||||
gw := New(config)
|
||||
|
||||
|
||||
provider := NewMockProvider("test", 10, []ChainID{ChainIDLux})
|
||||
gw.RegisterProvider(provider)
|
||||
|
||||
|
||||
checks := gw.HealthCheck(context.Background())
|
||||
|
||||
|
||||
if len(checks) != 1 {
|
||||
t.Fatalf("Expected 1 health check, got %d", len(checks))
|
||||
}
|
||||
|
||||
|
||||
if !checks[0].Healthy {
|
||||
t.Error("Expected healthy provider")
|
||||
}
|
||||
|
||||
|
||||
if checks[0].Provider != "test" {
|
||||
t.Errorf("Expected provider 'test', got %s", checks[0].Provider)
|
||||
}
|
||||
@@ -302,11 +302,11 @@ func TestChainIDConstants(t *testing.T) {
|
||||
if ChainIDLux != 96369 {
|
||||
t.Errorf("Expected ChainIDLux to be 96369, got %d", ChainIDLux)
|
||||
}
|
||||
|
||||
|
||||
if ChainIDZoo != 200200 {
|
||||
t.Errorf("Expected ChainIDZoo to be 200200, got %d", ChainIDZoo)
|
||||
}
|
||||
|
||||
|
||||
if ChainIDEthereum != 1 {
|
||||
t.Errorf("Expected ChainIDEthereum to be 1, got %d", ChainIDEthereum)
|
||||
}
|
||||
@@ -317,12 +317,12 @@ func TestProviderError(t *testing.T) {
|
||||
Provider: "test",
|
||||
Err: ErrNoProvidersAvailable,
|
||||
}
|
||||
|
||||
|
||||
expected := "test: no providers available"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Expected error message %q, got %q", expected, err.Error())
|
||||
}
|
||||
|
||||
|
||||
// Test Unwrap
|
||||
if err.Unwrap() != ErrNoProvidersAvailable {
|
||||
t.Error("Expected Unwrap to return original error")
|
||||
@@ -332,14 +332,14 @@ func TestProviderError(t *testing.T) {
|
||||
func TestWithRequestID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
requestID := "test-request-123"
|
||||
|
||||
|
||||
ctxWithID := WithRequestID(ctx, requestID)
|
||||
|
||||
|
||||
got := GetRequestID(ctxWithID)
|
||||
if got != requestID {
|
||||
t.Errorf("Expected request ID %q, got %q", requestID, got)
|
||||
}
|
||||
|
||||
|
||||
// Test getting request ID from context without one
|
||||
got = GetRequestID(ctx)
|
||||
if got != "" {
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/dex/pkg/gateway"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/geth/ethclient"
|
||||
"github.com/luxfi/dex/pkg/gateway"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -44,9 +44,9 @@ type Provider struct {
|
||||
|
||||
// ProviderConfig holds provider configuration
|
||||
type ProviderConfig struct {
|
||||
Name string
|
||||
Priority int
|
||||
Chains []gateway.ChainID
|
||||
Name string
|
||||
Priority int
|
||||
Chains []gateway.ChainID
|
||||
RPCEndpoints map[gateway.ChainID]string // Map of chainID to RPC endpoint URL
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func DefaultConfig() ProviderConfig {
|
||||
gateway.ChainIDZoo,
|
||||
},
|
||||
RPCEndpoints: map[gateway.ChainID]string{
|
||||
gateway.ChainIDLux: "http://127.0.0.1:9630/ext/bc/C/rpc", // Lux mainnet C-Chain
|
||||
gateway.ChainIDLux: "http://127.0.0.1:9630/ext/bc/C/rpc", // Lux mainnet C-Chain
|
||||
gateway.ChainIDZoo: "http://127.0.0.1:9630/ext/bc/2iJykKjE7gpWNjGUvGG6fVtj7u5Tbvo89CVCu6gjNPCnEdCVpY/rpc", // Zoo chain
|
||||
},
|
||||
}
|
||||
|
||||
+29
-29
@@ -8,10 +8,10 @@ import (
|
||||
type Provider interface {
|
||||
// Info returns provider information
|
||||
Info() ProviderInfo
|
||||
|
||||
|
||||
// HealthCheck performs a health check
|
||||
HealthCheck(ctx context.Context) HealthCheck
|
||||
|
||||
|
||||
// Close cleans up provider resources
|
||||
Close() error
|
||||
}
|
||||
@@ -19,10 +19,10 @@ type Provider interface {
|
||||
// QuoteProvider provides swap quotes
|
||||
type QuoteProvider interface {
|
||||
Provider
|
||||
|
||||
|
||||
// GetQuote returns a swap quote for the given request
|
||||
GetQuote(ctx context.Context, req QuoteRequest) (*SwapQuote, error)
|
||||
|
||||
|
||||
// GetQuotes returns multiple quotes for comparison
|
||||
GetQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuote, error)
|
||||
}
|
||||
@@ -30,10 +30,10 @@ type QuoteProvider interface {
|
||||
// SwapProvider handles swap execution
|
||||
type SwapProvider interface {
|
||||
QuoteProvider
|
||||
|
||||
|
||||
// BuildSwap builds a swap transaction from a quote
|
||||
BuildSwap(ctx context.Context, quote SwapQuote, recipient string, deadline int64) (*SwapTransaction, error)
|
||||
|
||||
|
||||
// ExecuteSwap executes a swap (for providers that support server-side execution)
|
||||
ExecuteSwap(ctx context.Context, req SwapRequest) (string, error)
|
||||
}
|
||||
@@ -41,16 +41,16 @@ type SwapProvider interface {
|
||||
// LiquidityProvider provides liquidity-related functionality
|
||||
type LiquidityProvider interface {
|
||||
Provider
|
||||
|
||||
|
||||
// GetPools returns pools matching the request criteria
|
||||
GetPools(ctx context.Context, req PoolsRequest) ([]Pool, error)
|
||||
|
||||
|
||||
// GetPool returns a specific pool by address
|
||||
GetPool(ctx context.Context, chainID ChainID, address string) (*Pool, error)
|
||||
|
||||
|
||||
// GetPositions returns positions for an owner
|
||||
GetPositions(ctx context.Context, req PositionsRequest) ([]Position, error)
|
||||
|
||||
|
||||
// GetPosition returns a specific position
|
||||
GetPosition(ctx context.Context, chainID ChainID, positionID string) (*Position, error)
|
||||
}
|
||||
@@ -58,13 +58,13 @@ type LiquidityProvider interface {
|
||||
// LiquidityMutationProvider handles liquidity mutations
|
||||
type LiquidityMutationProvider interface {
|
||||
LiquidityProvider
|
||||
|
||||
|
||||
// BuildAddLiquidity builds a transaction to add liquidity
|
||||
BuildAddLiquidity(ctx context.Context, pool Pool, amount0, amount1 string, tickLower, tickUpper int) (*SwapTransaction, error)
|
||||
|
||||
|
||||
// BuildRemoveLiquidity builds a transaction to remove liquidity
|
||||
BuildRemoveLiquidity(ctx context.Context, position Position, percentage float64) (*SwapTransaction, error)
|
||||
|
||||
|
||||
// BuildCollectFees builds a transaction to collect fees
|
||||
BuildCollectFees(ctx context.Context, position Position) (*SwapTransaction, error)
|
||||
}
|
||||
@@ -72,10 +72,10 @@ type LiquidityMutationProvider interface {
|
||||
// PriceProvider provides token price data
|
||||
type PriceProvider interface {
|
||||
Provider
|
||||
|
||||
|
||||
// GetTokenPrice returns the current price for a token
|
||||
GetTokenPrice(ctx context.Context, token Token) (*TokenPrice, error)
|
||||
|
||||
|
||||
// GetTokenPrices returns prices for multiple tokens
|
||||
GetTokenPrices(ctx context.Context, tokens []Token) ([]TokenPrice, error)
|
||||
}
|
||||
@@ -83,16 +83,16 @@ type PriceProvider interface {
|
||||
// ConversionProvider handles conversion tracking
|
||||
type ConversionProvider interface {
|
||||
Provider
|
||||
|
||||
|
||||
// CreateLead creates a new conversion lead
|
||||
CreateLead(ctx context.Context, lead ConversionLead) (*ConversionLead, error)
|
||||
|
||||
|
||||
// GetLead retrieves a conversion lead
|
||||
GetLead(ctx context.Context, leadID string) (*ConversionLead, error)
|
||||
|
||||
|
||||
// TrackEvent tracks a conversion event
|
||||
TrackEvent(ctx context.Context, event ConversionEvent) error
|
||||
|
||||
|
||||
// GetLeadEvents gets events for a lead
|
||||
GetLeadEvents(ctx context.Context, leadID string) ([]ConversionEvent, error)
|
||||
}
|
||||
@@ -100,13 +100,13 @@ type ConversionProvider interface {
|
||||
// TokenListProvider provides token list functionality
|
||||
type TokenListProvider interface {
|
||||
Provider
|
||||
|
||||
|
||||
// GetTokenList returns the token list for a chain
|
||||
GetTokenList(ctx context.Context, chainID ChainID) ([]Token, error)
|
||||
|
||||
|
||||
// SearchTokens searches for tokens by symbol or address
|
||||
SearchTokens(ctx context.Context, chainID ChainID, query string) ([]Token, error)
|
||||
|
||||
|
||||
// GetToken returns a specific token
|
||||
GetToken(ctx context.Context, chainID ChainID, address string) (*Token, error)
|
||||
}
|
||||
@@ -124,25 +124,25 @@ type FullProvider interface {
|
||||
type ProviderRegistry interface {
|
||||
// RegisterProvider registers a provider
|
||||
RegisterProvider(provider Provider) error
|
||||
|
||||
|
||||
// UnregisterProvider removes a provider
|
||||
UnregisterProvider(name string) error
|
||||
|
||||
|
||||
// GetProvider returns a provider by name
|
||||
GetProvider(name string) (Provider, error)
|
||||
|
||||
|
||||
// ListProviders returns all registered providers
|
||||
ListProviders() []ProviderInfo
|
||||
|
||||
|
||||
// GetQuoteProviders returns all quote providers
|
||||
GetQuoteProviders() []QuoteProvider
|
||||
|
||||
|
||||
// GetLiquidityProviders returns all liquidity providers
|
||||
GetLiquidityProviders() []LiquidityProvider
|
||||
|
||||
|
||||
// GetPriceProviders returns all price providers
|
||||
GetPriceProviders() []PriceProvider
|
||||
|
||||
|
||||
// GetConversionProviders returns all conversion providers
|
||||
GetConversionProviders() []ConversionProvider
|
||||
}
|
||||
|
||||
+25
-25
@@ -33,16 +33,16 @@ func NewRegistry() *Registry {
|
||||
func (r *Registry) RegisterProvider(provider Provider) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
|
||||
info := provider.Info()
|
||||
if _, exists := r.providers[info.Name]; exists {
|
||||
return ErrProviderExists
|
||||
}
|
||||
|
||||
|
||||
r.providers[info.Name] = provider
|
||||
r.order = append(r.order, info.Name)
|
||||
r.sortByPriority()
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -50,13 +50,13 @@ func (r *Registry) RegisterProvider(provider Provider) error {
|
||||
func (r *Registry) UnregisterProvider(name string) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
|
||||
if _, exists := r.providers[name]; !exists {
|
||||
return ErrProviderNotFound
|
||||
}
|
||||
|
||||
|
||||
delete(r.providers, name)
|
||||
|
||||
|
||||
// Remove from order slice
|
||||
newOrder := make([]string, 0, len(r.order)-1)
|
||||
for _, n := range r.order {
|
||||
@@ -65,7 +65,7 @@ func (r *Registry) UnregisterProvider(name string) error {
|
||||
}
|
||||
}
|
||||
r.order = newOrder
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -73,12 +73,12 @@ func (r *Registry) UnregisterProvider(name string) error {
|
||||
func (r *Registry) GetProvider(name string) (Provider, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
provider, exists := r.providers[name]
|
||||
if !exists {
|
||||
return nil, ErrProviderNotFound
|
||||
}
|
||||
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
@@ -86,14 +86,14 @@ func (r *Registry) GetProvider(name string) (Provider, error) {
|
||||
func (r *Registry) ListProviders() []ProviderInfo {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
infos := make([]ProviderInfo, 0, len(r.providers))
|
||||
for _, name := range r.order {
|
||||
if provider, exists := r.providers[name]; exists {
|
||||
infos = append(infos, provider.Info())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return infos
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ func (r *Registry) ListProviders() []ProviderInfo {
|
||||
func (r *Registry) GetQuoteProviders() []QuoteProvider {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
providers := make([]QuoteProvider, 0)
|
||||
for _, name := range r.order {
|
||||
if provider, exists := r.providers[name]; exists {
|
||||
@@ -110,7 +110,7 @@ func (r *Registry) GetQuoteProviders() []QuoteProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func (r *Registry) GetQuoteProviders() []QuoteProvider {
|
||||
func (r *Registry) GetLiquidityProviders() []LiquidityProvider {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
providers := make([]LiquidityProvider, 0)
|
||||
for _, name := range r.order {
|
||||
if provider, exists := r.providers[name]; exists {
|
||||
@@ -127,7 +127,7 @@ func (r *Registry) GetLiquidityProviders() []LiquidityProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ func (r *Registry) GetLiquidityProviders() []LiquidityProvider {
|
||||
func (r *Registry) GetPriceProviders() []PriceProvider {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
providers := make([]PriceProvider, 0)
|
||||
for _, name := range r.order {
|
||||
if provider, exists := r.providers[name]; exists {
|
||||
@@ -144,7 +144,7 @@ func (r *Registry) GetPriceProviders() []PriceProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func (r *Registry) GetPriceProviders() []PriceProvider {
|
||||
func (r *Registry) GetConversionProviders() []ConversionProvider {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
|
||||
providers := make([]ConversionProvider, 0)
|
||||
for _, name := range r.order {
|
||||
if provider, exists := r.providers[name]; exists {
|
||||
@@ -161,7 +161,7 @@ func (r *Registry) GetConversionProviders() []ConversionProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -199,10 +199,10 @@ func (r *Registry) HealthCheckAll(ctx context.Context) []HealthCheck {
|
||||
providers = append(providers, r.providers[name])
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
|
||||
|
||||
results := make([]HealthCheck, len(providers))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
|
||||
for i, provider := range providers {
|
||||
wg.Add(1)
|
||||
go func(idx int, p Provider) {
|
||||
@@ -210,7 +210,7 @@ func (r *Registry) HealthCheckAll(ctx context.Context) []HealthCheck {
|
||||
results[idx] = p.HealthCheck(ctx)
|
||||
}(i, provider)
|
||||
}
|
||||
|
||||
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
@@ -219,16 +219,16 @@ func (r *Registry) HealthCheckAll(ctx context.Context) []HealthCheck {
|
||||
func (r *Registry) Close() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
|
||||
var lastErr error
|
||||
for _, provider := range r.providers {
|
||||
if err := provider.Close(); err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
r.providers = make(map[string]Provider)
|
||||
r.order = make([]string, 0)
|
||||
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
+37
-37
@@ -26,16 +26,16 @@ func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
// Get quotes from all providers in parallel
|
||||
type result struct {
|
||||
quote *SwapQuote
|
||||
err error
|
||||
}
|
||||
|
||||
|
||||
results := make(chan result, len(providers))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
|
||||
for _, provider := range providers {
|
||||
wg.Add(1)
|
||||
go func(p QuoteProvider) {
|
||||
@@ -44,16 +44,16 @@ func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote
|
||||
results <- result{quote: quote, err: err}
|
||||
}(provider)
|
||||
}
|
||||
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
|
||||
// Collect quotes
|
||||
var quotes []SwapQuote
|
||||
var lastErr error
|
||||
|
||||
|
||||
for res := range results {
|
||||
if res.err != nil {
|
||||
lastErr = res.err
|
||||
@@ -61,14 +61,14 @@ func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote
|
||||
}
|
||||
quotes = append(quotes, *res.quote)
|
||||
}
|
||||
|
||||
|
||||
if len(quotes) == 0 {
|
||||
if lastErr != nil {
|
||||
return nil, lastErr
|
||||
}
|
||||
return nil, ErrAllProvidersFailed
|
||||
}
|
||||
|
||||
|
||||
// Sort by output amount (descending) for EXACT_INPUT
|
||||
// or input amount (ascending) for EXACT_OUTPUT
|
||||
sort.Slice(quotes, func(i, j int) bool {
|
||||
@@ -77,7 +77,7 @@ func (r *Router) GetBestQuote(ctx context.Context, req QuoteRequest) (*SwapQuote
|
||||
}
|
||||
return quotes[i].TokenIn.Amount.Cmp(quotes[j].TokenIn.Amount) < 0
|
||||
})
|
||||
|
||||
|
||||
return "es[0], nil
|
||||
}
|
||||
|
||||
@@ -87,15 +87,15 @@ func (r *Router) GetAllQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuot
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
type result struct {
|
||||
quotes []SwapQuote
|
||||
err error
|
||||
}
|
||||
|
||||
|
||||
results := make(chan result, len(providers))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
|
||||
for _, provider := range providers {
|
||||
wg.Add(1)
|
||||
go func(p QuoteProvider) {
|
||||
@@ -104,12 +104,12 @@ func (r *Router) GetAllQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuot
|
||||
results <- result{quotes: quotes, err: err}
|
||||
}(provider)
|
||||
}
|
||||
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
|
||||
var allQuotes []SwapQuote
|
||||
for res := range results {
|
||||
if res.err != nil {
|
||||
@@ -117,11 +117,11 @@ func (r *Router) GetAllQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuot
|
||||
}
|
||||
allQuotes = append(allQuotes, res.quotes...)
|
||||
}
|
||||
|
||||
|
||||
if len(allQuotes) == 0 {
|
||||
return nil, ErrAllProvidersFailed
|
||||
}
|
||||
|
||||
|
||||
// Sort by output amount
|
||||
sort.Slice(allQuotes, func(i, j int) bool {
|
||||
if req.IsExactIn {
|
||||
@@ -129,7 +129,7 @@ func (r *Router) GetAllQuotes(ctx context.Context, req QuoteRequest) ([]SwapQuot
|
||||
}
|
||||
return allQuotes[i].TokenIn.Amount.Cmp(allQuotes[j].TokenIn.Amount) < 0
|
||||
})
|
||||
|
||||
|
||||
return allQuotes, nil
|
||||
}
|
||||
|
||||
@@ -139,18 +139,18 @@ func (r *Router) GetPools(ctx context.Context, req PoolsRequest) ([]Pool, error)
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
for _, provider := range providers {
|
||||
pools, err := provider.GetPools(ctx, req)
|
||||
if err == nil && len(pools) > 0 {
|
||||
return pools, nil
|
||||
}
|
||||
|
||||
|
||||
if !r.enableFallback {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, ErrAllProvidersFailed
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ func (r *Router) GetPool(ctx context.Context, chainID ChainID, address string) (
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
var lastErr error
|
||||
for _, provider := range providers {
|
||||
pool, err := provider.GetPool(ctx, chainID, address)
|
||||
@@ -168,12 +168,12 @@ func (r *Router) GetPool(ctx context.Context, chainID ChainID, address string) (
|
||||
return pool, nil
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
|
||||
if !r.enableFallback {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ func (r *Router) GetPositions(ctx context.Context, req PositionsRequest) ([]Posi
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
var lastErr error
|
||||
for _, provider := range providers {
|
||||
positions, err := provider.GetPositions(ctx, req)
|
||||
@@ -191,12 +191,12 @@ func (r *Router) GetPositions(ctx context.Context, req PositionsRequest) ([]Posi
|
||||
return positions, nil
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
|
||||
if !r.enableFallback {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func (r *Router) GetTokenPrice(ctx context.Context, token Token) (*TokenPrice, e
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
var lastErr error
|
||||
for _, provider := range providers {
|
||||
price, err := provider.GetTokenPrice(ctx, token)
|
||||
@@ -214,12 +214,12 @@ func (r *Router) GetTokenPrice(ctx context.Context, token Token) (*TokenPrice, e
|
||||
return price, nil
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
|
||||
if !r.enableFallback {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
@@ -229,19 +229,19 @@ func (r *Router) GetTokenPrices(ctx context.Context, tokens []Token) ([]TokenPri
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
// Try first provider
|
||||
for _, provider := range providers {
|
||||
prices, err := provider.GetTokenPrices(ctx, tokens)
|
||||
if err == nil && len(prices) > 0 {
|
||||
return prices, nil
|
||||
}
|
||||
|
||||
|
||||
if !r.enableFallback {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, ErrAllProvidersFailed
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ func (r *Router) CreateLead(ctx context.Context, lead ConversionLead) (*Conversi
|
||||
if len(providers) == 0 {
|
||||
return nil, ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
var lastErr error
|
||||
for _, provider := range providers {
|
||||
created, err := provider.CreateLead(ctx, lead)
|
||||
@@ -259,12 +259,12 @@ func (r *Router) CreateLead(ctx context.Context, lead ConversionLead) (*Conversi
|
||||
return created, nil
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
|
||||
if !r.enableFallback {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ func (r *Router) TrackEvent(ctx context.Context, event ConversionEvent) error {
|
||||
if len(providers) == 0 {
|
||||
return ErrNoProvidersAvailable
|
||||
}
|
||||
|
||||
|
||||
// Track to all providers (don't fail if one fails)
|
||||
var lastErr error
|
||||
for _, provider := range providers {
|
||||
@@ -282,7 +282,7 @@ func (r *Router) TrackEvent(ctx context.Context, event ConversionEvent) error {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
|
||||
+47
-47
@@ -41,7 +41,7 @@ func DefaultServerConfig() ServerConfig {
|
||||
// NewServer creates a new gateway server
|
||||
func NewServer(router *Router, cfg ServerConfig) *Server {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
|
||||
s := &Server{
|
||||
router: router,
|
||||
mux: mux,
|
||||
@@ -53,7 +53,7 @@ func NewServer(router *Router, cfg ServerConfig) *Server {
|
||||
MaxHeaderBytes: cfg.MaxHeaderBytes,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
s.registerRoutes()
|
||||
return s
|
||||
}
|
||||
@@ -73,25 +73,25 @@ func (s *Server) registerRoutes() {
|
||||
// Health and info
|
||||
s.mux.HandleFunc("/health", s.handleHealth)
|
||||
s.mux.HandleFunc("/providers", s.handleProviders)
|
||||
|
||||
|
||||
// Quote API
|
||||
s.mux.HandleFunc("/v1/quote", s.handleQuote)
|
||||
s.mux.HandleFunc("/v1/quotes", s.handleQuotes)
|
||||
s.mux.HandleFunc("/v1/swap", s.handleSwap)
|
||||
|
||||
|
||||
// Liquidity API
|
||||
s.mux.HandleFunc("/v1/pools", s.handlePools)
|
||||
s.mux.HandleFunc("/v1/pool/", s.handlePool)
|
||||
s.mux.HandleFunc("/v1/positions", s.handlePositions)
|
||||
|
||||
|
||||
// Price API
|
||||
s.mux.HandleFunc("/v1/price", s.handlePrice)
|
||||
s.mux.HandleFunc("/v1/prices", s.handlePrices)
|
||||
|
||||
|
||||
// Token API
|
||||
s.mux.HandleFunc("/v1/tokens", s.handleTokens)
|
||||
s.mux.HandleFunc("/v1/tokens/search", s.handleTokenSearch)
|
||||
|
||||
|
||||
// Conversion tracking API
|
||||
s.mux.HandleFunc("/v1/leads", s.handleLeads)
|
||||
s.mux.HandleFunc("/v1/events", s.handleEvents)
|
||||
@@ -139,10 +139,10 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
checks := s.router.HealthCheck(ctx)
|
||||
|
||||
|
||||
healthy := true
|
||||
for _, check := range checks {
|
||||
if !check.Healthy {
|
||||
@@ -150,12 +150,12 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
status := http.StatusOK
|
||||
if !healthy {
|
||||
status = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, status, map[string]interface{}{
|
||||
"status": map[bool]string{true: "healthy", false: "unhealthy"}[healthy],
|
||||
"providers": checks,
|
||||
@@ -167,7 +167,7 @@ func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
providers := s.router.ListProviders()
|
||||
s.writeJSON(w, http.StatusOK, providers)
|
||||
}
|
||||
@@ -188,20 +188,20 @@ func (s *Server) handleQuote(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var req quoteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
quote, err := s.router.GetBestQuote(ctx, s.convertQuoteRequest(req))
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusOK, quote)
|
||||
}
|
||||
|
||||
@@ -210,20 +210,20 @@ func (s *Server) handleQuotes(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var req quoteRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
quotes, err := s.router.GetAllQuotes(ctx, s.convertQuoteRequest(req))
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusOK, quotes)
|
||||
}
|
||||
|
||||
@@ -232,14 +232,14 @@ func (s *Server) handleSwap(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// TODO: Implement swap transaction building
|
||||
s.writeError(w, http.StatusNotImplemented, fmt.Errorf("swap building not yet implemented"))
|
||||
}
|
||||
|
||||
func (s *Server) convertQuoteRequest(req quoteRequest) QuoteRequest {
|
||||
amount := parseBigIntStr(req.Amount)
|
||||
|
||||
|
||||
return QuoteRequest{
|
||||
TokenIn: Token{
|
||||
Address: req.TokenIn,
|
||||
@@ -263,9 +263,9 @@ func (s *Server) handlePools(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
|
||||
|
||||
var req PoolsRequest
|
||||
if r.Method == http.MethodPost {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -277,7 +277,7 @@ func (s *Server) handlePools(w http.ResponseWriter, r *http.Request) {
|
||||
chainID, _ := strconv.ParseUint(r.URL.Query().Get("chainId"), 10, 64)
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
|
||||
|
||||
req = PoolsRequest{
|
||||
ChainID: ChainID(chainID),
|
||||
Token0: r.URL.Query().Get("token0"),
|
||||
@@ -287,13 +287,13 @@ func (s *Server) handlePools(w http.ResponseWriter, r *http.Request) {
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pools, err := s.router.GetPools(ctx, req)
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusOK, pools)
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ func (s *Server) handlePool(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Extract pool address from path: /v1/pool/{chainId}/{address}
|
||||
path := strings.TrimPrefix(r.URL.Path, "/v1/pool/")
|
||||
parts := strings.Split(path, "/")
|
||||
@@ -310,20 +310,20 @@ func (s *Server) handlePool(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid pool path"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
chainID, err := strconv.ParseUint(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid chain ID"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
pool, err := s.router.GetPool(ctx, ChainID(chainID), parts[1])
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusOK, pool)
|
||||
}
|
||||
|
||||
@@ -332,9 +332,9 @@ func (s *Server) handlePositions(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
|
||||
|
||||
var req PositionsRequest
|
||||
if r.Method == http.MethodPost {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -349,13 +349,13 @@ func (s *Server) handlePositions(w http.ResponseWriter, r *http.Request) {
|
||||
PoolID: r.URL.Query().Get("poolId"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
positions, err := s.router.GetPositions(ctx, req)
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusOK, positions)
|
||||
}
|
||||
|
||||
@@ -366,15 +366,15 @@ func (s *Server) handlePrice(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
chainID, _ := strconv.ParseUint(r.URL.Query().Get("chainId"), 10, 64)
|
||||
address := r.URL.Query().Get("address")
|
||||
|
||||
|
||||
if address == "" {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("address is required"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
price, err := s.router.GetTokenPrice(ctx, Token{
|
||||
Address: address,
|
||||
@@ -384,7 +384,7 @@ func (s *Server) handlePrice(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusOK, price)
|
||||
}
|
||||
|
||||
@@ -393,20 +393,20 @@ func (s *Server) handlePrices(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var tokens []Token
|
||||
if err := json.NewDecoder(r.Body).Decode(&tokens); err != nil {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
prices, err := s.router.GetTokenPrices(ctx, tokens)
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusOK, prices)
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ func (s *Server) handleTokenSearch(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) handleLeads(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := s.requestContext(r)
|
||||
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
var lead ConversionLead
|
||||
@@ -451,15 +451,15 @@ func (s *Server) handleLeads(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
created, err := s.router.CreateLead(ctx, lead)
|
||||
if err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusCreated, created)
|
||||
|
||||
|
||||
default:
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
}
|
||||
@@ -470,19 +470,19 @@ func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) {
|
||||
s.writeError(w, http.StatusMethodNotAllowed, fmt.Errorf("method not allowed"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var event ConversionEvent
|
||||
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
|
||||
s.writeError(w, http.StatusBadRequest, fmt.Errorf("invalid request body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
ctx := s.requestContext(r)
|
||||
if err := s.router.TrackEvent(ctx, event); err != nil {
|
||||
s.writeError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
s.writeJSON(w, http.StatusAccepted, map[string]string{"status": "accepted"})
|
||||
}
|
||||
|
||||
|
||||
+39
-39
@@ -83,17 +83,17 @@ type SwapTransaction struct {
|
||||
|
||||
// Pool represents a liquidity pool
|
||||
type Pool struct {
|
||||
Address string `json:"address"`
|
||||
ChainID ChainID `json:"chainId"`
|
||||
Protocol string `json:"protocol"` // "uniswap_v2", "uniswap_v3", "lux_amm"
|
||||
Token0 Token `json:"token0"`
|
||||
Token1 Token `json:"token1"`
|
||||
Fee int `json:"fee,omitempty"`
|
||||
TVL *big.Int `json:"tvl,omitempty"`
|
||||
Volume24h *big.Int `json:"volume24h,omitempty"`
|
||||
APR float64 `json:"apr,omitempty"`
|
||||
TickData []TickRange `json:"tickData,omitempty"` // For V3/V4 pools
|
||||
Reserves *Reserves `json:"reserves,omitempty"` // For V2 pools
|
||||
Address string `json:"address"`
|
||||
ChainID ChainID `json:"chainId"`
|
||||
Protocol string `json:"protocol"` // "uniswap_v2", "uniswap_v3", "lux_amm"
|
||||
Token0 Token `json:"token0"`
|
||||
Token1 Token `json:"token1"`
|
||||
Fee int `json:"fee,omitempty"`
|
||||
TVL *big.Int `json:"tvl,omitempty"`
|
||||
Volume24h *big.Int `json:"volume24h,omitempty"`
|
||||
APR float64 `json:"apr,omitempty"`
|
||||
TickData []TickRange `json:"tickData,omitempty"` // For V3/V4 pools
|
||||
Reserves *Reserves `json:"reserves,omitempty"` // For V2 pools
|
||||
}
|
||||
|
||||
// Reserves represents V2 pool reserves
|
||||
@@ -111,15 +111,15 @@ type TickRange struct {
|
||||
|
||||
// Position represents a liquidity position
|
||||
type Position struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Pool Pool `json:"pool"`
|
||||
Liquidity *big.Int `json:"liquidity"`
|
||||
Token0Owed *big.Int `json:"token0Owed"`
|
||||
Token1Owed *big.Int `json:"token1Owed"`
|
||||
TickLower int `json:"tickLower,omitempty"`
|
||||
TickUpper int `json:"tickUpper,omitempty"`
|
||||
FeesEarned *FeesEarned `json:"feesEarned,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Pool Pool `json:"pool"`
|
||||
Liquidity *big.Int `json:"liquidity"`
|
||||
Token0Owed *big.Int `json:"token0Owed"`
|
||||
Token1Owed *big.Int `json:"token1Owed"`
|
||||
TickLower int `json:"tickLower,omitempty"`
|
||||
TickUpper int `json:"tickUpper,omitempty"`
|
||||
FeesEarned *FeesEarned `json:"feesEarned,omitempty"`
|
||||
}
|
||||
|
||||
// FeesEarned represents fees earned from a position
|
||||
@@ -130,13 +130,13 @@ type FeesEarned struct {
|
||||
|
||||
// ConversionLead represents a conversion tracking lead
|
||||
type ConversionLead struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Medium string `json:"medium,omitempty"`
|
||||
Campaign string `json:"campaign,omitempty"`
|
||||
WalletAddr string `json:"walletAddr,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Medium string `json:"medium,omitempty"`
|
||||
Campaign string `json:"campaign,omitempty"`
|
||||
WalletAddr string `json:"walletAddr,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ConversionEvent represents a conversion event
|
||||
@@ -162,12 +162,12 @@ type TokenPrice struct {
|
||||
|
||||
// ProviderInfo contains information about a provider
|
||||
type ProviderInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
SupportedChains []ChainID `json:"supportedChains"`
|
||||
Priority int `json:"priority"` // Lower = higher priority
|
||||
Healthy bool `json:"healthy"`
|
||||
Priority int `json:"priority"` // Lower = higher priority
|
||||
Healthy bool `json:"healthy"`
|
||||
}
|
||||
|
||||
// HealthCheck represents health check result
|
||||
@@ -191,13 +191,13 @@ type QuoteRequest struct {
|
||||
|
||||
// PoolsRequest for querying pools
|
||||
type PoolsRequest struct {
|
||||
ChainID ChainID `json:"chainId"`
|
||||
Token0 string `json:"token0,omitempty"`
|
||||
Token1 string `json:"token1,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
MinTVL *big.Int `json:"minTvl,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
ChainID ChainID `json:"chainId"`
|
||||
Token0 string `json:"token0,omitempty"`
|
||||
Token1 string `json:"token1,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
MinTVL *big.Int `json:"minTvl,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
}
|
||||
|
||||
// PositionsRequest for querying positions
|
||||
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
type APIEndpoints struct {
|
||||
// Core API for quotes, tokens, and general data
|
||||
CoreAPI string
|
||||
|
||||
|
||||
// Liquidity backend for pool and position data
|
||||
LiquidityAPI string
|
||||
|
||||
|
||||
// Conversion tracking/entry gateway
|
||||
ConversionAPI string
|
||||
}
|
||||
@@ -44,10 +44,10 @@ type Client struct {
|
||||
|
||||
// ClientConfig holds client configuration
|
||||
type ClientConfig struct {
|
||||
Endpoints APIEndpoints
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
UserAgent string
|
||||
Endpoints APIEndpoints
|
||||
APIKey string
|
||||
Timeout time.Duration
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// NewClient creates a new Uniswap API client
|
||||
@@ -61,7 +61,7 @@ func NewClient(cfg ClientConfig) *Client {
|
||||
if cfg.UserAgent == "" {
|
||||
cfg.UserAgent = "Lux-DEX-Gateway/1.0"
|
||||
}
|
||||
|
||||
|
||||
return &Client{
|
||||
endpoints: cfg.Endpoints,
|
||||
httpClient: &http.Client{
|
||||
@@ -82,42 +82,42 @@ func (c *Client) request(ctx context.Context, method, url string, body interface
|
||||
}
|
||||
bodyReader = bytes.NewReader(data)
|
||||
}
|
||||
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
if c.apiKey != "" {
|
||||
req.Header.Set("X-API-Key", c.apiKey)
|
||||
}
|
||||
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Message: string(respBody),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if result != nil {
|
||||
if err := json.Unmarshal(respBody, result); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -139,17 +139,17 @@ func (c *Client) HealthCheck(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("User-Agent", c.userAgent)
|
||||
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("health check failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -157,37 +157,37 @@ func (c *Client) HealthCheck(ctx context.Context) error {
|
||||
|
||||
// QuoteRequest for the Uniswap API
|
||||
type QuoteAPIRequest struct {
|
||||
TokenIn string `json:"tokenIn"`
|
||||
TokenInChainID uint64 `json:"tokenInChainId"`
|
||||
TokenOut string `json:"tokenOut"`
|
||||
TokenOutChainID uint64 `json:"tokenOutChainId"`
|
||||
Amount string `json:"amount"`
|
||||
Type string `json:"type"` // "EXACT_INPUT" or "EXACT_OUTPUT"
|
||||
Slippage int `json:"slippageTolerance,omitempty"` // in bps
|
||||
Protocols string `json:"protocols,omitempty"`
|
||||
Recipient string `json:"recipient,omitempty"`
|
||||
TokenIn string `json:"tokenIn"`
|
||||
TokenInChainID uint64 `json:"tokenInChainId"`
|
||||
TokenOut string `json:"tokenOut"`
|
||||
TokenOutChainID uint64 `json:"tokenOutChainId"`
|
||||
Amount string `json:"amount"`
|
||||
Type string `json:"type"` // "EXACT_INPUT" or "EXACT_OUTPUT"
|
||||
Slippage int `json:"slippageTolerance,omitempty"` // in bps
|
||||
Protocols string `json:"protocols,omitempty"`
|
||||
Recipient string `json:"recipient,omitempty"`
|
||||
}
|
||||
|
||||
// QuoteAPIResponse from the Uniswap API
|
||||
type QuoteAPIResponse struct {
|
||||
Quote string `json:"quote"`
|
||||
QuoteGasAdjusted string `json:"quoteGasAdjusted"`
|
||||
GasUseEstimate string `json:"gasUseEstimate"`
|
||||
GasUseEstimateQuote string `json:"gasUseEstimateQuote"`
|
||||
Route []RouteAPIData `json:"route"`
|
||||
PriceImpact string `json:"priceImpact,omitempty"`
|
||||
MethodParameters *MethodParams `json:"methodParameters,omitempty"`
|
||||
Quote string `json:"quote"`
|
||||
QuoteGasAdjusted string `json:"quoteGasAdjusted"`
|
||||
GasUseEstimate string `json:"gasUseEstimate"`
|
||||
GasUseEstimateQuote string `json:"gasUseEstimateQuote"`
|
||||
Route []RouteAPIData `json:"route"`
|
||||
PriceImpact string `json:"priceImpact,omitempty"`
|
||||
MethodParameters *MethodParams `json:"methodParameters,omitempty"`
|
||||
}
|
||||
|
||||
// RouteAPIData represents a route from the API
|
||||
type RouteAPIData struct {
|
||||
Type string `json:"type"`
|
||||
Address string `json:"address"`
|
||||
TokenIn TokenAPIData `json:"tokenIn"`
|
||||
TokenOut TokenAPIData `json:"tokenOut"`
|
||||
Fee string `json:"fee,omitempty"`
|
||||
AmountIn string `json:"amountIn,omitempty"`
|
||||
AmountOut string `json:"amountOut,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Address string `json:"address"`
|
||||
TokenIn TokenAPIData `json:"tokenIn"`
|
||||
TokenOut TokenAPIData `json:"tokenOut"`
|
||||
Fee string `json:"fee,omitempty"`
|
||||
AmountIn string `json:"amountIn,omitempty"`
|
||||
AmountOut string `json:"amountOut,omitempty"`
|
||||
}
|
||||
|
||||
// TokenAPIData represents token data from the API
|
||||
@@ -209,12 +209,12 @@ type MethodParams struct {
|
||||
// GetQuote gets a swap quote from the Core API
|
||||
func (c *Client) GetQuote(ctx context.Context, req QuoteAPIRequest) (*QuoteAPIResponse, error) {
|
||||
url := fmt.Sprintf("%s/v2/quote", c.endpoints.CoreAPI)
|
||||
|
||||
|
||||
var resp QuoteAPIResponse
|
||||
if err := c.request(ctx, "POST", url, req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@@ -222,40 +222,40 @@ func (c *Client) GetQuote(ctx context.Context, req QuoteAPIRequest) (*QuoteAPIRe
|
||||
|
||||
// PoolsAPIRequest for querying pools
|
||||
type PoolsAPIRequest struct {
|
||||
ChainID uint64 `json:"chainId"`
|
||||
TokenA string `json:"tokenA,omitempty"`
|
||||
TokenB string `json:"tokenB,omitempty"`
|
||||
PoolType string `json:"poolType,omitempty"`
|
||||
MinTVL float64 `json:"minTvl,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
ChainID uint64 `json:"chainId"`
|
||||
TokenA string `json:"tokenA,omitempty"`
|
||||
TokenB string `json:"tokenB,omitempty"`
|
||||
PoolType string `json:"poolType,omitempty"`
|
||||
MinTVL float64 `json:"minTvl,omitempty"`
|
||||
Limit int `json:"limit,omitempty"`
|
||||
Offset int `json:"offset,omitempty"`
|
||||
}
|
||||
|
||||
// PoolAPIData represents pool data from the API
|
||||
type PoolAPIData struct {
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
ChainID uint64 `json:"chainId"`
|
||||
Protocol string `json:"protocol"`
|
||||
Token0 TokenAPIData `json:"token0"`
|
||||
Token1 TokenAPIData `json:"token1"`
|
||||
FeeTier int `json:"feeTier,omitempty"`
|
||||
TVL string `json:"tvl"`
|
||||
Volume24h string `json:"volume24h"`
|
||||
APR float64 `json:"apr,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Address string `json:"address"`
|
||||
ChainID uint64 `json:"chainId"`
|
||||
Protocol string `json:"protocol"`
|
||||
Token0 TokenAPIData `json:"token0"`
|
||||
Token1 TokenAPIData `json:"token1"`
|
||||
FeeTier int `json:"feeTier,omitempty"`
|
||||
TVL string `json:"tvl"`
|
||||
Volume24h string `json:"volume24h"`
|
||||
APR float64 `json:"apr,omitempty"`
|
||||
}
|
||||
|
||||
// GetPools queries pools from the Liquidity API
|
||||
func (c *Client) GetPools(ctx context.Context, req PoolsAPIRequest) ([]PoolAPIData, error) {
|
||||
url := fmt.Sprintf("%s/v1/pools", c.endpoints.LiquidityAPI)
|
||||
|
||||
|
||||
var resp struct {
|
||||
Pools []PoolAPIData `json:"pools"`
|
||||
}
|
||||
if err := c.request(ctx, "POST", url, req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return resp.Pools, nil
|
||||
}
|
||||
|
||||
@@ -268,29 +268,29 @@ type PositionsAPIRequest struct {
|
||||
|
||||
// PositionAPIData represents position data from the API
|
||||
type PositionAPIData struct {
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Pool PoolAPIData `json:"pool"`
|
||||
Liquidity string `json:"liquidity"`
|
||||
Token0Owed string `json:"token0Owed"`
|
||||
Token1Owed string `json:"token1Owed"`
|
||||
TickLower int `json:"tickLower,omitempty"`
|
||||
TickUpper int `json:"tickUpper,omitempty"`
|
||||
Fees0 string `json:"fees0,omitempty"`
|
||||
Fees1 string `json:"fees1,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Pool PoolAPIData `json:"pool"`
|
||||
Liquidity string `json:"liquidity"`
|
||||
Token0Owed string `json:"token0Owed"`
|
||||
Token1Owed string `json:"token1Owed"`
|
||||
TickLower int `json:"tickLower,omitempty"`
|
||||
TickUpper int `json:"tickUpper,omitempty"`
|
||||
Fees0 string `json:"fees0,omitempty"`
|
||||
Fees1 string `json:"fees1,omitempty"`
|
||||
}
|
||||
|
||||
// GetPositions queries positions from the Liquidity API
|
||||
func (c *Client) GetPositions(ctx context.Context, req PositionsAPIRequest) ([]PositionAPIData, error) {
|
||||
url := fmt.Sprintf("%s/v1/positions", c.endpoints.LiquidityAPI)
|
||||
|
||||
|
||||
var resp struct {
|
||||
Positions []PositionAPIData `json:"positions"`
|
||||
}
|
||||
if err := c.request(ctx, "POST", url, req, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return resp.Positions, nil
|
||||
}
|
||||
|
||||
@@ -310,12 +310,12 @@ type LeadAPIData struct {
|
||||
// CreateLead creates a conversion lead
|
||||
func (c *Client) CreateLead(ctx context.Context, lead LeadAPIData) (*LeadAPIData, error) {
|
||||
url := fmt.Sprintf("%s/v1/leads", c.endpoints.ConversionAPI)
|
||||
|
||||
|
||||
var resp LeadAPIData
|
||||
if err := c.request(ctx, "POST", url, lead, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@@ -339,14 +339,14 @@ func (c *Client) TrackEvent(ctx context.Context, event EventAPIData) error {
|
||||
// GetLeadEvents gets events for a lead
|
||||
func (c *Client) GetLeadEvents(ctx context.Context, leadID string) ([]EventAPIData, error) {
|
||||
url := fmt.Sprintf("%s/v1/leads/%s/events", c.endpoints.ConversionAPI, leadID)
|
||||
|
||||
|
||||
var resp struct {
|
||||
Events []EventAPIData `json:"events"`
|
||||
}
|
||||
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return resp.Events, nil
|
||||
}
|
||||
|
||||
@@ -355,40 +355,40 @@ func (c *Client) GetLeadEvents(ctx context.Context, leadID string) ([]EventAPIDa
|
||||
// GetTokenList gets the token list for a chain
|
||||
func (c *Client) GetTokenList(ctx context.Context, chainID uint64) ([]TokenAPIData, error) {
|
||||
url := fmt.Sprintf("%s/v1/tokens?chainId=%d", c.endpoints.CoreAPI, chainID)
|
||||
|
||||
|
||||
var resp struct {
|
||||
Tokens []TokenAPIData `json:"tokens"`
|
||||
}
|
||||
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return resp.Tokens, nil
|
||||
}
|
||||
|
||||
// SearchTokens searches for tokens
|
||||
func (c *Client) SearchTokens(ctx context.Context, chainID uint64, query string) ([]TokenAPIData, error) {
|
||||
url := fmt.Sprintf("%s/v1/tokens/search?chainId=%d&query=%s", c.endpoints.CoreAPI, chainID, query)
|
||||
|
||||
|
||||
var resp struct {
|
||||
Tokens []TokenAPIData `json:"tokens"`
|
||||
}
|
||||
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return resp.Tokens, nil
|
||||
}
|
||||
|
||||
// GetTokenPrice gets token price
|
||||
func (c *Client) GetTokenPrice(ctx context.Context, chainID uint64, address string) (*TokenPriceAPIData, error) {
|
||||
url := fmt.Sprintf("%s/v1/price?chainId=%d&address=%s", c.endpoints.CoreAPI, chainID, address)
|
||||
|
||||
|
||||
var resp TokenPriceAPIData
|
||||
if err := c.request(ctx, "GET", url, nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,19 +5,19 @@ import "errors"
|
||||
var (
|
||||
// ErrNoMethodParameters indicates quote response missing method parameters
|
||||
ErrNoMethodParameters = errors.New("quote response missing method parameters")
|
||||
|
||||
|
||||
// ErrExecuteNotSupported indicates server-side swap execution is not supported
|
||||
ErrExecuteNotSupported = errors.New("server-side swap execution not supported")
|
||||
|
||||
|
||||
// ErrPoolNotFound indicates the requested pool was not found
|
||||
ErrPoolNotFound = errors.New("pool not found")
|
||||
|
||||
|
||||
// ErrPositionNotFound indicates the requested position was not found
|
||||
ErrPositionNotFound = errors.New("position not found")
|
||||
|
||||
|
||||
// ErrTokenNotFound indicates the requested token was not found
|
||||
ErrTokenNotFound = errors.New("token not found")
|
||||
|
||||
|
||||
// ErrNotImplemented indicates the feature is not implemented
|
||||
ErrNotImplemented = errors.New("feature not implemented in Uniswap provider")
|
||||
)
|
||||
|
||||
@@ -55,13 +55,13 @@ func NewProvider(cfg ProviderConfig) *Provider {
|
||||
if len(cfg.Chains) == 0 {
|
||||
cfg.Chains = DefaultConfig().Chains
|
||||
}
|
||||
|
||||
|
||||
client := NewClient(ClientConfig{
|
||||
Endpoints: cfg.Endpoints,
|
||||
APIKey: cfg.APIKey,
|
||||
Timeout: cfg.Timeout,
|
||||
})
|
||||
|
||||
|
||||
return &Provider{
|
||||
client: client,
|
||||
name: cfg.Name,
|
||||
@@ -87,18 +87,18 @@ func (p *Provider) HealthCheck(ctx context.Context) gateway.HealthCheck {
|
||||
start := time.Now()
|
||||
err := p.client.HealthCheck(ctx)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
|
||||
|
||||
result := gateway.HealthCheck{
|
||||
Provider: p.name,
|
||||
Healthy: err == nil,
|
||||
Latency: latency,
|
||||
LastCheck: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ func (p *Provider) GetQuote(ctx context.Context, req gateway.QuoteRequest) (*gat
|
||||
if !req.IsExactIn {
|
||||
quoteType = "EXACT_OUTPUT"
|
||||
}
|
||||
|
||||
|
||||
apiReq := QuoteAPIRequest{
|
||||
TokenIn: req.TokenIn.Address,
|
||||
TokenInChainID: uint64(req.TokenIn.ChainID),
|
||||
@@ -122,16 +122,16 @@ func (p *Provider) GetQuote(ctx context.Context, req gateway.QuoteRequest) (*gat
|
||||
Amount: req.Amount.String(),
|
||||
Type: quoteType,
|
||||
}
|
||||
|
||||
|
||||
if req.Slippage > 0 {
|
||||
apiReq.Slippage = int(req.Slippage * 10000) // Convert to bps
|
||||
}
|
||||
|
||||
|
||||
apiResp, err := p.client.GetQuote(ctx, apiReq)
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
return p.convertQuoteResponse(req, apiResp), nil
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ func (p *Provider) GetQuotes(ctx context.Context, req gateway.QuoteRequest) ([]g
|
||||
// BuildSwap builds a swap transaction
|
||||
func (p *Provider) BuildSwap(ctx context.Context, quote gateway.SwapQuote, recipient string, deadline int64) (*gateway.SwapTransaction, error) {
|
||||
quoteType := "EXACT_INPUT"
|
||||
|
||||
|
||||
apiReq := QuoteAPIRequest{
|
||||
TokenIn: quote.TokenIn.Token.Address,
|
||||
TokenInChainID: uint64(quote.TokenIn.Token.ChainID),
|
||||
@@ -157,24 +157,24 @@ func (p *Provider) BuildSwap(ctx context.Context, quote gateway.SwapQuote, recip
|
||||
Type: quoteType,
|
||||
Recipient: recipient,
|
||||
}
|
||||
|
||||
|
||||
apiResp, err := p.client.GetQuote(ctx, apiReq)
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
if apiResp.MethodParameters == nil {
|
||||
return nil, &gateway.ProviderError{
|
||||
Provider: p.name,
|
||||
Err: ErrNoMethodParameters,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
value := new(big.Int)
|
||||
if apiResp.MethodParameters.Value != "" {
|
||||
value.SetString(apiResp.MethodParameters.Value, 10)
|
||||
}
|
||||
|
||||
|
||||
gasLimit := new(big.Int)
|
||||
if apiResp.GasUseEstimate != "" {
|
||||
gasLimit.SetString(apiResp.GasUseEstimate, 10)
|
||||
@@ -182,7 +182,7 @@ func (p *Provider) BuildSwap(ctx context.Context, quote gateway.SwapQuote, recip
|
||||
gasLimit.Mul(gasLimit, big.NewInt(120))
|
||||
gasLimit.Div(gasLimit, big.NewInt(100))
|
||||
}
|
||||
|
||||
|
||||
return &gateway.SwapTransaction{
|
||||
To: apiResp.MethodParameters.To,
|
||||
Data: apiResp.MethodParameters.Calldata,
|
||||
@@ -209,22 +209,22 @@ func (p *Provider) GetPools(ctx context.Context, req gateway.PoolsRequest) ([]ga
|
||||
Limit: req.Limit,
|
||||
Offset: req.Offset,
|
||||
}
|
||||
|
||||
|
||||
if req.MinTVL != nil {
|
||||
tvl, _ := req.MinTVL.Float64()
|
||||
apiReq.MinTVL = tvl
|
||||
}
|
||||
|
||||
|
||||
apiPools, err := p.client.GetPools(ctx, apiReq)
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
pools := make([]gateway.Pool, len(apiPools))
|
||||
for i, ap := range apiPools {
|
||||
pools[i] = p.convertPoolData(ap)
|
||||
}
|
||||
|
||||
|
||||
return pools, nil
|
||||
}
|
||||
|
||||
@@ -238,11 +238,11 @@ func (p *Provider) GetPool(ctx context.Context, chainID gateway.ChainID, address
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
if len(pools) == 0 {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrPoolNotFound}
|
||||
}
|
||||
|
||||
|
||||
return &pools[0], nil
|
||||
}
|
||||
|
||||
@@ -253,17 +253,17 @@ func (p *Provider) GetPositions(ctx context.Context, req gateway.PositionsReques
|
||||
Owner: req.Owner,
|
||||
PoolID: req.PoolID,
|
||||
}
|
||||
|
||||
|
||||
apiPositions, err := p.client.GetPositions(ctx, apiReq)
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
positions := make([]gateway.Position, len(apiPositions))
|
||||
for i, ap := range apiPositions {
|
||||
positions[i] = p.convertPositionData(ap)
|
||||
}
|
||||
|
||||
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
@@ -276,13 +276,13 @@ func (p *Provider) GetPosition(ctx context.Context, chainID gateway.ChainID, pos
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
for _, pos := range positions {
|
||||
if pos.ID == positionID {
|
||||
return &pos, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrPositionNotFound}
|
||||
}
|
||||
|
||||
@@ -307,7 +307,7 @@ func (p *Provider) GetTokenPrice(ctx context.Context, token gateway.Token) (*gat
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
return &gateway.TokenPrice{
|
||||
Token: token,
|
||||
PriceUSD: apiPrice.PriceUSD,
|
||||
@@ -340,12 +340,12 @@ func (p *Provider) CreateLead(ctx context.Context, lead gateway.ConversionLead)
|
||||
WalletAddr: lead.WalletAddr,
|
||||
Metadata: lead.Metadata,
|
||||
}
|
||||
|
||||
|
||||
created, err := p.client.CreateLead(ctx, apiLead)
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
return p.convertLeadData(*created), nil
|
||||
}
|
||||
|
||||
@@ -365,15 +365,15 @@ func (p *Provider) TrackEvent(ctx context.Context, event gateway.ConversionEvent
|
||||
Timestamp: event.Timestamp.Format(time.RFC3339),
|
||||
Metadata: event.Metadata,
|
||||
}
|
||||
|
||||
|
||||
if event.Value != nil {
|
||||
apiEvent.Value = event.Value.String()
|
||||
}
|
||||
|
||||
|
||||
if err := p.client.TrackEvent(ctx, apiEvent); err != nil {
|
||||
return &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -383,12 +383,12 @@ func (p *Provider) GetLeadEvents(ctx context.Context, leadID string) ([]gateway.
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
events := make([]gateway.ConversionEvent, len(apiEvents))
|
||||
for i, ae := range apiEvents {
|
||||
events[i] = p.convertEventData(ae)
|
||||
}
|
||||
|
||||
|
||||
return events, nil
|
||||
}
|
||||
|
||||
@@ -398,12 +398,12 @@ func (p *Provider) GetTokenList(ctx context.Context, chainID gateway.ChainID) ([
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
tokens := make([]gateway.Token, len(apiTokens))
|
||||
for i, at := range apiTokens {
|
||||
tokens[i] = p.convertTokenData(at)
|
||||
}
|
||||
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
@@ -413,12 +413,12 @@ func (p *Provider) SearchTokens(ctx context.Context, chainID gateway.ChainID, qu
|
||||
if err != nil {
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: err}
|
||||
}
|
||||
|
||||
|
||||
tokens := make([]gateway.Token, len(apiTokens))
|
||||
for i, at := range apiTokens {
|
||||
tokens[i] = p.convertTokenData(at)
|
||||
}
|
||||
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
@@ -428,13 +428,13 @@ func (p *Provider) GetToken(ctx context.Context, chainID gateway.ChainID, addres
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
for _, token := range tokens {
|
||||
if token.Address == address {
|
||||
return &token, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil, &gateway.ProviderError{Provider: p.name, Err: ErrTokenNotFound}
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ func (p *Provider) GetToken(ctx context.Context, chainID gateway.ChainID, addres
|
||||
func (p *Provider) convertQuoteResponse(req gateway.QuoteRequest, resp *QuoteAPIResponse) *gateway.SwapQuote {
|
||||
amountOut, _ := new(big.Int).SetString(resp.Quote, 10)
|
||||
gasEstimate, _ := new(big.Int).SetString(resp.GasUseEstimate, 10)
|
||||
|
||||
|
||||
route := make([]gateway.PoolHop, len(resp.Route))
|
||||
for i, r := range resp.Route {
|
||||
fee := 0
|
||||
@@ -458,12 +458,12 @@ func (p *Provider) convertQuoteResponse(req gateway.QuoteRequest, resp *QuoteAPI
|
||||
Fee: fee,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
priceImpact := 0.0
|
||||
if resp.PriceImpact != "" {
|
||||
priceImpact, _ = strconv.ParseFloat(resp.PriceImpact, 64)
|
||||
}
|
||||
|
||||
|
||||
return &gateway.SwapQuote{
|
||||
TokenIn: gateway.TokenAmount{
|
||||
Token: req.TokenIn,
|
||||
@@ -483,7 +483,7 @@ func (p *Provider) convertQuoteResponse(req gateway.QuoteRequest, resp *QuoteAPI
|
||||
func (p *Provider) convertPoolData(ap PoolAPIData) gateway.Pool {
|
||||
tvl, _ := new(big.Int).SetString(ap.TVL, 10)
|
||||
volume, _ := new(big.Int).SetString(ap.Volume24h, 10)
|
||||
|
||||
|
||||
return gateway.Pool{
|
||||
Address: ap.Address,
|
||||
ChainID: gateway.ChainID(ap.ChainID),
|
||||
@@ -501,7 +501,7 @@ func (p *Provider) convertPositionData(ap PositionAPIData) gateway.Position {
|
||||
liquidity, _ := new(big.Int).SetString(ap.Liquidity, 10)
|
||||
token0Owed, _ := new(big.Int).SetString(ap.Token0Owed, 10)
|
||||
token1Owed, _ := new(big.Int).SetString(ap.Token1Owed, 10)
|
||||
|
||||
|
||||
var feesEarned *gateway.FeesEarned
|
||||
if ap.Fees0 != "" || ap.Fees1 != "" {
|
||||
fees0, _ := new(big.Int).SetString(ap.Fees0, 10)
|
||||
@@ -511,7 +511,7 @@ func (p *Provider) convertPositionData(ap PositionAPIData) gateway.Position {
|
||||
Token1Fees: fees1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return gateway.Position{
|
||||
ID: ap.ID,
|
||||
Owner: ap.Owner,
|
||||
@@ -551,7 +551,7 @@ func (p *Provider) convertLeadData(al LeadAPIData) *gateway.ConversionLead {
|
||||
func (p *Provider) convertEventData(ae EventAPIData) gateway.ConversionEvent {
|
||||
ts, _ := time.Parse(time.RFC3339, ae.Timestamp)
|
||||
value, _ := new(big.Int).SetString(ae.Value, 10)
|
||||
|
||||
|
||||
return gateway.ConversionEvent{
|
||||
LeadID: ae.LeadID,
|
||||
EventType: ae.EventType,
|
||||
|
||||
@@ -1164,8 +1164,8 @@ func TestCheckDailyLimit(t *testing.T) {
|
||||
t.Run("ExceedsDailyLimit", func(t *testing.T) {
|
||||
asset := &BridgeAsset{
|
||||
Symbol: "USDC",
|
||||
DailyLimit: big.NewInt(100000000000), // 100k USDC
|
||||
DailyVolume: big.NewInt(99000000000), // 99k already used
|
||||
DailyLimit: big.NewInt(100000000000), // 100k USDC
|
||||
DailyVolume: big.NewInt(99000000000), // 99k already used
|
||||
LastReset: time.Now(),
|
||||
}
|
||||
|
||||
@@ -1193,8 +1193,8 @@ func TestCheckDailyLimit(t *testing.T) {
|
||||
t.Run("DailyVolumeReset", func(t *testing.T) {
|
||||
asset := &BridgeAsset{
|
||||
Symbol: "USDC",
|
||||
DailyLimit: big.NewInt(100000000000), // 100k USDC
|
||||
DailyVolume: big.NewInt(100000000000), // At limit
|
||||
DailyLimit: big.NewInt(100000000000), // 100k USDC
|
||||
DailyVolume: big.NewInt(100000000000), // At limit
|
||||
LastReset: time.Now().Add(-25 * time.Hour), // More than 24h ago
|
||||
}
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ func TestLendingPool_Withdraw(t *testing.T) {
|
||||
// Set supply rate and simulate time passing
|
||||
supplier := lp.Suppliers["user1"]
|
||||
position := supplier.SuppliedAssets["ETH"]
|
||||
position.SupplyRate = 0.05 // 5% annual
|
||||
position.SupplyRate = 0.05 // 5% annual
|
||||
position.LastUpdate = time.Now().Add(-24 * time.Hour) // 1 day ago
|
||||
|
||||
err := lp.Withdraw("user1", "ETH", big.NewInt(500000))
|
||||
@@ -268,8 +268,8 @@ func TestLendingPool_calculateSupplyInterest(t *testing.T) {
|
||||
pool := lp.Pools["ETH"]
|
||||
position := &SupplyPosition{
|
||||
Asset: "ETH",
|
||||
Amount: big.NewInt(1000000000), // 1B units
|
||||
SupplyRate: 0.10, // 10% annual
|
||||
Amount: big.NewInt(1000000000), // 1B units
|
||||
SupplyRate: 0.10, // 10% annual
|
||||
LastUpdate: time.Now().Add(-8760 * time.Hour), // 1 year
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ func TestModifyLeverage_Comprehensive(t *testing.T) {
|
||||
account, _ := engine.CreateMarginAccount("user1", CrossMargin)
|
||||
|
||||
// Initialize account with sufficient margin
|
||||
account.Balance = big.NewInt(100000000) // 100M units
|
||||
account.FreeMargin = big.NewInt(100000000) // 100M units free
|
||||
account.Balance = big.NewInt(100000000) // 100M units
|
||||
account.FreeMargin = big.NewInt(100000000) // 100M units free
|
||||
account.MarginUsed = big.NewInt(0)
|
||||
|
||||
// Create position directly (bypassing OpenPosition which needs lending pool)
|
||||
@@ -179,7 +179,7 @@ func TestModifyLeverage_Comprehensive(t *testing.T) {
|
||||
engine, _, position := setupEngineWithPosition(10.0)
|
||||
|
||||
// Verify initial margin is correct: positionValue / leverage = 5000 / 10 = 500
|
||||
positionValue := position.EntryPrice * position.Size // 50000 * 0.1 = 5000
|
||||
positionValue := position.EntryPrice * position.Size // 50000 * 0.1 = 5000
|
||||
expectedMargin := int64(positionValue / position.Leverage) // 5000 / 10 = 500
|
||||
assert.Equal(t, expectedMargin, position.Margin.Int64())
|
||||
|
||||
@@ -391,34 +391,34 @@ func TestModifyLeverage_Comprehensive(t *testing.T) {
|
||||
|
||||
// Create two positions
|
||||
pos1 := &MarginPosition{
|
||||
ID: "pos1",
|
||||
Symbol: "BTC-USDT",
|
||||
Side: Buy,
|
||||
Size: 0.1,
|
||||
EntryPrice: 50000.0,
|
||||
Leverage: 10.0,
|
||||
Margin: big.NewInt(500),
|
||||
UnrealizedPnL: big.NewInt(0),
|
||||
RealizedPnL: big.NewInt(0),
|
||||
Fees: big.NewInt(0),
|
||||
OpenTime: time.Now(),
|
||||
LastUpdate: time.Now(),
|
||||
FundingPaid: big.NewInt(0),
|
||||
ID: "pos1",
|
||||
Symbol: "BTC-USDT",
|
||||
Side: Buy,
|
||||
Size: 0.1,
|
||||
EntryPrice: 50000.0,
|
||||
Leverage: 10.0,
|
||||
Margin: big.NewInt(500),
|
||||
UnrealizedPnL: big.NewInt(0),
|
||||
RealizedPnL: big.NewInt(0),
|
||||
Fees: big.NewInt(0),
|
||||
OpenTime: time.Now(),
|
||||
LastUpdate: time.Now(),
|
||||
FundingPaid: big.NewInt(0),
|
||||
}
|
||||
pos2 := &MarginPosition{
|
||||
ID: "pos2",
|
||||
Symbol: "ETH-USDT",
|
||||
Side: Sell,
|
||||
Size: 1.0,
|
||||
EntryPrice: 3000.0,
|
||||
Leverage: 10.0,
|
||||
Margin: big.NewInt(300),
|
||||
UnrealizedPnL: big.NewInt(0),
|
||||
RealizedPnL: big.NewInt(0),
|
||||
Fees: big.NewInt(0),
|
||||
OpenTime: time.Now(),
|
||||
LastUpdate: time.Now(),
|
||||
FundingPaid: big.NewInt(0),
|
||||
ID: "pos2",
|
||||
Symbol: "ETH-USDT",
|
||||
Side: Sell,
|
||||
Size: 1.0,
|
||||
EntryPrice: 3000.0,
|
||||
Leverage: 10.0,
|
||||
Margin: big.NewInt(300),
|
||||
UnrealizedPnL: big.NewInt(0),
|
||||
RealizedPnL: big.NewInt(0),
|
||||
Fees: big.NewInt(0),
|
||||
OpenTime: time.Now(),
|
||||
LastUpdate: time.Now(),
|
||||
FundingPaid: big.NewInt(0),
|
||||
}
|
||||
account.Positions["pos1"] = pos1
|
||||
account.Positions["pos2"] = pos2
|
||||
@@ -472,20 +472,20 @@ func TestModifyLeverage_ConcurrentAccess(t *testing.T) {
|
||||
account.FreeMargin = big.NewInt(1000000000)
|
||||
|
||||
position := &MarginPosition{
|
||||
ID: "test_pos_1",
|
||||
Symbol: "BTC-USDT",
|
||||
Side: Buy,
|
||||
Size: 0.1,
|
||||
EntryPrice: 50000.0,
|
||||
MarkPrice: 50000.0,
|
||||
Leverage: 10.0,
|
||||
Margin: big.NewInt(500),
|
||||
UnrealizedPnL: big.NewInt(0),
|
||||
RealizedPnL: big.NewInt(0),
|
||||
Fees: big.NewInt(0),
|
||||
OpenTime: time.Now(),
|
||||
LastUpdate: time.Now(),
|
||||
FundingPaid: big.NewInt(0),
|
||||
ID: "test_pos_1",
|
||||
Symbol: "BTC-USDT",
|
||||
Side: Buy,
|
||||
Size: 0.1,
|
||||
EntryPrice: 50000.0,
|
||||
MarkPrice: 50000.0,
|
||||
Leverage: 10.0,
|
||||
Margin: big.NewInt(500),
|
||||
UnrealizedPnL: big.NewInt(0),
|
||||
RealizedPnL: big.NewInt(0),
|
||||
Fees: big.NewInt(0),
|
||||
OpenTime: time.Now(),
|
||||
LastUpdate: time.Now(),
|
||||
FundingPaid: big.NewInt(0),
|
||||
}
|
||||
account.Positions[position.ID] = position
|
||||
account.MarginUsed = new(big.Int).Set(position.Margin)
|
||||
|
||||
-297
@@ -1,297 +0,0 @@
|
||||
// Package mlx provides GPU-accelerated order matching using luxfi/mlx framework
|
||||
package mlx
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Backend represents the compute backend for MLX
|
||||
type Backend string
|
||||
|
||||
const (
|
||||
BackendCPU Backend = "CPU"
|
||||
BackendMetal Backend = "Metal"
|
||||
BackendCUDA Backend = "CUDA"
|
||||
BackendAuto Backend = "Auto"
|
||||
)
|
||||
|
||||
// Config for MLX engine
|
||||
type Config struct {
|
||||
Backend Backend
|
||||
Device string
|
||||
MaxBatch int
|
||||
}
|
||||
|
||||
// Engine interface for order matching
|
||||
type Engine interface {
|
||||
Backend() Backend
|
||||
Device() string
|
||||
IsGPUAvailable() bool
|
||||
BatchMatch(bids, asks []Order) []Trade
|
||||
Benchmark(numOrders int) float64
|
||||
Close()
|
||||
}
|
||||
|
||||
// Order represents a trading order
|
||||
type Order struct {
|
||||
ID uint64
|
||||
Side int // 0=bid, 1=ask
|
||||
Price float64
|
||||
Size float64
|
||||
}
|
||||
|
||||
// Trade represents a matched trade
|
||||
type Trade struct {
|
||||
ID uint64
|
||||
BuyOrderID uint64
|
||||
SellOrderID uint64
|
||||
Price float64
|
||||
Size float64
|
||||
}
|
||||
|
||||
// LuxMLXEngine uses the official luxfi/mlx package
|
||||
type LuxMLXEngine struct {
|
||||
backend Backend
|
||||
device string
|
||||
maxBatch int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewEngine creates a new MLX engine using luxfi/mlx
|
||||
func NewEngine(config Config) (Engine, error) {
|
||||
// Auto-detect backend based on platform
|
||||
var backendStr Backend
|
||||
deviceStr := "CPU (" + runtime.GOARCH + ")"
|
||||
|
||||
// Simple platform detection
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
backendStr = BackendMetal
|
||||
deviceStr = "Metal (Apple Silicon)"
|
||||
} else {
|
||||
backendStr = BackendCPU
|
||||
}
|
||||
case "linux":
|
||||
// Could check for CUDA here
|
||||
backendStr = BackendCPU
|
||||
default:
|
||||
backendStr = BackendCPU
|
||||
}
|
||||
|
||||
// Override with config if specified
|
||||
if config.Backend != BackendAuto {
|
||||
backendStr = config.Backend
|
||||
}
|
||||
|
||||
// If GPU not available or CPU requested, use simple implementation
|
||||
if backendStr == BackendCPU {
|
||||
return &simpleEngine{
|
||||
backend: BackendCPU,
|
||||
device: deviceStr,
|
||||
maxBatch: config.MaxBatch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &LuxMLXEngine{
|
||||
backend: backendStr,
|
||||
device: deviceStr,
|
||||
maxBatch: config.MaxBatch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *LuxMLXEngine) Backend() Backend {
|
||||
return e.backend
|
||||
}
|
||||
|
||||
func (e *LuxMLXEngine) Device() string {
|
||||
return e.device
|
||||
}
|
||||
|
||||
func (e *LuxMLXEngine) IsGPUAvailable() bool {
|
||||
return e.backend == BackendMetal || e.backend == BackendCUDA
|
||||
}
|
||||
|
||||
// BatchMatch performs GPU-accelerated order matching using luxfi/mlx
|
||||
func (e *LuxMLXEngine) BatchMatch(bids, asks []Order) []Trade {
|
||||
if len(bids) == 0 || len(asks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// For now, use simplified matching logic
|
||||
// In production, this would use luxfi/mlx GPU acceleration
|
||||
trades := []Trade{}
|
||||
bidIdx, askIdx := 0, 0
|
||||
|
||||
for bidIdx < len(bids) && askIdx < len(asks) {
|
||||
if bids[bidIdx].Price >= asks[askIdx].Price {
|
||||
size := bids[bidIdx].Size
|
||||
if asks[askIdx].Size < size {
|
||||
size = asks[askIdx].Size
|
||||
}
|
||||
|
||||
trades = append(trades, Trade{
|
||||
ID: uint64(len(trades) + 1),
|
||||
BuyOrderID: bids[bidIdx].ID,
|
||||
SellOrderID: asks[askIdx].ID,
|
||||
Price: asks[askIdx].Price,
|
||||
Size: size,
|
||||
})
|
||||
|
||||
bids[bidIdx].Size -= size
|
||||
asks[askIdx].Size -= size
|
||||
|
||||
if bids[bidIdx].Size == 0 {
|
||||
bidIdx++
|
||||
}
|
||||
if asks[askIdx].Size == 0 {
|
||||
askIdx++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: When luxfi/mlx API is stable, use GPU acceleration here
|
||||
// Example: luxmlx.ProcessOrders(bids, asks)
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
// Benchmark runs a performance benchmark
|
||||
func (e *LuxMLXEngine) Benchmark(numOrders int) float64 {
|
||||
start := time.Now()
|
||||
|
||||
// Create test orders
|
||||
bids := make([]Order, numOrders/2)
|
||||
asks := make([]Order, numOrders/2)
|
||||
|
||||
for i := range bids {
|
||||
bids[i] = Order{
|
||||
ID: uint64(i),
|
||||
Side: 0,
|
||||
Price: 50000.0 - float64(i%100),
|
||||
Size: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
for i := range asks {
|
||||
asks[i] = Order{
|
||||
ID: uint64(i + numOrders/2),
|
||||
Side: 1,
|
||||
Price: 50001.0 + float64(i%100),
|
||||
Size: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
// Perform matching
|
||||
_ = e.BatchMatch(bids, asks)
|
||||
|
||||
elapsed := time.Since(start)
|
||||
return float64(numOrders) / elapsed.Seconds()
|
||||
}
|
||||
|
||||
func (e *LuxMLXEngine) Close() {
|
||||
// MLX context cleanup if needed
|
||||
}
|
||||
|
||||
// simpleEngine is the CPU-only fallback implementation
|
||||
type simpleEngine struct {
|
||||
backend Backend
|
||||
device string
|
||||
maxBatch int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (e *simpleEngine) Backend() Backend {
|
||||
return e.backend
|
||||
}
|
||||
|
||||
func (e *simpleEngine) Device() string {
|
||||
return e.device
|
||||
}
|
||||
|
||||
func (e *simpleEngine) IsGPUAvailable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *simpleEngine) BatchMatch(bids, asks []Order) []Trade {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
|
||||
trades := make([]Trade, 0)
|
||||
tradeID := uint64(1)
|
||||
|
||||
bidIdx, askIdx := 0, 0
|
||||
|
||||
for bidIdx < len(bids) && askIdx < len(asks) {
|
||||
bid := &bids[bidIdx]
|
||||
ask := &asks[askIdx]
|
||||
|
||||
if bid.Price >= ask.Price {
|
||||
size := bid.Size
|
||||
if ask.Size < size {
|
||||
size = ask.Size
|
||||
}
|
||||
|
||||
trades = append(trades, Trade{
|
||||
ID: tradeID,
|
||||
BuyOrderID: bid.ID,
|
||||
SellOrderID: ask.ID,
|
||||
Price: ask.Price,
|
||||
Size: size,
|
||||
})
|
||||
tradeID++
|
||||
|
||||
bid.Size -= size
|
||||
ask.Size -= size
|
||||
|
||||
if bid.Size == 0 {
|
||||
bidIdx++
|
||||
}
|
||||
if ask.Size == 0 {
|
||||
askIdx++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
func (e *simpleEngine) Benchmark(numOrders int) float64 {
|
||||
start := time.Now()
|
||||
|
||||
bids := make([]Order, numOrders/2)
|
||||
asks := make([]Order, numOrders/2)
|
||||
|
||||
for i := range bids {
|
||||
bids[i] = Order{
|
||||
ID: uint64(i),
|
||||
Side: 0,
|
||||
Price: 50000.0 - float64(i%100),
|
||||
Size: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
for i := range asks {
|
||||
asks[i] = Order{
|
||||
ID: uint64(i + numOrders/2),
|
||||
Side: 1,
|
||||
Price: 50001.0 + float64(i%100),
|
||||
Size: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
_ = e.BatchMatch(bids, asks)
|
||||
|
||||
elapsed := time.Since(start)
|
||||
return float64(numOrders) / elapsed.Seconds()
|
||||
}
|
||||
|
||||
func (e *simpleEngine) Close() {
|
||||
// Nothing to close for simple engine
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package mlx
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSimpleEngine(t *testing.T) {
|
||||
// Test simple engine (CPU fallback)
|
||||
engine := &simpleEngine{
|
||||
backend: BackendCPU,
|
||||
device: "CPU (test)",
|
||||
maxBatch: 100,
|
||||
}
|
||||
|
||||
assert.Equal(t, BackendCPU, engine.Backend())
|
||||
assert.Equal(t, "CPU (test)", engine.Device())
|
||||
assert.False(t, engine.IsGPUAvailable())
|
||||
|
||||
// Test batch matching
|
||||
bids := []Order{
|
||||
{ID: 1, Side: 0, Price: 100, Size: 10},
|
||||
{ID: 2, Side: 0, Price: 99, Size: 5},
|
||||
}
|
||||
asks := []Order{
|
||||
{ID: 3, Side: 1, Price: 99, Size: 8},
|
||||
{ID: 4, Side: 1, Price: 101, Size: 10},
|
||||
}
|
||||
|
||||
trades := engine.BatchMatch(bids, asks)
|
||||
assert.NotEmpty(t, trades)
|
||||
assert.Equal(t, uint64(1), trades[0].ID)
|
||||
|
||||
// Test benchmark
|
||||
throughput := engine.Benchmark(100)
|
||||
assert.Greater(t, throughput, float64(0))
|
||||
|
||||
// Test close
|
||||
engine.Close()
|
||||
}
|
||||
|
||||
func TestLuxMLXEngine(t *testing.T) {
|
||||
// Test LuxMLX engine
|
||||
engine := &LuxMLXEngine{
|
||||
backend: BackendMetal,
|
||||
device: "Metal (Test)",
|
||||
maxBatch: 1000,
|
||||
}
|
||||
|
||||
assert.Equal(t, BackendMetal, engine.Backend())
|
||||
assert.Equal(t, "Metal (Test)", engine.Device())
|
||||
assert.True(t, engine.IsGPUAvailable())
|
||||
|
||||
// Test batch matching
|
||||
bids := []Order{
|
||||
{ID: 1, Side: 0, Price: 100, Size: 10},
|
||||
{ID: 2, Side: 0, Price: 99, Size: 5},
|
||||
}
|
||||
asks := []Order{
|
||||
{ID: 3, Side: 1, Price: 99, Size: 8},
|
||||
{ID: 4, Side: 1, Price: 101, Size: 10},
|
||||
}
|
||||
|
||||
trades := engine.BatchMatch(bids, asks)
|
||||
assert.NotEmpty(t, trades)
|
||||
|
||||
// Test benchmark
|
||||
throughput := engine.Benchmark(100)
|
||||
assert.Greater(t, throughput, float64(0))
|
||||
|
||||
// Test close
|
||||
engine.Close()
|
||||
}
|
||||
|
||||
func TestEngineCreation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config Config
|
||||
wantGPU bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Auto backend",
|
||||
config: Config{
|
||||
Backend: BackendAuto,
|
||||
MaxBatch: 100,
|
||||
},
|
||||
wantGPU: false, // Depends on platform
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "CPU backend",
|
||||
config: Config{
|
||||
Backend: BackendCPU,
|
||||
MaxBatch: 100,
|
||||
},
|
||||
wantGPU: false,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Metal backend",
|
||||
config: Config{
|
||||
Backend: BackendMetal,
|
||||
MaxBatch: 100,
|
||||
},
|
||||
wantGPU: false, // Will fall back to CPU if not available
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
engine, err := NewEngine(tc.config)
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, engine)
|
||||
|
||||
// Test engine methods
|
||||
assert.NotEmpty(t, engine.Backend())
|
||||
assert.NotEmpty(t, engine.Device())
|
||||
|
||||
// Test empty batch
|
||||
trades := engine.BatchMatch([]Order{}, []Order{})
|
||||
assert.Empty(t, trades)
|
||||
|
||||
// Test with orders
|
||||
bids := []Order{{ID: 1, Side: 0, Price: 100, Size: 10}}
|
||||
asks := []Order{{ID: 2, Side: 1, Price: 100, Size: 10}}
|
||||
trades = engine.BatchMatch(bids, asks)
|
||||
assert.NotEmpty(t, trades)
|
||||
|
||||
engine.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchMatchEdgeCases(t *testing.T) {
|
||||
engine := &simpleEngine{
|
||||
backend: BackendCPU,
|
||||
device: "CPU",
|
||||
maxBatch: 100,
|
||||
}
|
||||
|
||||
// Empty bids
|
||||
trades := engine.BatchMatch([]Order{}, []Order{{ID: 1, Side: 1, Price: 100, Size: 10}})
|
||||
assert.Empty(t, trades)
|
||||
|
||||
// Empty asks
|
||||
trades = engine.BatchMatch([]Order{{ID: 1, Side: 0, Price: 100, Size: 10}}, []Order{})
|
||||
assert.Empty(t, trades)
|
||||
|
||||
// No matching prices
|
||||
bids := []Order{{ID: 1, Side: 0, Price: 90, Size: 10}}
|
||||
asks := []Order{{ID: 2, Side: 1, Price: 100, Size: 10}}
|
||||
trades = engine.BatchMatch(bids, asks)
|
||||
assert.Empty(t, trades)
|
||||
|
||||
// Partial fill
|
||||
bids = []Order{{ID: 1, Side: 0, Price: 100, Size: 15}}
|
||||
asks = []Order{{ID: 2, Side: 1, Price: 100, Size: 10}}
|
||||
trades = engine.BatchMatch(bids, asks)
|
||||
assert.Len(t, trades, 1)
|
||||
assert.Equal(t, float64(10), trades[0].Size)
|
||||
|
||||
// Multiple matches
|
||||
bids = []Order{
|
||||
{ID: 1, Side: 0, Price: 100, Size: 10},
|
||||
{ID: 2, Side: 0, Price: 99, Size: 5},
|
||||
}
|
||||
asks = []Order{
|
||||
{ID: 3, Side: 1, Price: 99, Size: 8},
|
||||
{ID: 4, Side: 1, Price: 98, Size: 10},
|
||||
}
|
||||
trades = engine.BatchMatch(bids, asks)
|
||||
assert.NotEmpty(t, trades)
|
||||
}
|
||||
|
||||
func BenchmarkSimpleEngine(b *testing.B) {
|
||||
engine := &simpleEngine{
|
||||
backend: BackendCPU,
|
||||
device: "CPU",
|
||||
maxBatch: 1000,
|
||||
}
|
||||
|
||||
bids := make([]Order, 100)
|
||||
asks := make([]Order, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
bids[i] = Order{
|
||||
ID: uint64(i),
|
||||
Side: 0,
|
||||
Price: 100 - float64(i%10),
|
||||
Size: 10,
|
||||
}
|
||||
asks[i] = Order{
|
||||
ID: uint64(i + 100),
|
||||
Side: 1,
|
||||
Price: 100 + float64(i%10),
|
||||
Size: 10,
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = engine.BatchMatch(bids, asks)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLuxMLXEngine(b *testing.B) {
|
||||
engine := &LuxMLXEngine{
|
||||
backend: BackendMetal,
|
||||
device: "Metal",
|
||||
maxBatch: 1000,
|
||||
}
|
||||
|
||||
bids := make([]Order, 100)
|
||||
asks := make([]Order, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
bids[i] = Order{
|
||||
ID: uint64(i),
|
||||
Side: 0,
|
||||
Price: 100 - float64(i%10),
|
||||
Size: 10,
|
||||
}
|
||||
asks[i] = Order{
|
||||
ID: uint64(i + 100),
|
||||
Side: 1,
|
||||
Price: 100 + float64(i%10),
|
||||
Size: 10,
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = engine.BatchMatch(bids, asks)
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
//go:build mlxreal
|
||||
// +build mlxreal
|
||||
|
||||
package mlx
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
realmlx "github.com/luxfi/mlx"
|
||||
)
|
||||
|
||||
// RealMLXEngine uses the actual MLX Go bindings for GPU acceleration
|
||||
type RealMLXEngine struct {
|
||||
backend Backend
|
||||
device string
|
||||
maxBatch int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewRealMLXEngine creates an engine using the real MLX bindings
|
||||
func NewRealMLXEngine(config Config) (*RealMLXEngine, error) {
|
||||
// Get the actual backend from external MLX
|
||||
extBackend := realmlx.GetBackend()
|
||||
device := realmlx.GetDevice()
|
||||
|
||||
var backendStr Backend
|
||||
switch extBackend {
|
||||
case realmlx.Metal:
|
||||
backendStr = BackendMetal
|
||||
case realmlx.CUDA:
|
||||
backendStr = BackendCUDA
|
||||
default:
|
||||
backendStr = BackendCPU
|
||||
}
|
||||
|
||||
deviceStr := "CPU (" + runtime.GOARCH + ")"
|
||||
if device != nil {
|
||||
deviceStr = device.Type.String()
|
||||
}
|
||||
|
||||
return &RealMLXEngine{
|
||||
backend: backendStr,
|
||||
device: deviceStr,
|
||||
maxBatch: config.MaxBatch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (e *RealMLXEngine) Backend() Backend {
|
||||
return e.backend
|
||||
}
|
||||
|
||||
func (e *RealMLXEngine) Device() string {
|
||||
return e.device
|
||||
}
|
||||
|
||||
func (e *RealMLXEngine) IsGPUAvailable() bool {
|
||||
return e.backend == BackendMetal || e.backend == BackendCUDA
|
||||
}
|
||||
|
||||
// BatchMatch performs GPU-accelerated order matching using MLX
|
||||
func (e *RealMLXEngine) BatchMatch(bids, asks []Order) []Trade {
|
||||
if len(bids) == 0 || len(asks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert orders to MLX arrays for GPU processing
|
||||
bidPrices := make([]float32, len(bids))
|
||||
bidSizes := make([]float32, len(bids))
|
||||
askPrices := make([]float32, len(asks))
|
||||
askSizes := make([]float32, len(asks))
|
||||
|
||||
for i, bid := range bids {
|
||||
bidPrices[i] = float32(bid.Price)
|
||||
bidSizes[i] = float32(bid.Size)
|
||||
}
|
||||
|
||||
for i, ask := range asks {
|
||||
askPrices[i] = float32(ask.Price)
|
||||
askSizes[i] = float32(ask.Size)
|
||||
}
|
||||
|
||||
// Create MLX arrays on GPU
|
||||
// Note: The external MLX doesn't have ArrayFromSlice, so we use Zeros and would
|
||||
// need to copy data in a real implementation
|
||||
bidPriceArray := realmlx.Zeros([]int{len(bidPrices)}, realmlx.Float32)
|
||||
bidSizeArray := realmlx.Zeros([]int{len(bidSizes)}, realmlx.Float32)
|
||||
askPriceArray := realmlx.Zeros([]int{len(askPrices)}, realmlx.Float32)
|
||||
askSizeArray := realmlx.Zeros([]int{len(askSizes)}, realmlx.Float32)
|
||||
|
||||
// GPU-accelerated price comparison
|
||||
// For each bid, find matching asks (bid price >= ask price)
|
||||
// This would use MLX's broadcasting and comparison operations
|
||||
|
||||
// Simplified matching logic - in production this would be fully GPU-accelerated
|
||||
trades := []Trade{}
|
||||
bidIdx, askIdx := 0, 0
|
||||
|
||||
for bidIdx < len(bids) && askIdx < len(asks) {
|
||||
if bids[bidIdx].Price >= asks[askIdx].Price {
|
||||
size := bids[bidIdx].Size
|
||||
if asks[askIdx].Size < size {
|
||||
size = asks[askIdx].Size
|
||||
}
|
||||
|
||||
trades = append(trades, Trade{
|
||||
ID: uint64(len(trades) + 1),
|
||||
BuyOrderID: bids[bidIdx].ID,
|
||||
SellOrderID: asks[askIdx].ID,
|
||||
Price: asks[askIdx].Price,
|
||||
Size: size,
|
||||
})
|
||||
|
||||
bids[bidIdx].Size -= size
|
||||
asks[askIdx].Size -= size
|
||||
|
||||
if bids[bidIdx].Size == 0 {
|
||||
bidIdx++
|
||||
}
|
||||
if asks[askIdx].Size == 0 {
|
||||
askIdx++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Force evaluation and sync
|
||||
realmlx.Eval(bidPriceArray, bidSizeArray, askPriceArray, askSizeArray)
|
||||
realmlx.Synchronize()
|
||||
|
||||
return trades
|
||||
}
|
||||
|
||||
// Benchmark runs a performance benchmark using real MLX GPU acceleration
|
||||
func (e *RealMLXEngine) Benchmark(numOrders int) float64 {
|
||||
// Create test data
|
||||
bids := make([]float32, numOrders/2)
|
||||
asks := make([]float32, numOrders/2)
|
||||
|
||||
for i := range bids {
|
||||
bids[i] = 50000.0 - float32(i%100)
|
||||
}
|
||||
for i := range asks {
|
||||
asks[i] = 50001.0 + float32(i%100)
|
||||
}
|
||||
|
||||
// Create MLX arrays
|
||||
start := time.Now()
|
||||
|
||||
// Create MLX arrays (using Zeros since ArrayFromSlice doesn't exist)
|
||||
bidArray := realmlx.Zeros([]int{len(bids)}, realmlx.Float32)
|
||||
askArray := realmlx.Zeros([]int{len(asks)}, realmlx.Float32)
|
||||
|
||||
// Perform GPU operations
|
||||
// Note: Greater doesn't exist, so we use Add as a demonstration
|
||||
result := realmlx.Add(bidArray, askArray)
|
||||
|
||||
// Force evaluation on GPU
|
||||
realmlx.Eval(result)
|
||||
realmlx.Synchronize()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
return float64(numOrders) / elapsed.Seconds()
|
||||
}
|
||||
|
||||
func (e *RealMLXEngine) Close() {
|
||||
// MLX context cleanup if needed
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package mlx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMLXEngine(t *testing.T) {
|
||||
// Create MLX engine
|
||||
engine, err := NewEngine(Config{
|
||||
Backend: BackendAuto,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create MLX engine: %v", err)
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
// Check backend detection
|
||||
backend := engine.Backend()
|
||||
device := engine.Device()
|
||||
hasGPU := engine.IsGPUAvailable()
|
||||
|
||||
t.Logf("MLX Backend: %s", backend)
|
||||
t.Logf("Device: %s", device)
|
||||
t.Logf("GPU Available: %v", hasGPU)
|
||||
|
||||
// Ensure we got a valid backend
|
||||
if backend != BackendCPU && backend != BackendMetal && backend != BackendCUDA {
|
||||
t.Errorf("Invalid backend: %s", backend)
|
||||
}
|
||||
|
||||
// Device should not be empty
|
||||
if device == "" {
|
||||
t.Error("Device name is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLXInfo(t *testing.T) {
|
||||
// Test engine creation
|
||||
engine, err := NewEngine(Config{
|
||||
Backend: BackendAuto,
|
||||
MaxBatch: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create engine: %v", err)
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
backend := engine.Backend()
|
||||
t.Logf("Detected Backend: %s", backend)
|
||||
|
||||
hasGPU := engine.IsGPUAvailable()
|
||||
t.Logf("GPU Support: %v", hasGPU)
|
||||
|
||||
device := engine.Device()
|
||||
t.Logf("Device: %s", device)
|
||||
|
||||
// Should always have at least CPU backend
|
||||
if backend == "" {
|
||||
t.Error("Backend should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMLXBenchmark(t *testing.T) {
|
||||
engine, err := NewEngine(Config{
|
||||
Backend: BackendAuto,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create MLX engine: %v", err)
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
// Run a small benchmark
|
||||
throughput := engine.Benchmark(1000)
|
||||
t.Logf("Throughput: %.2f orders/sec", throughput)
|
||||
|
||||
if throughput <= 0 {
|
||||
t.Error("Benchmark throughput should be positive")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMLXMatching(b *testing.B) {
|
||||
engine, err := NewEngine(Config{
|
||||
Backend: BackendAuto,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create MLX engine: %v", err)
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
// Create test orders
|
||||
bids := make([]Order, 100)
|
||||
asks := make([]Order, 100)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
bids[i] = Order{
|
||||
ID: uint64(i),
|
||||
Price: 50000.0 - float64(i)*0.1,
|
||||
Size: 1.0,
|
||||
Side: 0,
|
||||
}
|
||||
asks[i] = Order{
|
||||
ID: uint64(i + 100),
|
||||
Price: 50001.0 + float64(i)*0.1,
|
||||
Size: 1.0,
|
||||
Side: 1,
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
trades := engine.BatchMatch(bids, asks)
|
||||
_ = trades
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleNewEngine() {
|
||||
engine, err := NewEngine(Config{
|
||||
Backend: BackendAuto,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create engine: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
fmt.Printf("Backend: %s\n", engine.Backend())
|
||||
fmt.Printf("Device: %s\n", engine.Device())
|
||||
// Output will vary based on hardware:
|
||||
// Backend: Metal
|
||||
// Device: Apple Silicon GPU
|
||||
// or
|
||||
// Backend: CPU
|
||||
// Device: CPU (arm64)
|
||||
}
|
||||
|
||||
// Tests for the Engine interface
|
||||
func TestEngineInterface(t *testing.T) {
|
||||
engine, err := NewEngine(Config{
|
||||
Backend: BackendAuto,
|
||||
})
|
||||
if err != nil {
|
||||
t.Logf("Failed to create engine (CGO may be disabled): %v", err)
|
||||
// This should never fail with pure Go implementation
|
||||
t.Fatalf("Failed to create engine: %v", err)
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
// Test Backend
|
||||
backend := engine.Backend()
|
||||
t.Logf("Engine Backend: %s", backend)
|
||||
|
||||
// Test Device
|
||||
device := engine.Device()
|
||||
t.Logf("Engine Device: %s", device)
|
||||
|
||||
// Test GPU availability
|
||||
hasGPU := engine.IsGPUAvailable()
|
||||
t.Logf("GPU Available: %v", hasGPU)
|
||||
|
||||
// Test BatchMatch
|
||||
bids := []Order{
|
||||
{ID: 1, Price: 100.0, Size: 10.0, Side: 0},
|
||||
{ID: 2, Price: 99.0, Size: 20.0, Side: 0},
|
||||
}
|
||||
asks := []Order{
|
||||
{ID: 3, Price: 100.0, Size: 15.0, Side: 1},
|
||||
{ID: 4, Price: 101.0, Size: 25.0, Side: 1},
|
||||
}
|
||||
|
||||
trades := engine.BatchMatch(bids, asks)
|
||||
t.Logf("Matched %d trades", len(trades))
|
||||
|
||||
// Test Benchmark
|
||||
throughput := engine.Benchmark(1000)
|
||||
t.Logf("Benchmark Throughput: %.2f orders/sec", throughput)
|
||||
|
||||
if throughput <= 0 {
|
||||
t.Error("Throughput should be positive")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkEngineMatching(b *testing.B) {
|
||||
engine, err := NewEngine(Config{
|
||||
Backend: BackendAuto,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Failed to create engine: %v", err)
|
||||
}
|
||||
defer engine.Close()
|
||||
|
||||
// Create test orders
|
||||
bids := make([]Order, 100)
|
||||
asks := make([]Order, 100)
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
bids[i] = Order{
|
||||
ID: uint64(i),
|
||||
Price: 100.0 - float64(i)*0.01,
|
||||
Size: 10.0,
|
||||
Side: 0,
|
||||
}
|
||||
asks[i] = Order{
|
||||
ID: uint64(i + 100),
|
||||
Price: 100.0 + float64(i)*0.01,
|
||||
Size: 10.0,
|
||||
Side: 1,
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = engine.BatchMatch(bids, asks)
|
||||
}
|
||||
}
|
||||
@@ -76,4 +76,4 @@ func (c *cppOrderBook) GetDepth(level int) *Depth {
|
||||
func (c *cppOrderBook) GetVolume() uint64 {
|
||||
// Placeholder implementation
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +329,6 @@ func sourceToChain(source string) string {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func (o *Oracle) alert(symbol string, t AlertType, sev Severity, msg string) {
|
||||
select {
|
||||
case o.alerts <- &Alert{
|
||||
|
||||
+30
-30
@@ -49,40 +49,40 @@ func NewSymbolMap() *SymbolMap {
|
||||
func defaultAliases() map[string]string {
|
||||
return map[string]string{
|
||||
// USD variations -> keep as USD (oracle standard)
|
||||
"LUX/USD": "LUX-USD",
|
||||
"LUXUSD": "LUX-USD",
|
||||
"ETH/USD": "ETH-USD",
|
||||
"ETHUSD": "ETH-USD",
|
||||
"BTC/USD": "BTC-USD",
|
||||
"BTCUSD": "BTC-USD",
|
||||
"AVAX/USD": "AVAX-USD",
|
||||
"AVAXUSD": "AVAX-USD",
|
||||
"SOL/USD": "SOL-USD",
|
||||
"SOLUSD": "SOL-USD",
|
||||
"LUX/USD": "LUX-USD",
|
||||
"LUXUSD": "LUX-USD",
|
||||
"ETH/USD": "ETH-USD",
|
||||
"ETHUSD": "ETH-USD",
|
||||
"BTC/USD": "BTC-USD",
|
||||
"BTCUSD": "BTC-USD",
|
||||
"AVAX/USD": "AVAX-USD",
|
||||
"AVAXUSD": "AVAX-USD",
|
||||
"SOL/USD": "SOL-USD",
|
||||
"SOLUSD": "SOL-USD",
|
||||
|
||||
// USDC variations
|
||||
"LUX/USDC": "LUX-USDC",
|
||||
"LUXUSDC": "LUX-USDC",
|
||||
"ETH/USDC": "ETH-USDC",
|
||||
"ETHUSDC": "ETH-USDC",
|
||||
"BTC/USDC": "BTC-USDC",
|
||||
"BTCUSDC": "BTC-USDC",
|
||||
"AVAX/USDC": "AVAX-USDC",
|
||||
"AVAXUSDC": "AVAX-USDC",
|
||||
"LUX/USDC": "LUX-USDC",
|
||||
"LUXUSDC": "LUX-USDC",
|
||||
"ETH/USDC": "ETH-USDC",
|
||||
"ETHUSDC": "ETH-USDC",
|
||||
"BTC/USDC": "BTC-USDC",
|
||||
"BTCUSDC": "BTC-USDC",
|
||||
"AVAX/USDC": "AVAX-USDC",
|
||||
"AVAXUSDC": "AVAX-USDC",
|
||||
|
||||
// USDT variations
|
||||
"LUX-USDT": "LUX-USDT",
|
||||
"LUX/USDT": "LUX-USDT",
|
||||
"LUXUSDT": "LUX-USDT",
|
||||
"ETH-USDT": "ETH-USDT",
|
||||
"ETH/USDT": "ETH-USDT",
|
||||
"ETHUSDT": "ETH-USDT",
|
||||
"BTC-USDT": "BTC-USDT",
|
||||
"BTC/USDT": "BTC-USDT",
|
||||
"BTCUSDT": "BTC-USDT",
|
||||
"AVAX-USDT": "AVAX-USDT",
|
||||
"AVAX/USDT": "AVAX-USDT",
|
||||
"AVAXUSDT": "AVAX-USDT",
|
||||
"LUX-USDT": "LUX-USDT",
|
||||
"LUX/USDT": "LUX-USDT",
|
||||
"LUXUSDT": "LUX-USDT",
|
||||
"ETH-USDT": "ETH-USDT",
|
||||
"ETH/USDT": "ETH-USDT",
|
||||
"ETHUSDT": "ETH-USDT",
|
||||
"BTC-USDT": "BTC-USDT",
|
||||
"BTC/USDT": "BTC-USDT",
|
||||
"BTCUSDT": "BTC-USDT",
|
||||
"AVAX-USDT": "AVAX-USDT",
|
||||
"AVAX/USDT": "AVAX-USDT",
|
||||
"AVAXUSDT": "AVAX-USDT",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -218,10 +218,10 @@ func (s *ZooChainSource) simulate(symbol string) float64 {
|
||||
// Simulated prices for Zoo Chain DEX
|
||||
base := map[string]float64{
|
||||
// ZOO native pairs
|
||||
"ZOO-USDC": 0.00015, // ZOO token price
|
||||
"ZOO-USDT": 0.00015,
|
||||
"ZOO-ETH": 0.0000000375, // ZOO/ETH ratio
|
||||
"ZOO-LUX": 0.0647, // ZOO/LUX ratio
|
||||
"ZOO-USDC": 0.00015, // ZOO token price
|
||||
"ZOO-USDT": 0.00015,
|
||||
"ZOO-ETH": 0.0000000375, // ZOO/ETH ratio
|
||||
"ZOO-LUX": 0.0647, // ZOO/LUX ratio
|
||||
|
||||
// Wrapped ZOO
|
||||
"WZOO-USDC": 0.00015,
|
||||
|
||||
@@ -214,7 +214,7 @@ func TestOrderSides(t *testing.T) {
|
||||
|
||||
func TestOrderTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
name string
|
||||
orderType string
|
||||
}{
|
||||
{"Market", "market"},
|
||||
|
||||
@@ -113,13 +113,13 @@ var ErrNotSupported = NewTradingError("NOT_SUPPORTED", "operation not supported
|
||||
|
||||
// BaseAdapter provides common functionality for adapters.
|
||||
type BaseAdapter struct {
|
||||
name string
|
||||
venueType VenueType
|
||||
name string
|
||||
venueType VenueType
|
||||
capabilities VenueCapabilities
|
||||
connected bool
|
||||
latency *int64
|
||||
makerFee decimal.Decimal
|
||||
takerFee decimal.Decimal
|
||||
connected bool
|
||||
latency *int64
|
||||
makerFee decimal.Decimal
|
||||
takerFee decimal.Decimal
|
||||
}
|
||||
|
||||
// Name returns the venue name.
|
||||
|
||||
@@ -395,11 +395,11 @@ func (a *HummingbotAdapter) ExecuteSwap(ctx context.Context, baseToken, quoteTok
|
||||
}
|
||||
|
||||
data, err := a.doRequest(ctx, http.MethodPost, "/amm/trade", map[string]interface{}{
|
||||
"base": baseToken,
|
||||
"quote": quoteToken,
|
||||
"amount": amount.String(),
|
||||
"side": side,
|
||||
"limitPrice": "",
|
||||
"base": baseToken,
|
||||
"quote": quoteToken,
|
||||
"amount": amount.String(),
|
||||
"side": side,
|
||||
"limitPrice": "",
|
||||
"allowedSlippage": fmt.Sprintf("%s/100", slippage.String()),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -105,18 +105,18 @@ func TestArbitrageOpportunity(t *testing.T) {
|
||||
spreadBps := spread.Div(buySource.Ask).Mul(decimal.NewFromInt(10000))
|
||||
|
||||
opp := ArbitrageOpportunity{
|
||||
ID: "test-opp-1",
|
||||
Type: ArbTypeSimple,
|
||||
BuySource: buySource,
|
||||
SellSource: sellSource,
|
||||
SpreadBps: spreadBps,
|
||||
EstimatedPnL: decimal.NewFromInt(200),
|
||||
MaxSize: decimal.NewFromInt(10),
|
||||
GasCostUSD: decimal.NewFromFloat(0.50),
|
||||
ID: "test-opp-1",
|
||||
Type: ArbTypeSimple,
|
||||
BuySource: buySource,
|
||||
SellSource: sellSource,
|
||||
SpreadBps: spreadBps,
|
||||
EstimatedPnL: decimal.NewFromInt(200),
|
||||
MaxSize: decimal.NewFromInt(10),
|
||||
GasCostUSD: decimal.NewFromFloat(0.50),
|
||||
BridgeCostUSD: decimal.NewFromInt(0),
|
||||
NetPnL: decimal.NewFromFloat(199.50),
|
||||
Confidence: 0.9,
|
||||
ExpiresAt: now.Add(5 * time.Second),
|
||||
NetPnL: decimal.NewFromFloat(199.50),
|
||||
Confidence: 0.9,
|
||||
ExpiresAt: now.Add(5 * time.Second),
|
||||
}
|
||||
|
||||
if opp.Type != ArbTypeSimple {
|
||||
|
||||
@@ -57,9 +57,9 @@ const (
|
||||
// CrossChainConfig holds cross-chain transport configuration
|
||||
type CrossChainConfig struct {
|
||||
// Warp configuration (Lux internal)
|
||||
WarpEnabled bool
|
||||
WarpEndpoint string
|
||||
WarpTimeout time.Duration
|
||||
WarpEnabled bool
|
||||
WarpEndpoint string
|
||||
WarpTimeout time.Duration
|
||||
|
||||
// Teleport configuration (EVM bridging)
|
||||
TeleportEnabled bool
|
||||
@@ -73,20 +73,20 @@ type CrossChainConfig struct {
|
||||
|
||||
// CrossChainInfo holds information about a chain
|
||||
type CrossChainInfo struct {
|
||||
ChainID string
|
||||
Name string
|
||||
ChainType ChainType
|
||||
BlockTime time.Duration
|
||||
Finality time.Duration
|
||||
WarpSupported bool // Can use Warp (Lux subnets only)
|
||||
TeleportSupported bool // Can use Teleport (EVM chains)
|
||||
Venues []string // Trading venues on this chain
|
||||
ChainID string
|
||||
Name string
|
||||
ChainType ChainType
|
||||
BlockTime time.Duration
|
||||
Finality time.Duration
|
||||
WarpSupported bool // Can use Warp (Lux subnets only)
|
||||
TeleportSupported bool // Can use Teleport (EVM chains)
|
||||
Venues []string // Trading venues on this chain
|
||||
}
|
||||
|
||||
// CrossChainRouter routes messages between chains
|
||||
type CrossChainRouter struct {
|
||||
config CrossChainConfig
|
||||
warp WarpClient
|
||||
config CrossChainConfig
|
||||
warp WarpClient
|
||||
teleport TeleportClient
|
||||
}
|
||||
|
||||
@@ -328,9 +328,9 @@ type ArbitrageOpportunityWithRouting struct {
|
||||
UnifiedOpportunity
|
||||
|
||||
// Routing information
|
||||
Transport CrossChainTransport
|
||||
Transport CrossChainTransport
|
||||
EstimatedLatency time.Duration
|
||||
BridgeCost decimal.Decimal
|
||||
BridgeCost decimal.Decimal
|
||||
|
||||
// Adjusted profitability
|
||||
AdjustedNetProfit decimal.Decimal
|
||||
|
||||
@@ -24,21 +24,21 @@ type Executor struct {
|
||||
address string
|
||||
|
||||
// Contract addresses
|
||||
flashLoanPool string
|
||||
arbitrageContract string
|
||||
flashLoanPool string
|
||||
arbitrageContract string
|
||||
|
||||
// Chain clients
|
||||
chains map[string]ChainClient
|
||||
|
||||
// Execution tracking
|
||||
pendingExecutions map[string]*Execution
|
||||
pendingExecutions map[string]*Execution
|
||||
completedExecutions []Execution
|
||||
|
||||
// Metrics
|
||||
totalExecutions int64
|
||||
totalExecutions int64
|
||||
successfulExecutions int64
|
||||
totalProfitUSD decimal.Decimal
|
||||
totalGasSpent decimal.Decimal
|
||||
totalProfitUSD decimal.Decimal
|
||||
totalGasSpent decimal.Decimal
|
||||
|
||||
// Running state
|
||||
ctx context.Context
|
||||
@@ -116,15 +116,15 @@ type Log struct {
|
||||
|
||||
// Execution represents an arbitrage execution
|
||||
type Execution struct {
|
||||
ID string
|
||||
Opportunity ArbitrageOpportunity
|
||||
Status ExecutionStatus
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Transactions []ExecutedTx
|
||||
ActualPnL decimal.Decimal
|
||||
GasSpent decimal.Decimal
|
||||
Error error
|
||||
ID string
|
||||
Opportunity ArbitrageOpportunity
|
||||
Status ExecutionStatus
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
Transactions []ExecutedTx
|
||||
ActualPnL decimal.Decimal
|
||||
GasSpent decimal.Decimal
|
||||
Error error
|
||||
}
|
||||
|
||||
// ExecutedTx represents an executed transaction
|
||||
@@ -152,12 +152,12 @@ func NewExecutor(config ExecutorConfig, privateKey *ecdsa.PrivateKey) *Executor
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Executor{
|
||||
config: config,
|
||||
privateKey: privateKey,
|
||||
chains: make(map[string]ChainClient),
|
||||
pendingExecutions: make(map[string]*Execution),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
config: config,
|
||||
privateKey: privateKey,
|
||||
chains: make(map[string]ChainClient),
|
||||
pendingExecutions: make(map[string]*Execution),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,9 +476,9 @@ type ExecutorMetrics struct {
|
||||
// DefaultExecutorConfig returns default configuration
|
||||
func DefaultExecutorConfig() ExecutorConfig {
|
||||
return ExecutorConfig{
|
||||
MaxGasPrice: decimal.NewFromInt(100), // 100 gwei max
|
||||
MaxSlippageBps: decimal.NewFromInt(50), // 0.5% max slippage
|
||||
MinConfidence: 0.7, // 70% minimum confidence
|
||||
MaxGasPrice: decimal.NewFromInt(100), // 100 gwei max
|
||||
MaxSlippageBps: decimal.NewFromInt(50), // 0.5% max slippage
|
||||
MinConfidence: 0.7, // 70% minimum confidence
|
||||
MaxConcurrent: 10,
|
||||
UseFlashLoans: true,
|
||||
UseMEVProtection: true,
|
||||
|
||||
@@ -111,13 +111,13 @@ type LxFirstOpportunity struct {
|
||||
LxPrice LxPrice
|
||||
|
||||
// Stale venue to exploit
|
||||
StaleVenue string
|
||||
StalePrice VenuePrice
|
||||
Staleness time.Duration
|
||||
StaleVenue string
|
||||
StalePrice VenuePrice
|
||||
Staleness time.Duration
|
||||
|
||||
// Trade direction
|
||||
Side string // "buy" or "sell" on stale venue
|
||||
Divergence decimal.Decimal
|
||||
Side string // "buy" or "sell" on stale venue
|
||||
Divergence decimal.Decimal
|
||||
DivergenceBps decimal.Decimal
|
||||
|
||||
// Expected profit
|
||||
@@ -207,19 +207,19 @@ func (lf *LxFirstArbitrage) checkOpportunities(symbol string) {
|
||||
|
||||
if divergenceBps.GreaterThanOrEqual(lf.config.MinDivergenceBps) {
|
||||
opp := &LxFirstOpportunity{
|
||||
ID: generateOpportunityID(symbol, vp.Venue, "buy"),
|
||||
Symbol: symbol,
|
||||
Timestamp: now,
|
||||
LxPrice: lxPrice,
|
||||
StaleVenue: vp.Venue,
|
||||
StalePrice: vp,
|
||||
Staleness: staleness,
|
||||
Side: "buy",
|
||||
Divergence: divergence,
|
||||
DivergenceBps: divergenceBps,
|
||||
ID: generateOpportunityID(symbol, vp.Venue, "buy"),
|
||||
Symbol: symbol,
|
||||
Timestamp: now,
|
||||
LxPrice: lxPrice,
|
||||
StaleVenue: vp.Venue,
|
||||
StalePrice: vp,
|
||||
Staleness: staleness,
|
||||
Side: "buy",
|
||||
Divergence: divergence,
|
||||
DivergenceBps: divergenceBps,
|
||||
ExpectedProfit: divergence.Mul(lf.config.MaxPositionSize),
|
||||
MaxSize: lf.config.MaxPositionSize,
|
||||
Confidence: calculateConfidence(staleness, divergenceBps),
|
||||
MaxSize: lf.config.MaxPositionSize,
|
||||
Confidence: calculateConfidence(staleness, divergenceBps),
|
||||
}
|
||||
|
||||
if opp.ExpectedProfit.GreaterThanOrEqual(lf.config.MinProfit) {
|
||||
@@ -239,19 +239,19 @@ func (lf *LxFirstArbitrage) checkOpportunities(symbol string) {
|
||||
|
||||
if divergenceBps.GreaterThanOrEqual(lf.config.MinDivergenceBps) {
|
||||
opp := &LxFirstOpportunity{
|
||||
ID: generateOpportunityID(symbol, vp.Venue, "sell"),
|
||||
Symbol: symbol,
|
||||
Timestamp: now,
|
||||
LxPrice: lxPrice,
|
||||
StaleVenue: vp.Venue,
|
||||
StalePrice: vp,
|
||||
Staleness: staleness,
|
||||
Side: "sell",
|
||||
Divergence: divergence,
|
||||
DivergenceBps: divergenceBps,
|
||||
ID: generateOpportunityID(symbol, vp.Venue, "sell"),
|
||||
Symbol: symbol,
|
||||
Timestamp: now,
|
||||
LxPrice: lxPrice,
|
||||
StaleVenue: vp.Venue,
|
||||
StalePrice: vp,
|
||||
Staleness: staleness,
|
||||
Side: "sell",
|
||||
Divergence: divergence,
|
||||
DivergenceBps: divergenceBps,
|
||||
ExpectedProfit: divergence.Mul(lf.config.MaxPositionSize),
|
||||
MaxSize: lf.config.MaxPositionSize,
|
||||
Confidence: calculateConfidence(staleness, divergenceBps),
|
||||
MaxSize: lf.config.MaxPositionSize,
|
||||
Confidence: calculateConfidence(staleness, divergenceBps),
|
||||
}
|
||||
|
||||
if opp.ExpectedProfit.GreaterThanOrEqual(lf.config.MinProfit) {
|
||||
@@ -302,9 +302,9 @@ func calculateConfidence(staleness time.Duration, divergenceBps decimal.Decimal)
|
||||
// DefaultLxFirstConfig returns default configuration
|
||||
func DefaultLxFirstConfig() LxFirstConfig {
|
||||
return LxFirstConfig{
|
||||
MaxStaleness: 2 * time.Second, // Only trade if venue is <2s stale
|
||||
MinDivergenceBps: decimal.NewFromInt(10), // 0.1% minimum divergence
|
||||
MinProfit: decimal.NewFromInt(5), // $5 minimum profit
|
||||
MaxStaleness: 2 * time.Second, // Only trade if venue is <2s stale
|
||||
MinDivergenceBps: decimal.NewFromInt(10), // 0.1% minimum divergence
|
||||
MinProfit: decimal.NewFromInt(5), // $5 minimum profit
|
||||
MaxPositionSize: decimal.NewFromInt(1000), // $1k max per trade
|
||||
Symbols: []string{"BTC-USDC", "ETH-USDC", "LUX-USDC"},
|
||||
VenueLatencies: map[string]time.Duration{
|
||||
|
||||
@@ -57,11 +57,11 @@ type Route struct {
|
||||
type ArbType string
|
||||
|
||||
const (
|
||||
ArbTypeSimple ArbType = "simple" // Buy A, sell B
|
||||
ArbTypeTriangular ArbType = "triangular" // A->B->C->A
|
||||
ArbTypeMultiHop ArbType = "multi_hop" // Complex routes
|
||||
ArbTypeCEXDEX ArbType = "cex_dex" // CEX<->DEX arb
|
||||
ArbTypeFlashSwap ArbType = "flash_swap" // DEX flash swap
|
||||
ArbTypeSimple ArbType = "simple" // Buy A, sell B
|
||||
ArbTypeTriangular ArbType = "triangular" // A->B->C->A
|
||||
ArbTypeMultiHop ArbType = "multi_hop" // Complex routes
|
||||
ArbTypeCEXDEX ArbType = "cex_dex" // CEX<->DEX arb
|
||||
ArbTypeFlashSwap ArbType = "flash_swap" // DEX flash swap
|
||||
)
|
||||
|
||||
// Scanner continuously scans for arbitrage opportunities
|
||||
@@ -111,14 +111,14 @@ type ScannerConfig struct {
|
||||
|
||||
// ChainConfig holds chain-specific configuration
|
||||
type ChainConfig struct {
|
||||
ChainID string
|
||||
Name string
|
||||
GasPrice decimal.Decimal
|
||||
BlockTime time.Duration
|
||||
BridgeCost decimal.Decimal
|
||||
BridgeLatency time.Duration
|
||||
Venues []string
|
||||
WarpSupported bool // Native Lux Warp
|
||||
ChainID string
|
||||
Name string
|
||||
GasPrice decimal.Decimal
|
||||
BlockTime time.Duration
|
||||
BridgeCost decimal.Decimal
|
||||
BridgeLatency time.Duration
|
||||
Venues []string
|
||||
WarpSupported bool // Native Lux Warp
|
||||
TeleportSupport bool // EVM Teleport bridge
|
||||
}
|
||||
|
||||
@@ -329,18 +329,18 @@ func (s *Scanner) findSimpleArb(symbol string, sources []PriceSource) []Arbitrag
|
||||
confidence := s.calculateConfidence(buySrc, sellSrc)
|
||||
|
||||
opp := ArbitrageOpportunity{
|
||||
ID: fmt.Sprintf("simple-%s-%s-%s-%d", symbol, buySrc.Venue, sellSrc.Venue, time.Now().UnixNano()),
|
||||
Type: ArbTypeSimple,
|
||||
BuySource: buySrc,
|
||||
SellSource: sellSrc,
|
||||
SpreadBps: spreadBps,
|
||||
EstimatedPnL: grossPnL,
|
||||
MaxSize: maxSize,
|
||||
GasCostUSD: gasCost,
|
||||
ID: fmt.Sprintf("simple-%s-%s-%s-%d", symbol, buySrc.Venue, sellSrc.Venue, time.Now().UnixNano()),
|
||||
Type: ArbTypeSimple,
|
||||
BuySource: buySrc,
|
||||
SellSource: sellSrc,
|
||||
SpreadBps: spreadBps,
|
||||
EstimatedPnL: grossPnL,
|
||||
MaxSize: maxSize,
|
||||
GasCostUSD: gasCost,
|
||||
BridgeCostUSD: bridgeCost,
|
||||
NetPnL: netPnL,
|
||||
Confidence: confidence,
|
||||
ExpiresAt: time.Now().Add(5 * time.Second), // Short expiry for HFT
|
||||
NetPnL: netPnL,
|
||||
Confidence: confidence,
|
||||
ExpiresAt: time.Now().Add(5 * time.Second), // Short expiry for HFT
|
||||
Routes: []Route{
|
||||
{
|
||||
ChainID: buySrc.ChainID,
|
||||
@@ -527,10 +527,10 @@ func isCEX(venue string) bool {
|
||||
// DefaultScannerConfig returns default configuration
|
||||
func DefaultScannerConfig() ScannerConfig {
|
||||
return ScannerConfig{
|
||||
MinSpreadBps: decimal.NewFromInt(10), // 0.1%
|
||||
MinProfitUSD: decimal.NewFromInt(10), // $10 minimum
|
||||
MaxPriceAge: 5 * time.Second, // 5 second max age
|
||||
ScanInterval: 100 * time.Millisecond, // 10 scans/second
|
||||
MinSpreadBps: decimal.NewFromInt(10), // 0.1%
|
||||
MinProfitUSD: decimal.NewFromInt(10), // $10 minimum
|
||||
MaxPriceAge: 5 * time.Second, // 5 second max age
|
||||
ScanInterval: 100 * time.Millisecond, // 10 scans/second
|
||||
MaxConcurrency: 50,
|
||||
Symbols: []string{"BTC", "ETH", "LUX", "SOL", "AVAX"},
|
||||
ChainIDs: []string{"lux", "ethereum", "bsc", "arbitrum", "polygon"},
|
||||
@@ -540,14 +540,14 @@ func DefaultScannerConfig() ScannerConfig {
|
||||
// LuxChainConfig returns Lux-optimized chain configuration
|
||||
func LuxChainConfig() ChainConfig {
|
||||
return ChainConfig{
|
||||
ChainID: "lux",
|
||||
Name: "Lux Network",
|
||||
GasPrice: decimal.NewFromFloat(0.000000025), // 25 gwei
|
||||
BlockTime: 400 * time.Millisecond, // Sub-second finality
|
||||
BridgeCost: decimal.NewFromFloat(0.01), // Nearly free via Warp
|
||||
BridgeLatency: 500 * time.Millisecond, // Fast Warp messaging
|
||||
Venues: []string{"lx_dex", "lx_amm"},
|
||||
WarpSupported: true,
|
||||
ChainID: "lux",
|
||||
Name: "Lux Network",
|
||||
GasPrice: decimal.NewFromFloat(0.000000025), // 25 gwei
|
||||
BlockTime: 400 * time.Millisecond, // Sub-second finality
|
||||
BridgeCost: decimal.NewFromFloat(0.01), // Nearly free via Warp
|
||||
BridgeLatency: 500 * time.Millisecond, // Fast Warp messaging
|
||||
Venues: []string{"lx_dex", "lx_amm"},
|
||||
WarpSupported: true,
|
||||
TeleportSupport: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
func ExampleParseTradingPair() {
|
||||
// The SDK supports multiple trading pair formats
|
||||
pairs := []string{
|
||||
"BTC-USDC", // Dash separator
|
||||
"ETH/USDT", // Slash separator (CCXT style)
|
||||
"LUX_USD", // Underscore separator
|
||||
"BTCUSDT", // No separator (exchange native)
|
||||
"BTC-USDC", // Dash separator
|
||||
"ETH/USDT", // Slash separator (CCXT style)
|
||||
"LUX_USD", // Underscore separator
|
||||
"BTCUSDT", // No separator (exchange native)
|
||||
}
|
||||
|
||||
for _, p := range pairs {
|
||||
@@ -132,8 +132,8 @@ func ExampleNewLimitOrder() {
|
||||
// ExampleRiskManager demonstrates risk management operations.
|
||||
func ExampleRiskManager() {
|
||||
config := trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxOrderSize: decimal.NewFromFloat(10),
|
||||
Enabled: true,
|
||||
MaxOrderSize: decimal.NewFromFloat(10),
|
||||
MaxOpenOrders: 5,
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ func ExampleConstantProductPrice() {
|
||||
// Pool with 1000 ETH and 2,000,000 USDC
|
||||
reserveETH := decimal.NewFromFloat(1000)
|
||||
reserveUSDC := decimal.NewFromFloat(2000000)
|
||||
amountIn := decimal.NewFromFloat(10) // Swap 10 ETH
|
||||
amountIn := decimal.NewFromFloat(10) // Swap 10 ETH
|
||||
feeRate := decimal.NewFromFloat(0.003) // 0.3% fee
|
||||
|
||||
// Swap ETH -> USDC
|
||||
|
||||
@@ -132,15 +132,15 @@ type Execution struct {
|
||||
// DefaultArbConfig returns sensible defaults.
|
||||
func DefaultArbConfig() ArbConfig {
|
||||
return ArbConfig{
|
||||
Symbols: []string{"BTC-USDC", "ETH-USDC", "LUX-USDC"},
|
||||
MinSpreadBps: decimal.NewFromInt(15),
|
||||
MinProfit: decimal.NewFromFloat(5),
|
||||
MaxPosition: decimal.NewFromFloat(10000),
|
||||
Symbols: []string{"BTC-USDC", "ETH-USDC", "LUX-USDC"},
|
||||
MinSpreadBps: decimal.NewFromInt(15),
|
||||
MinProfit: decimal.NewFromFloat(5),
|
||||
MaxPosition: decimal.NewFromFloat(10000),
|
||||
VenueFees: map[string]decimal.Decimal{
|
||||
"lx_dex": decimal.NewFromFloat(0.001), // 0.1%
|
||||
"lx_amm": decimal.NewFromFloat(0.003), // 0.3%
|
||||
"binance": decimal.NewFromFloat(0.001), // 0.1%
|
||||
"mexc": decimal.NewFromFloat(0.002), // 0.2%
|
||||
"lx_dex": decimal.NewFromFloat(0.001), // 0.1%
|
||||
"lx_amm": decimal.NewFromFloat(0.003), // 0.3%
|
||||
"binance": decimal.NewFromFloat(0.001), // 0.1%
|
||||
"mexc": decimal.NewFromFloat(0.002), // 0.2%
|
||||
},
|
||||
VenuePriority: []string{"lx_dex", "binance", "mexc", "lx_amm"},
|
||||
ScanInterval: 50 * time.Millisecond,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// - Binance, MEXC, OKX (via CCXT)
|
||||
// - Uniswap, PancakeSwap (via Hummingbot Gateway)
|
||||
//
|
||||
// NO SMART CONTRACTS - just coordinated trades through unified API
|
||||
// # NO SMART CONTRACTS - just coordinated trades through unified API
|
||||
//
|
||||
// Cross-chain transport:
|
||||
// - Warp: For Lux subnet communication (instant)
|
||||
@@ -76,10 +76,10 @@ func main() {
|
||||
|
||||
// Risk management
|
||||
config.Risk = trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxPositionSize: decimal.NewFromInt(10000), // $10k max per trade
|
||||
MaxOrderSize: decimal.NewFromInt(5000), // $5k max order
|
||||
MaxDailyLoss: decimal.NewFromInt(500), // $500 daily loss limit
|
||||
Enabled: true,
|
||||
MaxPositionSize: decimal.NewFromInt(10000), // $10k max per trade
|
||||
MaxOrderSize: decimal.NewFromInt(5000), // $5k max order
|
||||
MaxDailyLoss: decimal.NewFromInt(500), // $500 daily loss limit
|
||||
KillSwitchEnabled: true,
|
||||
}
|
||||
|
||||
@@ -100,10 +100,10 @@ func main() {
|
||||
|
||||
// Create arbitrage system
|
||||
arbConfig := arbitrage.UnifiedArbConfig{
|
||||
MinSpreadBps: decimal.NewFromInt(15), // 0.15% minimum spread
|
||||
MinProfit: decimal.NewFromInt(10), // $10 minimum profit
|
||||
MaxPositionSize: decimal.NewFromInt(5000), // $5k max per arb
|
||||
MaxTotalExposure: decimal.NewFromInt(50000),// $50k max total
|
||||
MinSpreadBps: decimal.NewFromInt(15), // 0.15% minimum spread
|
||||
MinProfit: decimal.NewFromInt(10), // $10 minimum profit
|
||||
MaxPositionSize: decimal.NewFromInt(5000), // $5k max per arb
|
||||
MaxTotalExposure: decimal.NewFromInt(50000), // $50k max total
|
||||
Symbols: []string{
|
||||
"BTC-USDC",
|
||||
"ETH-USDC",
|
||||
@@ -111,8 +111,8 @@ func main() {
|
||||
"SOL-USDC",
|
||||
},
|
||||
VenuePriority: []string{
|
||||
"lx_dex", // Fastest (native)
|
||||
"binance", // High liquidity
|
||||
"lx_dex", // Fastest (native)
|
||||
"binance", // High liquidity
|
||||
"mexc",
|
||||
"lx_amm",
|
||||
},
|
||||
|
||||
@@ -37,13 +37,13 @@ type MarketMaker struct {
|
||||
config MarketMakerConfig
|
||||
|
||||
// State
|
||||
mu sync.RWMutex
|
||||
position decimal.Decimal
|
||||
activeOrders map[string]trading.Order
|
||||
totalPnL decimal.Decimal
|
||||
tradesCount int
|
||||
lastMidPrice decimal.Decimal
|
||||
volatility decimal.Decimal
|
||||
mu sync.RWMutex
|
||||
position decimal.Decimal
|
||||
activeOrders map[string]trading.Order
|
||||
totalPnL decimal.Decimal
|
||||
tradesCount int
|
||||
lastMidPrice decimal.Decimal
|
||||
volatility decimal.Decimal
|
||||
|
||||
// Control
|
||||
ctx context.Context
|
||||
@@ -54,17 +54,17 @@ type MarketMaker struct {
|
||||
// MarketMakerConfig configures the market maker.
|
||||
type MarketMakerConfig struct {
|
||||
// Trading parameters
|
||||
Symbol string
|
||||
Venue string
|
||||
BaseSpreadBps decimal.Decimal // Base spread in basis points
|
||||
OrderSize decimal.Decimal // Size per order
|
||||
MaxPosition decimal.Decimal // Maximum position (absolute)
|
||||
NumLevels int // Number of price levels
|
||||
LevelSpacingBps decimal.Decimal // Spacing between levels
|
||||
Symbol string
|
||||
Venue string
|
||||
BaseSpreadBps decimal.Decimal // Base spread in basis points
|
||||
OrderSize decimal.Decimal // Size per order
|
||||
MaxPosition decimal.Decimal // Maximum position (absolute)
|
||||
NumLevels int // Number of price levels
|
||||
LevelSpacingBps decimal.Decimal // Spacing between levels
|
||||
|
||||
// Risk parameters
|
||||
MaxDailyLoss decimal.Decimal
|
||||
PositionSkewBps decimal.Decimal // Skew spread based on position
|
||||
MaxDailyLoss decimal.Decimal
|
||||
PositionSkewBps decimal.Decimal // Skew spread based on position
|
||||
|
||||
// Timing
|
||||
QuoteInterval time.Duration
|
||||
@@ -76,13 +76,13 @@ func DefaultMarketMakerConfig() MarketMakerConfig {
|
||||
return MarketMakerConfig{
|
||||
Symbol: "BTC-USDC",
|
||||
Venue: "lx_dex",
|
||||
BaseSpreadBps: decimal.NewFromInt(10), // 0.10%
|
||||
BaseSpreadBps: decimal.NewFromInt(10), // 0.10%
|
||||
OrderSize: decimal.NewFromFloat(0.01), // 0.01 BTC
|
||||
MaxPosition: decimal.NewFromFloat(0.1), // 0.1 BTC
|
||||
NumLevels: 3,
|
||||
LevelSpacingBps: decimal.NewFromInt(5), // 0.05%
|
||||
LevelSpacingBps: decimal.NewFromInt(5), // 0.05%
|
||||
MaxDailyLoss: decimal.NewFromFloat(100), // $100
|
||||
PositionSkewBps: decimal.NewFromInt(2), // Skew per position unit
|
||||
PositionSkewBps: decimal.NewFromInt(2), // Skew per position unit
|
||||
QuoteInterval: time.Second,
|
||||
CancelAgeSeconds: 30,
|
||||
}
|
||||
@@ -361,9 +361,9 @@ func main() {
|
||||
// Risk config
|
||||
config.Risk = trading.RiskConfig{
|
||||
Enabled: true,
|
||||
MaxPositionSize: decimal.NewFromFloat(1), // 1 BTC max position
|
||||
MaxOrderSize: decimal.NewFromFloat(0.1), // 0.1 BTC max order
|
||||
MaxDailyLoss: decimal.NewFromFloat(500), // $500 daily loss limit
|
||||
MaxPositionSize: decimal.NewFromFloat(1), // 1 BTC max position
|
||||
MaxOrderSize: decimal.NewFromFloat(0.1), // 0.1 BTC max order
|
||||
MaxDailyLoss: decimal.NewFromFloat(500), // $500 daily loss limit
|
||||
MaxOpenOrders: 50,
|
||||
}
|
||||
|
||||
|
||||
@@ -57,9 +57,9 @@ type RiskConfig struct {
|
||||
MaxPositionAsset map[string]decimal.Decimal
|
||||
|
||||
// PnL limits
|
||||
MaxDailyLoss decimal.Decimal
|
||||
MaxDrawdown decimal.Decimal
|
||||
ProfitTarget decimal.Decimal
|
||||
MaxDailyLoss decimal.Decimal
|
||||
MaxDrawdown decimal.Decimal
|
||||
ProfitTarget decimal.Decimal
|
||||
|
||||
// Alert thresholds
|
||||
AlertPositionPct decimal.Decimal // Alert when position > X% of limit
|
||||
|
||||
@@ -112,10 +112,10 @@ type VwapExecutor struct {
|
||||
|
||||
// VwapConfig configures VWAP execution.
|
||||
type VwapConfig struct {
|
||||
Symbol string
|
||||
Side Side
|
||||
TotalQuantity decimal.Decimal
|
||||
ParticipationRate decimal.Decimal // e.g., 0.1 = 10% of volume
|
||||
Symbol string
|
||||
Side Side
|
||||
TotalQuantity decimal.Decimal
|
||||
ParticipationRate decimal.Decimal // e.g., 0.1 = 10% of volume
|
||||
MaxDurationSeconds int
|
||||
}
|
||||
|
||||
@@ -397,12 +397,12 @@ type PovExecutor struct {
|
||||
|
||||
// PovConfig configures POV execution.
|
||||
type PovConfig struct {
|
||||
Symbol string
|
||||
Side Side
|
||||
TotalQuantity decimal.Decimal
|
||||
ParticipationRate decimal.Decimal // e.g., 0.05 = 5% of volume
|
||||
Symbol string
|
||||
Side Side
|
||||
TotalQuantity decimal.Decimal
|
||||
ParticipationRate decimal.Decimal // e.g., 0.05 = 5% of volume
|
||||
MaxDurationSeconds int
|
||||
MinOrderSize decimal.Decimal
|
||||
MinOrderSize decimal.Decimal
|
||||
}
|
||||
|
||||
// NewPovExecutor creates a POV executor.
|
||||
|
||||
+10
-10
@@ -252,16 +252,16 @@ func (t Trade) Value() decimal.Decimal {
|
||||
|
||||
// Ticker represents market ticker data.
|
||||
type Ticker struct {
|
||||
Symbol string
|
||||
Venue string
|
||||
Bid *decimal.Decimal
|
||||
Ask *decimal.Decimal
|
||||
Last *decimal.Decimal
|
||||
Volume24H *decimal.Decimal
|
||||
High24H *decimal.Decimal
|
||||
Low24H *decimal.Decimal
|
||||
Change24H *decimal.Decimal
|
||||
Timestamp time.Time
|
||||
Symbol string
|
||||
Venue string
|
||||
Bid *decimal.Decimal
|
||||
Ask *decimal.Decimal
|
||||
Last *decimal.Decimal
|
||||
Volume24H *decimal.Decimal
|
||||
High24H *decimal.Decimal
|
||||
Low24H *decimal.Decimal
|
||||
Change24H *decimal.Decimal
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
// MidPrice returns (bid + ask) / 2.
|
||||
|
||||
@@ -35,7 +35,7 @@ func (suite *IntegrationTestSuite) TearDownTest() {
|
||||
func (suite *IntegrationTestSuite) TestOrderLifecycle() {
|
||||
// Create orderbook
|
||||
ob := lx.NewOrderBook("BTC-USDT")
|
||||
|
||||
|
||||
// Place buy order
|
||||
buyOrder := &lx.Order{
|
||||
ID: 1,
|
||||
@@ -47,10 +47,10 @@ func (suite *IntegrationTestSuite) TestOrderLifecycle() {
|
||||
Size: 1.0,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
err := ob.PlaceOrder(buyOrder)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
|
||||
// Place sell order (should match)
|
||||
sellOrder := &lx.Order{
|
||||
ID: 2,
|
||||
@@ -62,12 +62,12 @@ func (suite *IntegrationTestSuite) TestOrderLifecycle() {
|
||||
Size: 0.5,
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
|
||||
|
||||
trades, err := ob.PlaceOrder(sellOrder)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Len(suite.T(), trades, 1)
|
||||
assert.Equal(suite.T(), 0.5, trades[0].Size)
|
||||
|
||||
|
||||
// Cancel remaining order
|
||||
err = ob.CancelOrder(1)
|
||||
assert.NoError(suite.T(), err)
|
||||
@@ -76,18 +76,18 @@ func (suite *IntegrationTestSuite) TestOrderLifecycle() {
|
||||
// Test margin trading flow
|
||||
func (suite *IntegrationTestSuite) TestMarginTradingFlow() {
|
||||
margin := lx.NewMarginEngine(suite.engine)
|
||||
|
||||
|
||||
// Open position
|
||||
position, err := margin.OpenPosition("user1", "BTC-USDT", lx.Buy, 0.1, 10)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotNil(suite.T(), position)
|
||||
assert.Equal(suite.T(), 10.0, position.Leverage)
|
||||
|
||||
|
||||
// Update position (price change)
|
||||
position.MarkPrice = 55000
|
||||
pnl := margin.CalculatePnL(position)
|
||||
assert.True(suite.T(), pnl > 0) // Profit since price went up
|
||||
|
||||
|
||||
// Close position
|
||||
err = margin.ClosePosition(position.ID)
|
||||
assert.NoError(suite.T(), err)
|
||||
@@ -96,23 +96,23 @@ func (suite *IntegrationTestSuite) TestMarginTradingFlow() {
|
||||
// Test vault operations
|
||||
func (suite *IntegrationTestSuite) TestVaultOperations() {
|
||||
manager := lx.NewVaultManager(suite.engine)
|
||||
|
||||
|
||||
// Create vault
|
||||
config := lx.VaultConfig{
|
||||
ID: "test-vault",
|
||||
Name: "Test Vault",
|
||||
MinDeposit: big.NewInt(1000),
|
||||
}
|
||||
|
||||
|
||||
vault, err := manager.CreateVault(config)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotNil(suite.T(), vault)
|
||||
|
||||
|
||||
// Deposit
|
||||
position, err := vault.Deposit("user1", big.NewInt(5000))
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotNil(suite.T(), position)
|
||||
|
||||
|
||||
// Withdraw
|
||||
amount, err := vault.Withdraw("user1", big.NewInt(2000))
|
||||
assert.NoError(suite.T(), err)
|
||||
@@ -122,16 +122,16 @@ func (suite *IntegrationTestSuite) TestVaultOperations() {
|
||||
// Test cross-chain bridge
|
||||
func (suite *IntegrationTestSuite) TestCrossChainBridge() {
|
||||
bridge := lx.NewCrossChainBridge()
|
||||
|
||||
|
||||
// Lock tokens on source chain
|
||||
txHash, err := bridge.LockTokens("ETH", "user1", 1.5, "BSC")
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotEmpty(suite.T(), txHash)
|
||||
|
||||
|
||||
// Verify on destination chain
|
||||
verified := bridge.VerifyTransaction(txHash)
|
||||
assert.True(suite.T(), verified)
|
||||
|
||||
|
||||
// Mint on destination
|
||||
err = bridge.MintTokens("BSC", "user1", 1.5, txHash)
|
||||
assert.NoError(suite.T(), err)
|
||||
@@ -141,25 +141,25 @@ func (suite *IntegrationTestSuite) TestCrossChainBridge() {
|
||||
func (suite *IntegrationTestSuite) TestLiquidationEngine() {
|
||||
engine := lx.NewLiquidationEngine()
|
||||
margin := lx.NewMarginEngine(suite.engine)
|
||||
|
||||
|
||||
// Create underwater position
|
||||
position := &lx.MarginPosition{
|
||||
ID: "pos1",
|
||||
User: "user1",
|
||||
Symbol: "BTC-USDT",
|
||||
Side: lx.Buy,
|
||||
Size: 1.0,
|
||||
EntryPrice: 50000,
|
||||
MarkPrice: 45000, // Price dropped
|
||||
Leverage: 20,
|
||||
Margin: 2500,
|
||||
ID: "pos1",
|
||||
User: "user1",
|
||||
Symbol: "BTC-USDT",
|
||||
Side: lx.Buy,
|
||||
Size: 1.0,
|
||||
EntryPrice: 50000,
|
||||
MarkPrice: 45000, // Price dropped
|
||||
Leverage: 20,
|
||||
Margin: 2500,
|
||||
MaintenanceMargin: 0.005,
|
||||
}
|
||||
|
||||
|
||||
// Check if liquidatable
|
||||
shouldLiquidate := engine.ShouldLiquidate(position)
|
||||
assert.True(suite.T(), shouldLiquidate)
|
||||
|
||||
|
||||
// Execute liquidation
|
||||
err := engine.Liquidate(position)
|
||||
assert.NoError(suite.T(), err)
|
||||
@@ -168,22 +168,22 @@ func (suite *IntegrationTestSuite) TestLiquidationEngine() {
|
||||
// Test oracle integration
|
||||
func (suite *IntegrationTestSuite) TestOracleIntegration() {
|
||||
oracle := lx.NewPriceOracle()
|
||||
|
||||
|
||||
// Add price sources
|
||||
oracle.AddSource("chainlink", lx.NewChainlinkSource())
|
||||
oracle.AddSource("pyth", lx.NewPythSource())
|
||||
|
||||
|
||||
// Get aggregated price
|
||||
price, err := oracle.GetPrice("BTC-USDT")
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.True(suite.T(), price > 0)
|
||||
|
||||
|
||||
// Test price deviation detection
|
||||
prices := map[string]float64{
|
||||
"chainlink": 50000,
|
||||
"pyth": 50100,
|
||||
}
|
||||
|
||||
|
||||
deviation := oracle.CalculateDeviation(prices)
|
||||
assert.True(suite.T(), deviation < 0.01) // Less than 1% deviation
|
||||
}
|
||||
@@ -191,20 +191,20 @@ func (suite *IntegrationTestSuite) TestOracleIntegration() {
|
||||
// Test staking operations
|
||||
func (suite *IntegrationTestSuite) TestStakingOperations() {
|
||||
staking := lx.NewStakingManager()
|
||||
|
||||
|
||||
// Stake tokens
|
||||
position, err := staking.Stake("pool1", "user1", big.NewInt(10000))
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.NotNil(suite.T(), position)
|
||||
|
||||
|
||||
// Earn rewards (simulate time passing)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
|
||||
// Claim rewards
|
||||
rewards, err := staking.ClaimRewards("user1", "pool1")
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.True(suite.T(), rewards.Cmp(big.NewInt(0)) > 0)
|
||||
|
||||
|
||||
// Unstake
|
||||
amount, err := staking.Unstake("pool1", "user1", big.NewInt(5000))
|
||||
assert.NoError(suite.T(), err)
|
||||
@@ -214,11 +214,11 @@ func (suite *IntegrationTestSuite) TestStakingOperations() {
|
||||
// Test high load scenario
|
||||
func (suite *IntegrationTestSuite) TestHighLoadScenario() {
|
||||
ob := lx.NewOrderBook("ETH-USDT")
|
||||
|
||||
|
||||
// Place many orders concurrently
|
||||
orderCount := 1000
|
||||
done := make(chan bool, orderCount)
|
||||
|
||||
|
||||
for i := 0; i < orderCount; i++ {
|
||||
go func(id int) {
|
||||
order := &lx.Order{
|
||||
@@ -235,17 +235,17 @@ func (suite *IntegrationTestSuite) TestHighLoadScenario() {
|
||||
done <- true
|
||||
}(i)
|
||||
}
|
||||
|
||||
|
||||
// Wait for all orders
|
||||
for i := 0; i < orderCount; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
|
||||
// Verify orderbook integrity
|
||||
bids, asks := ob.GetDepth(100)
|
||||
assert.True(suite.T(), len(bids) > 0)
|
||||
assert.True(suite.T(), len(asks) > 0)
|
||||
|
||||
|
||||
// Verify price ordering
|
||||
for i := 1; i < len(bids); i++ {
|
||||
assert.True(suite.T(), bids[i-1].Price >= bids[i].Price)
|
||||
@@ -261,4 +261,4 @@ func TestIntegrationSuite(t *testing.T) {
|
||||
t.Skip("Skipping integration tests in short mode")
|
||||
}
|
||||
suite.Run(t, new(IntegrationTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user