mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
The matching engine (pkg/lx, 45.5K LOC incl. tests) and the deterministic core layered on it (pkg/dex) are one matcher spread across two packages (dex imported lx in 12 files; lx imported neither). Fold lx into dex and drop the "lx" name entirely — pkg/dex is now the single matcher package. Pure rename/fold, ZERO logic change. Behavior proven identical: the merged pkg/dex runs 457 top-level tests (431 former-lx + 26 former-dex) and 489 test funcs (468 + 21), all green — matching, batch (BatchMatchEqualsSequential, run-to-run determinism, tile boundary), GPU/CPU parity, truncation, and CL swap math all pass unchanged. What moved: - git mv all 101 pkg/lx/*.go + 1 .c into pkg/dex/ (package lx -> package dex). - lx's swap_test.go -> swap_math_test.go (only filename collision with dex's existing swap_test.go). - Rewrote imports repo-wide: 54 pkg/lx import lines removed; 42 external importers (api, client, consensus, dchain, dpdk, lxgpu, marketdata, test/, tests/) repointed to pkg/dex with lx.X -> dex.X; the 12 in-package dex files had lx.X -> bare X. 717 selector lines rewritten. Stale pkg/lx comment refs updated (pkg/lxgpu left intact). Collisions resolved (2, both package-level vars): - ErrNoLiquidity: lx's pool-level error (dexError, used only in swap.go, never referenced via qualifier) renamed ErrPoolNoLiquidity to match the existing ErrPool* convention; dex's router-level ErrNoLiquidity (public, has errors.Is tests) kept. - priceMultiplierBig: identical after fold (both big.NewInt(PriceMultiplier)); deduped by deleting dex's copy. Not folded — pkg/orderbook: it is NOT the GPU seam (that is orderbook_gpu.go, which lives in the engine and folded in here). pkg/orderbook is a zero-importer, placeholder-cgo island whose 13 core domain identifiers (Order, OrderBook, Trade, OrderType, OrderStatus, PriceLevel, Buy, Sell, Market, Limit, Filled, PartiallyFilled, NewOrderBook) collide incompatibly with the engine's. Folding it verbatim breaks the build; folding via rename reintroduces the exact parallel duplication this fold removes. Left for owner decision (recommend deletion as dead code) — see refactor notes. Follow-on (separate repos, not touched): 19 files under luxfi/dex import pkg/lx externally — lux/precompile/dex, lux/precompile-dex/dex, lux/consensus/examples/01-simple-bridge, lux/_consensus-v12537/examples, lux/cex/pkg/engine — repoint to pkg/dex after this lands. Verified: CGO_ENABLED=0 GOWORK=off go build ./... clean; go vet ./... clean; go test ./... -count=1 all green (22 ok / 0 fail). Co-authored-by: Hanzo Dev <[email protected]>
492 lines
12 KiB
Go
492 lines
12 KiB
Go
//go:build integration
|
|
// +build integration
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/luxfi/dex/pkg/dex"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
testJSONRPCURL = "http://localhost:8080"
|
|
testWSURL = "ws://localhost:8081"
|
|
testGRPCURL = "localhost:50051"
|
|
)
|
|
|
|
// Simple test client for integration testing
|
|
type TestClient struct {
|
|
baseURL string
|
|
}
|
|
|
|
func NewTestClient(url string) *TestClient {
|
|
return &TestClient{baseURL: url}
|
|
}
|
|
|
|
// TestFullOrderLifecycle tests complete order flow
|
|
func TestFullOrderLifecycle(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create client
|
|
c := NewTestClient(testJSONRPCURL)
|
|
|
|
// Connect to gRPC for best performance
|
|
err = c.ConnectGRPC(ctx)
|
|
if err != nil {
|
|
t.Logf("gRPC not available, using JSON-RPC: %v", err)
|
|
}
|
|
|
|
// Test 1: Place a buy limit order
|
|
buyOrder := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideBuy,
|
|
Price: 50000.00,
|
|
Size: 0.1,
|
|
UserID: "test-user-1",
|
|
ClientID: "test-order-001",
|
|
TimeInForce: client.TimeInForceGTC,
|
|
}
|
|
|
|
buyResp, err := c.PlaceOrder(ctx, buyOrder)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, buyResp.OrderID)
|
|
assert.Equal(t, "open", buyResp.Status)
|
|
|
|
// Test 2: Place a matching sell order
|
|
sellOrder := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideSell,
|
|
Price: 50000.00,
|
|
Size: 0.1,
|
|
UserID: "test-user-2",
|
|
ClientID: "test-order-002",
|
|
TimeInForce: client.TimeInForceGTC,
|
|
}
|
|
|
|
sellResp, err := c.PlaceOrder(ctx, sellOrder)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, sellResp.OrderID)
|
|
|
|
// Orders should match immediately
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Test 3: Verify trades were created
|
|
trades, err := c.GetTrades(ctx, "BTC-USD", 10)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, trades)
|
|
|
|
// Find our trade
|
|
var ourTrade *client.Trade
|
|
for _, trade := range trades {
|
|
if trade.BuyOrderID == buyResp.OrderID || trade.SellOrderID == sellResp.OrderID {
|
|
ourTrade = trade
|
|
break
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, ourTrade, "Trade not found")
|
|
assert.Equal(t, 50000.0, ourTrade.Price)
|
|
assert.Equal(t, 0.1, ourTrade.Size)
|
|
}
|
|
|
|
// TestOrderTypes tests different order types
|
|
func TestOrderTypes(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
c, err := client.NewClient(client.WithJSONRPCURL(testJSONRPCURL))
|
|
require.NoError(t, err)
|
|
defer c.Disconnect()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
orderType client.OrderType
|
|
side client.OrderSide
|
|
price float64
|
|
size float64
|
|
}{
|
|
{"Limit Buy", client.OrderTypeLimit, client.OrderSideBuy, 49000, 0.05},
|
|
{"Limit Sell", client.OrderTypeLimit, client.OrderSideSell, 51000, 0.05},
|
|
{"Market Buy", client.OrderTypeMarket, client.OrderSideBuy, 0, 0.02},
|
|
{"Market Sell", client.OrderTypeMarket, client.OrderSideSell, 0, 0.02},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
order := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: tc.orderType,
|
|
Side: tc.side,
|
|
Price: tc.price,
|
|
Size: tc.size,
|
|
UserID: fmt.Sprintf("test-user-%s", tc.name),
|
|
}
|
|
|
|
resp, err := c.PlaceOrder(ctx, order)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, resp.OrderID)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWebSocketStreaming tests real-time data streaming
|
|
func TestWebSocketStreaming(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
c, err := client.NewClient(client.WithWebSocketURL(testWSURL))
|
|
require.NoError(t, err)
|
|
defer c.Disconnect()
|
|
|
|
// Connect WebSocket
|
|
err = c.ConnectWebSocket(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Subscribe to order book updates
|
|
orderBookChan := make(chan *client.OrderBook, 10)
|
|
err = c.SubscribeOrderBook("BTC-USD", func(ob *client.OrderBook) {
|
|
select {
|
|
case orderBookChan <- ob:
|
|
default:
|
|
}
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Subscribe to trades
|
|
tradeChan := make(chan *client.Trade, 10)
|
|
err = c.SubscribeTrades("BTC-USD", func(trade *client.Trade) {
|
|
select {
|
|
case tradeChan <- trade:
|
|
default:
|
|
}
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Place orders to generate events
|
|
go func() {
|
|
for i := 0; i < 5; i++ {
|
|
order := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideBuy,
|
|
Price: 49000 + float64(i*100),
|
|
Size: 0.01,
|
|
UserID: fmt.Sprintf("ws-test-user-%d", i),
|
|
}
|
|
c.PlaceOrder(ctx, order)
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
// Wait for events
|
|
orderBookReceived := false
|
|
tradeReceived := false
|
|
|
|
for {
|
|
select {
|
|
case ob := <-orderBookChan:
|
|
assert.NotNil(t, ob)
|
|
orderBookReceived = true
|
|
|
|
case trade := <-tradeChan:
|
|
assert.NotNil(t, trade)
|
|
tradeReceived = true
|
|
|
|
case <-ctx.Done():
|
|
assert.True(t, orderBookReceived, "No order book updates received")
|
|
return
|
|
}
|
|
|
|
if orderBookReceived && tradeReceived {
|
|
return // Success
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConcurrentOrders tests high-concurrency order placement
|
|
func TestConcurrentOrders(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
c, err := client.NewClient(client.WithJSONRPCURL(testJSONRPCURL))
|
|
require.NoError(t, err)
|
|
defer c.Disconnect()
|
|
|
|
numWorkers := 10
|
|
ordersPerWorker := 100
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, numWorkers*ordersPerWorker)
|
|
orderIDs := make(chan uint64, numWorkers*ordersPerWorker)
|
|
|
|
// Launch workers
|
|
for w := 0; w < numWorkers; w++ {
|
|
wg.Add(1)
|
|
go func(workerID int) {
|
|
defer wg.Done()
|
|
|
|
for i := 0; i < ordersPerWorker; i++ {
|
|
order := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSide(i % 2), // Alternate buy/sell
|
|
Price: 50000 + float64((i%20-10)*10),
|
|
Size: 0.001,
|
|
UserID: fmt.Sprintf("worker-%d", workerID),
|
|
}
|
|
|
|
resp, err := c.PlaceOrder(ctx, order)
|
|
if err != nil {
|
|
errors <- err
|
|
} else {
|
|
orderIDs <- resp.OrderID
|
|
}
|
|
}
|
|
}(w)
|
|
}
|
|
|
|
// Wait for completion
|
|
wg.Wait()
|
|
close(errors)
|
|
close(orderIDs)
|
|
|
|
// Check results
|
|
errorCount := 0
|
|
for err := range errors {
|
|
t.Logf("Order error: %v", err)
|
|
errorCount++
|
|
}
|
|
|
|
orderCount := 0
|
|
uniqueOrders := make(map[uint64]bool)
|
|
for id := range orderIDs {
|
|
orderCount++
|
|
uniqueOrders[id] = true
|
|
}
|
|
|
|
assert.Equal(t, 0, errorCount, "Should have no errors")
|
|
assert.Equal(t, numWorkers*ordersPerWorker, orderCount, "All orders should succeed")
|
|
assert.Equal(t, orderCount, len(uniqueOrders), "All order IDs should be unique")
|
|
}
|
|
|
|
// TestOrderBookDepth tests order book depth and aggregation
|
|
func TestOrderBookDepth(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
c, err := client.NewClient(client.WithJSONRPCURL(testJSONRPCURL))
|
|
require.NoError(t, err)
|
|
defer c.Disconnect()
|
|
|
|
// Place multiple orders at different price levels
|
|
for i := 0; i < 20; i++ {
|
|
buyOrder := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideBuy,
|
|
Price: 49900 - float64(i*10),
|
|
Size: 0.1 + float64(i)*0.01,
|
|
UserID: fmt.Sprintf("depth-test-buy-%d", i),
|
|
}
|
|
|
|
sellOrder := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideSell,
|
|
Price: 50100 + float64(i*10),
|
|
Size: 0.1 + float64(i)*0.01,
|
|
UserID: fmt.Sprintf("depth-test-sell-%d", i),
|
|
}
|
|
|
|
_, err = c.PlaceOrder(ctx, buyOrder)
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.PlaceOrder(ctx, sellOrder)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Get order book with depth
|
|
orderBook, err := c.GetOrderBook(ctx, "BTC-USD", 10)
|
|
require.NoError(t, err)
|
|
|
|
assert.LessOrEqual(t, len(orderBook.Bids), 10)
|
|
assert.LessOrEqual(t, len(orderBook.Asks), 10)
|
|
|
|
// Verify price ordering
|
|
for i := 1; i < len(orderBook.Bids); i++ {
|
|
assert.GreaterOrEqual(t, orderBook.Bids[i-1].Price, orderBook.Bids[i].Price,
|
|
"Bids should be in descending price order")
|
|
}
|
|
|
|
for i := 1; i < len(orderBook.Asks); i++ {
|
|
assert.LessOrEqual(t, orderBook.Asks[i-1].Price, orderBook.Asks[i].Price,
|
|
"Asks should be in ascending price order")
|
|
}
|
|
|
|
// Verify spread
|
|
if len(orderBook.Bids) > 0 && len(orderBook.Asks) > 0 {
|
|
spread := orderBook.Spread()
|
|
assert.Greater(t, spread, 0.0, "Spread should be positive")
|
|
}
|
|
}
|
|
|
|
// TestOrderCancellation tests order cancellation
|
|
func TestOrderCancellation(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
c, err := client.NewClient(client.WithJSONRPCURL(testJSONRPCURL))
|
|
require.NoError(t, err)
|
|
defer c.Disconnect()
|
|
|
|
// Place an order
|
|
order := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideBuy,
|
|
Price: 45000, // Far from market to avoid immediate fill
|
|
Size: 1.0,
|
|
UserID: "cancel-test-user",
|
|
ClientID: "cancel-test-001",
|
|
}
|
|
|
|
resp, err := c.PlaceOrder(ctx, order)
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, resp.OrderID)
|
|
|
|
// Cancel the order
|
|
err = c.CancelOrder(ctx, resp.OrderID)
|
|
require.NoError(t, err)
|
|
|
|
// Try to cancel again (should fail or be idempotent)
|
|
err = c.CancelOrder(ctx, resp.OrderID)
|
|
// This may or may not error depending on implementation
|
|
if err != nil {
|
|
t.Logf("Double cancel error (expected): %v", err)
|
|
}
|
|
}
|
|
|
|
// TestNodeHealth tests node health endpoints
|
|
func TestNodeHealth(t *testing.T) {
|
|
// Test JSON-RPC health
|
|
resp, err := http.Get(testJSONRPCURL + "/health")
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
var health map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&health)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "healthy", health["status"])
|
|
assert.NotZero(t, health["block"])
|
|
assert.NotZero(t, health["orders"])
|
|
}
|
|
|
|
// TestPerformanceMetrics tests that performance meets requirements
|
|
func TestPerformanceMetrics(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
c, err := client.NewClient(
|
|
client.WithJSONRPCURL(testJSONRPCURL),
|
|
client.WithGRPCURL(testGRPCURL),
|
|
)
|
|
require.NoError(t, err)
|
|
defer c.Disconnect()
|
|
|
|
// Try to connect to gRPC for best performance
|
|
c.ConnectGRPC(ctx)
|
|
|
|
// Measure order placement latency
|
|
numOrders := 1000
|
|
latencies := make([]time.Duration, numOrders)
|
|
|
|
for i := 0; i < numOrders; i++ {
|
|
order := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSide(i % 2),
|
|
Price: 50000 + float64((i%100)-50),
|
|
Size: 0.001,
|
|
UserID: "perf-test",
|
|
}
|
|
|
|
start := time.Now()
|
|
_, err := c.PlaceOrder(ctx, order)
|
|
latencies[i] = time.Since(start)
|
|
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Calculate statistics
|
|
var totalLatency time.Duration
|
|
var maxLatency time.Duration
|
|
for _, lat := range latencies {
|
|
totalLatency += lat
|
|
if lat > maxLatency {
|
|
maxLatency = lat
|
|
}
|
|
}
|
|
|
|
avgLatency := totalLatency / time.Duration(numOrders)
|
|
|
|
t.Logf("Performance Metrics:")
|
|
t.Logf(" Average latency: %v", avgLatency)
|
|
t.Logf(" Max latency: %v", maxLatency)
|
|
t.Logf(" Throughput: %.0f orders/sec", float64(numOrders)/totalLatency.Seconds())
|
|
|
|
// Assert performance requirements
|
|
assert.Less(t, avgLatency, 10*time.Millisecond, "Average latency should be < 10ms")
|
|
assert.Less(t, maxLatency, 100*time.Millisecond, "Max latency should be < 100ms")
|
|
}
|
|
|
|
// TestErrorHandling tests error scenarios
|
|
func TestErrorHandling(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
c, err := client.NewClient(client.WithJSONRPCURL(testJSONRPCURL))
|
|
require.NoError(t, err)
|
|
defer c.Disconnect()
|
|
|
|
// Test invalid order (negative price)
|
|
invalidOrder := &client.Order{
|
|
Symbol: "BTC-USD",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideBuy,
|
|
Price: -100, // Invalid
|
|
Size: 0.1,
|
|
UserID: "error-test",
|
|
}
|
|
|
|
_, err = c.PlaceOrder(ctx, invalidOrder)
|
|
assert.Error(t, err, "Should reject negative price")
|
|
|
|
// Test invalid symbol
|
|
invalidSymbol := &client.Order{
|
|
Symbol: "INVALID-PAIR",
|
|
Type: client.OrderTypeLimit,
|
|
Side: client.OrderSideBuy,
|
|
Price: 50000,
|
|
Size: 0.1,
|
|
UserID: "error-test",
|
|
}
|
|
|
|
_, err = c.PlaceOrder(ctx, invalidSymbol)
|
|
// May or may not error depending on implementation
|
|
if err != nil {
|
|
t.Logf("Invalid symbol error (expected): %v", err)
|
|
}
|
|
|
|
// Test cancelling non-existent order
|
|
err = c.CancelOrder(ctx, 999999999)
|
|
assert.Error(t, err, "Should error when cancelling non-existent order")
|
|
}
|