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

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

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

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

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

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

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

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

462 lines
12 KiB
Go

package api
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/luxfi/dex/pkg/dex"
"github.com/luxfi/log"
"github.com/stretchr/testify/assert"
)
// Test complete JSON-RPC lifecycle
func TestJSONRPCServer_CompleteLifecycle(t *testing.T) {
orderBook := dex.NewOrderBook("TEST")
level, _ := log.ToLevel("debug")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
// Add orders to orderbook
orderBook.AddOrder(&dex.Order{
ID: 1,
Type: dex.Limit,
Side: dex.Buy,
Price: 99,
Size: 10,
User: "buyer1",
Timestamp: time.Now(),
})
orderBook.AddOrder(&dex.Order{
ID: 2,
Type: dex.Limit,
Side: dex.Buy,
Price: 98,
Size: 20,
User: "buyer2",
Timestamp: time.Now(),
})
orderBook.AddOrder(&dex.Order{
ID: 3,
Type: dex.Limit,
Side: dex.Sell,
Price: 101,
Size: 15,
User: "seller1",
Timestamp: time.Now(),
})
orderBook.AddOrder(&dex.Order{
ID: 4,
Type: dex.Limit,
Side: dex.Sell,
Price: 102,
Size: 25,
User: "seller2",
Timestamp: time.Now(),
})
// Test getBestBid
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getBestBid","params":{},"id":1}`
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, float64(99), resp["result"])
// Test getBestAsk
reqBody = `{"jsonrpc":"2.0","method":"orderbook.getBestAsk","params":{},"id":2}`
req = httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w = httptest.NewRecorder()
server.ServeHTTP(w, req)
json.Unmarshal(w.Body.Bytes(), &resp)
assert.Equal(t, float64(101), resp["result"])
// Test getStats
reqBody = `{"jsonrpc":"2.0","method":"orderbook.getStats","params":{},"id":3}`
req = httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w = httptest.NewRecorder()
server.ServeHTTP(w, req)
json.Unmarshal(w.Body.Bytes(), &resp)
assert.NotNil(t, resp["result"])
stats := resp["result"].(map[string]interface{})
assert.Equal(t, "TEST", stats["symbol"])
}
// Test error handling for all error codes
func TestJSONRPCServer_ErrorCodes(t *testing.T) {
orderBook := dex.NewOrderBook("ERROR")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
testCases := []struct {
name string
reqBody string
expectedCode float64
expectedMsg string
}{
{
name: "Parse Error",
reqBody: `{invalid json`,
expectedCode: -32700,
expectedMsg: "Parse error",
},
{
name: "Invalid Request",
reqBody: `{"jsonrpc":"1.0","method":"test","id":1}`,
expectedCode: -32600,
expectedMsg: "Invalid Request",
},
{
name: "Method Not Found",
reqBody: `{"jsonrpc":"2.0","method":"invalid.method","id":1}`,
expectedCode: -32601,
expectedMsg: "Method not found",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(tc.reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(t, err)
if errorObj, ok := resp["error"].(map[string]interface{}); ok {
assert.Equal(t, tc.expectedCode, errorObj["code"])
assert.Equal(t, tc.expectedMsg, errorObj["message"])
}
})
}
}
// Test handling of different HTTP methods
func TestJSONRPCServer_HTTPMethods(t *testing.T) {
orderBook := dex.NewOrderBook("METHODS")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
methods := []string{"GET", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "TRACE", "CONNECT"}
for _, method := range methods {
t.Run(method, func(t *testing.T) {
req := httptest.NewRequest(method, "/rpc", nil)
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
})
}
// POST should work
t.Run("POST", func(t *testing.T) {
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getStats","id":1}`
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
})
}
// Test request/response headers
func TestJSONRPCServer_Headers(t *testing.T) {
orderBook := dex.NewOrderBook("HEADERS")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getStats","id":1}`
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
// Set various request headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "test-client")
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
// Check response headers
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
assert.Equal(t, http.StatusOK, w.Code)
}
// Test large request handling
func TestJSONRPCServer_LargeRequest(t *testing.T) {
orderBook := dex.NewOrderBook("LARGE")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
// Create a large params object
largeParams := make(map[string]interface{})
for i := 0; i < 1000; i++ {
largeParams[strings.Repeat("a", 100)] = strings.Repeat("b", 100)
}
reqObj := map[string]interface{}{
"jsonrpc": "2.0",
"method": "orderbook.getStats",
"params": largeParams,
"id": 1,
}
reqBytes, _ := json.Marshal(reqObj)
req := httptest.NewRequest("POST", "/rpc", bytes.NewBuffer(reqBytes))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// Test concurrent requests
func TestJSONRPCServer_ConcurrentRequests(t *testing.T) {
orderBook := dex.NewOrderBook("CONCURRENT")
// Add some orders
for i := 1; i <= 100; i++ {
orderBook.AddOrder(&dex.Order{
ID: uint64(i),
Type: dex.Limit,
Side: dex.Buy,
Price: float64(100 - i%10),
Size: 10,
User: "user",
Timestamp: time.Now(),
})
}
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
// Run concurrent requests
done := make(chan bool, 100)
for i := 0; i < 100; i++ {
go func(id int) {
var method string
switch id % 3 {
case 0:
method = "orderbook.getBestBid"
case 1:
method = "orderbook.getBestAsk"
case 2:
method = "orderbook.getStats"
}
reqBody := `{"jsonrpc":"2.0","method":"` + method + `","params":{},"id":` + string(rune(id)) + `}`
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
done <- true
}(i)
}
// Wait for all requests to complete
for i := 0; i < 100; i++ {
<-done
}
}
// Test request body size limits
func TestJSONRPCServer_BodySizeLimit(t *testing.T) {
orderBook := dex.NewOrderBook("SIZE")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
// Create a very large request body (>10MB)
largeBody := strings.Repeat("a", 11*1024*1024)
req := httptest.NewRequest("POST", "/rpc", strings.NewReader(largeBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
// Should handle large body gracefully
assert.NotNil(t, w.Code)
}
// Test empty request body
func TestJSONRPCServer_EmptyBody(t *testing.T) {
orderBook := dex.NewOrderBook("EMPTY")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
req := httptest.NewRequest("POST", "/rpc", nil)
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
if err == nil {
assert.NotNil(t, resp["error"])
}
}
// Test notification (no id field)
func TestJSONRPCServer_Notification(t *testing.T) {
orderBook := dex.NewOrderBook("NOTIFY")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
// Notification has no id field
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getStats","params":{}}`
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// For notifications, server may not return response
// or may return response without id
if w.Body.Len() > 0 {
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
_, hasID := resp["id"]
assert.True(t, !hasID || resp["id"] == nil)
}
}
// Test with various id types
func TestJSONRPCServer_IDTypes(t *testing.T) {
orderBook := dex.NewOrderBook("ID")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
testCases := []struct {
name string
id interface{}
idStr string
}{
{"Number ID", 123, "123"},
{"String ID", "test-id", `"test-id"`},
{"Null ID", nil, "null"},
{"Float ID", 123.456, "123.456"},
{"Negative ID", -1, "-1"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getStats","params":{},"id":` + tc.idStr + `}`
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &resp)
// ID should be echoed back
if tc.id != nil {
assert.NotNil(t, resp["id"])
}
})
}
}
// Test reading request body multiple times
func TestJSONRPCServer_BodyReading(t *testing.T) {
orderBook := dex.NewOrderBook("BODY")
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
// Wrap the server to read body twice
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// First read
body1, _ := io.ReadAll(r.Body)
r.Body.Close()
// Reset body for server
r.Body = io.NopCloser(bytes.NewBuffer(body1))
server := NewJSONRPCServer(orderBook, logger)
server.ServeHTTP(w, r)
})
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getStats","params":{},"id":1}`
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// Benchmark different methods
func BenchmarkJSONRPCServer_Methods(b *testing.B) {
orderBook := dex.NewOrderBook("BENCH")
// Add many orders
for i := 0; i < 10000; i++ {
orderBook.AddOrder(&dex.Order{
ID: uint64(i),
Type: dex.Limit,
Side: dex.Buy,
Price: float64(100 - i%100),
Size: 10,
User: "bench",
Timestamp: time.Now(),
})
orderBook.AddOrder(&dex.Order{
ID: uint64(i + 10000),
Type: dex.Limit,
Side: dex.Sell,
Price: float64(100 + i%100),
Size: 10,
User: "bench",
Timestamp: time.Now(),
})
}
level, _ := log.ToLevel("error")
logger := log.NewTestLogger(level)
server := NewJSONRPCServer(orderBook, logger)
b.Run("getBestBid", func(b *testing.B) {
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getBestBid","params":{},"id":1}`
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
}
})
b.Run("getBestAsk", func(b *testing.B) {
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getBestAsk","params":{},"id":1}`
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
}
})
b.Run("getStats", func(b *testing.B) {
reqBody := `{"jsonrpc":"2.0","method":"orderbook.getStats","params":{},"id":1}`
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("POST", "/rpc", bytes.NewBufferString(reqBody))
w := httptest.NewRecorder()
server.ServeHTTP(w, req)
}
})
}