mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
991 lines
29 KiB
Go
991 lines
29 KiB
Go
package gateway
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ==========================================================================
|
|
// Order Types & Validation Tests
|
|
// ==========================================================================
|
|
|
|
func TestOrderKindValidation(t *testing.T) {
|
|
base := CreateOrderRequest{
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
|
|
t.Run("invalid type", func(t *testing.T) {
|
|
req := base
|
|
req.Type = "market"
|
|
if err := req.Validate(); err != ErrInvalidOrderKind {
|
|
t.Fatalf("expected ErrInvalidOrderKind, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("empty type", func(t *testing.T) {
|
|
req := base
|
|
req.Type = ""
|
|
if err := req.Validate(); err != ErrInvalidOrderKind {
|
|
t.Fatalf("expected ErrInvalidOrderKind, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestLimitOrderValidation(t *testing.T) {
|
|
valid := CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000000000000000",
|
|
LimitPrice: "2500000000000000000000",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
|
|
t.Run("valid limit order", func(t *testing.T) {
|
|
if err := valid.Validate(); err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing tokenIn", func(t *testing.T) {
|
|
req := valid
|
|
req.TokenIn = ""
|
|
if err := req.Validate(); err != ErrMissingTokenIn {
|
|
t.Fatalf("expected ErrMissingTokenIn, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing tokenOut", func(t *testing.T) {
|
|
req := valid
|
|
req.TokenOut = ""
|
|
if err := req.Validate(); err != ErrMissingTokenOut {
|
|
t.Fatalf("expected ErrMissingTokenOut, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("invalid address format", func(t *testing.T) {
|
|
req := valid
|
|
req.TokenIn = "not_an_address"
|
|
if err := req.Validate(); err != ErrInvalidAddress {
|
|
t.Fatalf("expected ErrInvalidAddress, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("zero amountIn", func(t *testing.T) {
|
|
req := valid
|
|
req.AmountIn = "0"
|
|
if err := req.Validate(); err != ErrMissingAmountIn {
|
|
t.Fatalf("expected ErrMissingAmountIn, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("negative amountIn", func(t *testing.T) {
|
|
req := valid
|
|
req.AmountIn = "-100"
|
|
if err := req.Validate(); err != ErrMissingAmountIn {
|
|
t.Fatalf("expected ErrMissingAmountIn, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("expired deadline", func(t *testing.T) {
|
|
req := valid
|
|
req.Deadline = time.Now().Add(-1 * time.Minute).Unix()
|
|
if err := req.Validate(); err != ErrInvalidDeadline {
|
|
t.Fatalf("expected ErrInvalidDeadline, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing limit price", func(t *testing.T) {
|
|
req := valid
|
|
req.LimitPrice = ""
|
|
if err := req.Validate(); err != ErrMissingLimitPrice {
|
|
t.Fatalf("expected ErrMissingLimitPrice, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("zero limit price", func(t *testing.T) {
|
|
req := valid
|
|
req.LimitPrice = "0"
|
|
if err := req.Validate(); err != ErrInvalidLimitPrice {
|
|
t.Fatalf("expected ErrInvalidLimitPrice, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing recipient", func(t *testing.T) {
|
|
req := valid
|
|
req.Recipient = ""
|
|
if err := req.Validate(); err != ErrMissingRecipient {
|
|
t.Fatalf("expected ErrMissingRecipient, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDutchOrderValidation(t *testing.T) {
|
|
now := time.Now()
|
|
valid := CreateOrderRequest{
|
|
Type: OrderKindDutch,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000000000000000",
|
|
StartAmount: "2500000000000000000000",
|
|
EndAmount: "2400000000000000000000",
|
|
DecayStartTime: now.Add(1 * time.Minute).Unix(),
|
|
DecayEndTime: now.Add(10 * time.Minute).Unix(),
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: now.Add(1 * time.Hour).Unix(),
|
|
}
|
|
|
|
t.Run("valid dutch order", func(t *testing.T) {
|
|
if err := valid.Validate(); err != nil {
|
|
t.Fatalf("expected nil, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing startAmount", func(t *testing.T) {
|
|
req := valid
|
|
req.StartAmount = ""
|
|
if err := req.Validate(); err != ErrMissingStartAmount {
|
|
t.Fatalf("expected ErrMissingStartAmount, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing endAmount", func(t *testing.T) {
|
|
req := valid
|
|
req.EndAmount = ""
|
|
if err := req.Validate(); err != ErrMissingEndAmount {
|
|
t.Fatalf("expected ErrMissingEndAmount, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing decayStartTime", func(t *testing.T) {
|
|
req := valid
|
|
req.DecayStartTime = 0
|
|
if err := req.Validate(); err != ErrMissingDecayStart {
|
|
t.Fatalf("expected ErrMissingDecayStart, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("missing decayEndTime", func(t *testing.T) {
|
|
req := valid
|
|
req.DecayEndTime = 0
|
|
if err := req.Validate(); err != ErrMissingDecayEnd {
|
|
t.Fatalf("expected ErrMissingDecayEnd, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("decayEnd before decayStart", func(t *testing.T) {
|
|
req := valid
|
|
req.DecayEndTime = req.DecayStartTime - 1
|
|
if err := req.Validate(); err != ErrInvalidDecayWindow {
|
|
t.Fatalf("expected ErrInvalidDecayWindow, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("startAmount less than endAmount", func(t *testing.T) {
|
|
req := valid
|
|
req.StartAmount = "100"
|
|
req.EndAmount = "200"
|
|
if err := req.Validate(); err != ErrInvalidDutchAmounts {
|
|
t.Fatalf("expected ErrInvalidDutchAmounts, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("equal start and end amounts allowed", func(t *testing.T) {
|
|
req := valid
|
|
req.StartAmount = "2500000000000000000000"
|
|
req.EndAmount = "2500000000000000000000"
|
|
if err := req.Validate(); err != nil {
|
|
t.Fatalf("expected nil for equal start/end amounts, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHexAddressValidation(t *testing.T) {
|
|
tests := []struct {
|
|
addr string
|
|
valid bool
|
|
}{
|
|
{"0x0000000000000000000000000000000000000001", true},
|
|
{"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF", true},
|
|
{"0X0000000000000000000000000000000000000001", true},
|
|
{"", false},
|
|
{"0x", false},
|
|
{"0x000000000000000000000000000000000000000", false}, // 39 chars
|
|
{"0x00000000000000000000000000000000000000001", false}, // 41 chars
|
|
{"0xGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", false}, // invalid hex
|
|
{"000000000000000000000000000000000000000001", false}, // no 0x
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.addr, func(t *testing.T) {
|
|
got := isValidHexAddress(tt.addr)
|
|
if got != tt.valid {
|
|
t.Fatalf("isValidHexAddress(%q) = %v, want %v", tt.addr, got, tt.valid)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Order State Machine Tests
|
|
// ==========================================================================
|
|
|
|
func TestOrderStatusTransitions(t *testing.T) {
|
|
tests := []struct {
|
|
from OrderStatus
|
|
to OrderStatus
|
|
valid bool
|
|
}{
|
|
// Valid transitions from open
|
|
{OrderStatusOpen, OrderStatusPartiallyFilled, true},
|
|
{OrderStatusOpen, OrderStatusFilled, true},
|
|
{OrderStatusOpen, OrderStatusExpired, true},
|
|
{OrderStatusOpen, OrderStatusCancelled, true},
|
|
|
|
// Valid transitions from partially_filled
|
|
{OrderStatusPartiallyFilled, OrderStatusPartiallyFilled, true},
|
|
{OrderStatusPartiallyFilled, OrderStatusFilled, true},
|
|
{OrderStatusPartiallyFilled, OrderStatusExpired, true},
|
|
{OrderStatusPartiallyFilled, OrderStatusCancelled, true},
|
|
|
|
// Invalid: terminal states cannot transition
|
|
{OrderStatusFilled, OrderStatusOpen, false},
|
|
{OrderStatusFilled, OrderStatusCancelled, false},
|
|
{OrderStatusExpired, OrderStatusOpen, false},
|
|
{OrderStatusExpired, OrderStatusFilled, false},
|
|
{OrderStatusCancelled, OrderStatusOpen, false},
|
|
{OrderStatusCancelled, OrderStatusFilled, false},
|
|
|
|
// Invalid: cannot go backwards
|
|
{OrderStatusOpen, OrderStatusOpen, false},
|
|
{OrderStatusPartiallyFilled, OrderStatusOpen, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
name := fmt.Sprintf("%s->%s", tt.from, tt.to)
|
|
t.Run(name, func(t *testing.T) {
|
|
err := ValidateTransition(tt.from, tt.to)
|
|
if tt.valid && err != nil {
|
|
t.Fatalf("expected valid transition, got error: %v", err)
|
|
}
|
|
if !tt.valid && err == nil {
|
|
t.Fatal("expected invalid transition, got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsTerminal(t *testing.T) {
|
|
terminal := []OrderStatus{OrderStatusFilled, OrderStatusExpired, OrderStatusCancelled}
|
|
nonTerminal := []OrderStatus{OrderStatusOpen, OrderStatusPartiallyFilled}
|
|
|
|
for _, s := range terminal {
|
|
if !s.IsTerminal() {
|
|
t.Fatalf("expected %s to be terminal", s)
|
|
}
|
|
}
|
|
for _, s := range nonTerminal {
|
|
if s.IsTerminal() {
|
|
t.Fatalf("expected %s to be non-terminal", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Order Store Tests
|
|
// ==========================================================================
|
|
|
|
func TestOrderStoreCreate(t *testing.T) {
|
|
store := NewOrderStore()
|
|
|
|
req := &CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000000000000000",
|
|
LimitPrice: "2500000000000000000000",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
ChainID: 96369,
|
|
}
|
|
|
|
order := store.Create(req)
|
|
|
|
if order.ID == "" {
|
|
t.Fatal("order ID should not be empty")
|
|
}
|
|
if order.Status != OrderStatusOpen {
|
|
t.Fatalf("expected status open, got %s", order.Status)
|
|
}
|
|
if order.Kind != OrderKindLimit {
|
|
t.Fatalf("expected kind limit, got %s", order.Kind)
|
|
}
|
|
if order.AmountIn.Cmp(big.NewInt(1000000000000000000)) != 0 {
|
|
t.Fatalf("unexpected amountIn: %s", order.AmountIn)
|
|
}
|
|
if order.FilledAmount.Sign() != 0 {
|
|
t.Fatal("filled amount should be 0")
|
|
}
|
|
if store.Count() != 1 {
|
|
t.Fatalf("expected count 1, got %d", store.Count())
|
|
}
|
|
}
|
|
|
|
func TestOrderStoreGet(t *testing.T) {
|
|
store := NewOrderStore()
|
|
|
|
req := &CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
|
|
created := store.Create(req)
|
|
got := store.Get(created.ID)
|
|
|
|
if got == nil {
|
|
t.Fatal("expected order, got nil")
|
|
}
|
|
if got.ID != created.ID {
|
|
t.Fatalf("expected ID %s, got %s", created.ID, got.ID)
|
|
}
|
|
|
|
// Non-existent
|
|
if store.Get("nonexistent") != nil {
|
|
t.Fatal("expected nil for nonexistent order")
|
|
}
|
|
}
|
|
|
|
func TestOrderStoreListByOwner(t *testing.T) {
|
|
store := NewOrderStore()
|
|
addr := "0x0000000000000000000000000000000000000003"
|
|
|
|
// Create 3 orders for the same owner
|
|
for i := 0; i < 3; i++ {
|
|
store.Create(&CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000",
|
|
LimitPrice: "2500",
|
|
Recipient: addr,
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
})
|
|
}
|
|
|
|
// Create 1 order for a different owner
|
|
store.Create(&CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000099",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
})
|
|
|
|
orders := store.ListByOwner(addr, "")
|
|
if len(orders) != 3 {
|
|
t.Fatalf("expected 3 orders, got %d", len(orders))
|
|
}
|
|
|
|
// Filter by status
|
|
orders = store.ListByOwner(addr, OrderStatusOpen)
|
|
if len(orders) != 3 {
|
|
t.Fatalf("expected 3 open orders, got %d", len(orders))
|
|
}
|
|
|
|
orders = store.ListByOwner(addr, OrderStatusFilled)
|
|
if len(orders) != 0 {
|
|
t.Fatalf("expected 0 filled orders, got %d", len(orders))
|
|
}
|
|
|
|
// Case insensitivity
|
|
orders = store.ListByOwner("0x0000000000000000000000000000000000000003", "")
|
|
if len(orders) != 3 {
|
|
t.Fatalf("expected 3 orders with uppercase addr, got %d", len(orders))
|
|
}
|
|
}
|
|
|
|
func TestOrderStoreCancel(t *testing.T) {
|
|
store := NewOrderStore()
|
|
|
|
order := store.Create(&CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
})
|
|
|
|
cancelled, err := store.Cancel(order.ID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if cancelled.Status != OrderStatusCancelled {
|
|
t.Fatalf("expected cancelled status, got %s", cancelled.Status)
|
|
}
|
|
|
|
// Cannot cancel again
|
|
_, err = store.Cancel(order.ID)
|
|
if err != ErrInvalidTransition {
|
|
t.Fatalf("expected ErrInvalidTransition, got %v", err)
|
|
}
|
|
|
|
// Cannot cancel nonexistent
|
|
_, err = store.Cancel("nonexistent")
|
|
if err != ErrOrderNotFoundGateway {
|
|
t.Fatalf("expected ErrOrderNotFoundGateway, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOrderStoreRecordFill(t *testing.T) {
|
|
store := NewOrderStore()
|
|
|
|
order := store.Create(&CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
})
|
|
|
|
t.Run("partial fill", func(t *testing.T) {
|
|
filled, err := store.RecordFill(order.ID, big.NewInt(300), big.NewInt(2500), "0xabc")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if filled.Status != OrderStatusPartiallyFilled {
|
|
t.Fatalf("expected partially_filled, got %s", filled.Status)
|
|
}
|
|
if filled.FilledAmount.Cmp(big.NewInt(300)) != 0 {
|
|
t.Fatalf("expected filled 300, got %s", filled.FilledAmount)
|
|
}
|
|
if len(filled.Fills) != 1 {
|
|
t.Fatalf("expected 1 fill, got %d", len(filled.Fills))
|
|
}
|
|
})
|
|
|
|
t.Run("complete fill", func(t *testing.T) {
|
|
filled, err := store.RecordFill(order.ID, big.NewInt(700), big.NewInt(2500), "0xdef")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if filled.Status != OrderStatusFilled {
|
|
t.Fatalf("expected filled, got %s", filled.Status)
|
|
}
|
|
if filled.FilledAmount.Cmp(big.NewInt(1000)) != 0 {
|
|
t.Fatalf("expected filled 1000, got %s", filled.FilledAmount)
|
|
}
|
|
if len(filled.Fills) != 2 {
|
|
t.Fatalf("expected 2 fills, got %d", len(filled.Fills))
|
|
}
|
|
})
|
|
|
|
t.Run("fill after filled", func(t *testing.T) {
|
|
_, err := store.RecordFill(order.ID, big.NewInt(1), big.NewInt(2500), "0xghi")
|
|
if err != ErrOrderTerminal {
|
|
t.Fatalf("expected ErrOrderTerminal, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("fill exceeds remaining", func(t *testing.T) {
|
|
order2 := store.Create(&CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "100",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
})
|
|
_, err := store.RecordFill(order2.ID, big.NewInt(101), big.NewInt(2500), "0x")
|
|
if err != ErrFillExceedsRemaining {
|
|
t.Fatalf("expected ErrFillExceedsRemaining, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("fill nonexistent", func(t *testing.T) {
|
|
_, err := store.RecordFill("nonexistent", big.NewInt(1), nil, "")
|
|
if err != ErrOrderNotFoundGateway {
|
|
t.Fatalf("expected ErrOrderNotFoundGateway, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("fill with nil amount", func(t *testing.T) {
|
|
_, err := store.RecordFill(order.ID, nil, nil, "")
|
|
if err != ErrMissingAmountIn {
|
|
t.Fatalf("expected ErrMissingAmountIn, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOrderStoreExpireStale(t *testing.T) {
|
|
store := NewOrderStore()
|
|
|
|
// Create order with past deadline
|
|
store.Create(&CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
})
|
|
|
|
// Manually set a past deadline on the order
|
|
store.mu.Lock()
|
|
for _, order := range store.orders {
|
|
order.Deadline = time.Now().Add(-1 * time.Minute).Unix()
|
|
}
|
|
store.mu.Unlock()
|
|
|
|
expired := store.ExpireStale()
|
|
if expired != 1 {
|
|
t.Fatalf("expected 1 expired, got %d", expired)
|
|
}
|
|
|
|
// Running again should expire 0 (already terminal)
|
|
expired = store.ExpireStale()
|
|
if expired != 0 {
|
|
t.Fatalf("expected 0 expired on second run, got %d", expired)
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// HTTP Handler Tests
|
|
// ==========================================================================
|
|
|
|
func newTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
registry := NewRegistry()
|
|
router := NewRouter(registry, true)
|
|
cfg := DefaultServerConfig()
|
|
s := NewServer(router, cfg)
|
|
return s
|
|
}
|
|
|
|
func TestCreateLimitOrderHandler(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
body := CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000000000000000",
|
|
LimitPrice: "2500000000000000000000",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
ChainID: 96369,
|
|
}
|
|
|
|
b, _ := json.Marshal(body)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
s.handleOrder(rr, req)
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp apiResponse
|
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to decode response: %v", err)
|
|
}
|
|
if !resp.Success {
|
|
t.Fatalf("expected success, got error: %s", resp.Error)
|
|
}
|
|
|
|
// Decode the data field
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var orderResp CreateOrderResponse
|
|
json.Unmarshal(dataBytes, &orderResp)
|
|
|
|
if orderResp.OrderID == "" {
|
|
t.Fatal("expected order ID in response")
|
|
}
|
|
if orderResp.Status != OrderStatusOpen {
|
|
t.Fatalf("expected status open, got %s", orderResp.Status)
|
|
}
|
|
if orderResp.Tx == nil {
|
|
t.Fatal("expected unsigned tx for limit order")
|
|
}
|
|
if orderResp.EIP712 != nil {
|
|
t.Fatal("did not expect EIP712 data for limit order")
|
|
}
|
|
if orderResp.Tx.To != "0x0000000000000000000000000000000000009020" {
|
|
t.Fatalf("expected CLOB address, got %s", orderResp.Tx.To)
|
|
}
|
|
if orderResp.Tx.ChainID != 96369 {
|
|
t.Fatalf("expected chainId 96369, got %d", orderResp.Tx.ChainID)
|
|
}
|
|
}
|
|
|
|
func TestCreateDutchOrderHandler(t *testing.T) {
|
|
s := newTestServer(t)
|
|
now := time.Now()
|
|
|
|
body := CreateOrderRequest{
|
|
Type: OrderKindDutch,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000000000000000000",
|
|
StartAmount: "2500000000000000000000",
|
|
EndAmount: "2400000000000000000000",
|
|
DecayStartTime: now.Add(1 * time.Minute).Unix(),
|
|
DecayEndTime: now.Add(10 * time.Minute).Unix(),
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: now.Add(1 * time.Hour).Unix(),
|
|
ChainID: 96369,
|
|
}
|
|
|
|
b, _ := json.Marshal(body)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rr := httptest.NewRecorder()
|
|
|
|
s.handleOrder(rr, req)
|
|
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp apiResponse
|
|
json.Unmarshal(rr.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var orderResp CreateOrderResponse
|
|
json.Unmarshal(dataBytes, &orderResp)
|
|
|
|
if orderResp.Tx != nil {
|
|
t.Fatal("did not expect unsigned tx for dutch order")
|
|
}
|
|
if orderResp.EIP712 == nil {
|
|
t.Fatal("expected EIP712 data for dutch order")
|
|
}
|
|
if orderResp.EIP712.PrimaryType != "DutchOrder" {
|
|
t.Fatalf("expected primary type DutchOrder, got %s", orderResp.EIP712.PrimaryType)
|
|
}
|
|
if orderResp.EIP712.Domain.Name != "LuxDutchOrderReactor" {
|
|
t.Fatalf("expected domain name LuxDutchOrderReactor, got %s", orderResp.EIP712.Domain.Name)
|
|
}
|
|
if orderResp.EIP712.Domain.ChainId != "96369" {
|
|
t.Fatalf("expected chainId 96369, got %s", orderResp.EIP712.Domain.ChainId)
|
|
}
|
|
|
|
// Verify message fields
|
|
msg := orderResp.EIP712.Message
|
|
if msg["inputToken"] != "0x0000000000000000000000000000000000000001" {
|
|
t.Fatalf("unexpected inputToken: %v", msg["inputToken"])
|
|
}
|
|
if msg["outputToken"] != "0x0000000000000000000000000000000000000002" {
|
|
t.Fatalf("unexpected outputToken: %v", msg["outputToken"])
|
|
}
|
|
if msg["nonce"] == "" || msg["nonce"] == nil {
|
|
t.Fatal("expected nonce in message")
|
|
}
|
|
}
|
|
|
|
func TestCreateOrderValidationError(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
// Invalid JSON
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader([]byte("{")))
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid JSON, got %d", rr.Code)
|
|
}
|
|
|
|
// Missing required fields
|
|
b, _ := json.Marshal(CreateOrderRequest{Type: OrderKindLimit})
|
|
req = httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader(b))
|
|
rr = httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for missing fields, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestListOrdersHandler(t *testing.T) {
|
|
s := newTestServer(t)
|
|
addr := "0x0000000000000000000000000000000000000003"
|
|
|
|
// Create 2 orders
|
|
for i := 0; i < 2; i++ {
|
|
body := CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000",
|
|
LimitPrice: "2500",
|
|
Recipient: addr,
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader(b))
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
if rr.Code != http.StatusCreated {
|
|
t.Fatalf("failed to create order: %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
// List orders
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/order?address="+addr, nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp apiResponse
|
|
json.Unmarshal(rr.Body.Bytes(), &resp)
|
|
|
|
dataBytes, _ := json.Marshal(resp.Data)
|
|
var listResp OrderListResponse
|
|
json.Unmarshal(dataBytes, &listResp)
|
|
|
|
if listResp.Total != 2 {
|
|
t.Fatalf("expected 2 orders, got %d", listResp.Total)
|
|
}
|
|
|
|
// List with status filter
|
|
req = httptest.NewRequest(http.MethodGet, "/v1/order?address="+addr+"&status=filled", nil)
|
|
rr = httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
json.Unmarshal(rr.Body.Bytes(), &resp)
|
|
dataBytes, _ = json.Marshal(resp.Data)
|
|
json.Unmarshal(dataBytes, &listResp)
|
|
|
|
if listResp.Total != 0 {
|
|
t.Fatalf("expected 0 filled orders, got %d", listResp.Total)
|
|
}
|
|
}
|
|
|
|
func TestListOrdersMissingAddress(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/order", nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestListOrdersInvalidStatusFilter(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/order?address=0x0000000000000000000000000000000000000003&status=bogus", nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestGetOrderHandler(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
// Create an order
|
|
body := CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
createReq := httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader(b))
|
|
createRR := httptest.NewRecorder()
|
|
s.handleOrder(createRR, createReq)
|
|
|
|
var createResp apiResponse
|
|
json.Unmarshal(createRR.Body.Bytes(), &createResp)
|
|
dataBytes, _ := json.Marshal(createResp.Data)
|
|
var orderResp CreateOrderResponse
|
|
json.Unmarshal(dataBytes, &orderResp)
|
|
|
|
// Get the order
|
|
req := httptest.NewRequest(http.MethodGet, "/v1/order/"+orderResp.OrderID, nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrderByID(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
// Get nonexistent
|
|
req = httptest.NewRequest(http.MethodGet, "/v1/order/nonexistent", nil)
|
|
rr = httptest.NewRecorder()
|
|
s.handleOrderByID(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestCancelOrderHandler(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
// Create a limit order
|
|
body := CreateOrderRequest{
|
|
Type: OrderKindLimit,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000",
|
|
LimitPrice: "2500",
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: time.Now().Add(1 * time.Hour).Unix(),
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
createReq := httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader(b))
|
|
createRR := httptest.NewRecorder()
|
|
s.handleOrder(createRR, createReq)
|
|
|
|
var createResp apiResponse
|
|
json.Unmarshal(createRR.Body.Bytes(), &createResp)
|
|
dataBytes, _ := json.Marshal(createResp.Data)
|
|
var orderResp CreateOrderResponse
|
|
json.Unmarshal(dataBytes, &orderResp)
|
|
|
|
// Cancel the order
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/order/"+orderResp.OrderID, nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrderByID(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var cancelResp apiResponse
|
|
json.Unmarshal(rr.Body.Bytes(), &cancelResp)
|
|
cancelData, _ := json.Marshal(cancelResp.Data)
|
|
var cancelOrder CancelOrderResponse
|
|
json.Unmarshal(cancelData, &cancelOrder)
|
|
|
|
if cancelOrder.Status != OrderStatusCancelled {
|
|
t.Fatalf("expected cancelled, got %s", cancelOrder.Status)
|
|
}
|
|
if cancelOrder.Tx == nil {
|
|
t.Fatal("expected cancel tx for limit order")
|
|
}
|
|
|
|
// Cancel again should fail
|
|
req = httptest.NewRequest(http.MethodDelete, "/v1/order/"+orderResp.OrderID, nil)
|
|
rr = httptest.NewRecorder()
|
|
s.handleOrderByID(rr, req)
|
|
|
|
if rr.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409 for double cancel, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestCancelDutchOrderHandler(t *testing.T) {
|
|
s := newTestServer(t)
|
|
now := time.Now()
|
|
|
|
// Create a dutch order
|
|
body := CreateOrderRequest{
|
|
Type: OrderKindDutch,
|
|
TokenIn: "0x0000000000000000000000000000000000000001",
|
|
TokenOut: "0x0000000000000000000000000000000000000002",
|
|
AmountIn: "1000",
|
|
StartAmount: "2500",
|
|
EndAmount: "2400",
|
|
DecayStartTime: now.Add(1 * time.Minute).Unix(),
|
|
DecayEndTime: now.Add(10 * time.Minute).Unix(),
|
|
Recipient: "0x0000000000000000000000000000000000000003",
|
|
Deadline: now.Add(1 * time.Hour).Unix(),
|
|
}
|
|
b, _ := json.Marshal(body)
|
|
createReq := httptest.NewRequest(http.MethodPost, "/v1/order", bytes.NewReader(b))
|
|
createRR := httptest.NewRecorder()
|
|
s.handleOrder(createRR, createReq)
|
|
|
|
var createResp apiResponse
|
|
json.Unmarshal(createRR.Body.Bytes(), &createResp)
|
|
dataBytes, _ := json.Marshal(createResp.Data)
|
|
var orderResp CreateOrderResponse
|
|
json.Unmarshal(dataBytes, &orderResp)
|
|
|
|
// Cancel the dutch order — should NOT return an unsigned tx
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/order/"+orderResp.OrderID, nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrderByID(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var cancelResp apiResponse
|
|
json.Unmarshal(rr.Body.Bytes(), &cancelResp)
|
|
cancelData, _ := json.Marshal(cancelResp.Data)
|
|
var cancelOrder CancelOrderResponse
|
|
json.Unmarshal(cancelData, &cancelOrder)
|
|
|
|
if cancelOrder.Tx != nil {
|
|
t.Fatal("did not expect cancel tx for dutch order (off-chain cancellation)")
|
|
}
|
|
}
|
|
|
|
func TestCancelNonexistentOrder(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
req := httptest.NewRequest(http.MethodDelete, "/v1/order/nonexistent", nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrderByID(rr, req)
|
|
|
|
if rr.Code != http.StatusNotFound {
|
|
t.Fatalf("expected 404, got %d", rr.Code)
|
|
}
|
|
}
|
|
|
|
func TestMethodNotAllowed(t *testing.T) {
|
|
s := newTestServer(t)
|
|
|
|
// PUT on /v1/order
|
|
req := httptest.NewRequest(http.MethodPut, "/v1/order", nil)
|
|
rr := httptest.NewRecorder()
|
|
s.handleOrder(rr, req)
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected 405, got %d", rr.Code)
|
|
}
|
|
|
|
// POST on /v1/order/{id}
|
|
req = httptest.NewRequest(http.MethodPost, "/v1/order/someid", nil)
|
|
rr = httptest.NewRecorder()
|
|
s.handleOrderByID(rr, req)
|
|
if rr.Code != http.StatusMethodNotAllowed {
|
|
t.Fatalf("expected 405, got %d", rr.Code)
|
|
}
|
|
}
|