diff --git a/cmd/lighter_test/main.go b/cmd/lighter_test/main.go new file mode 100644 index 00000000..6f896a23 --- /dev/null +++ b/cmd/lighter_test/main.go @@ -0,0 +1,233 @@ +// Lighter API Authentication Test Tool +// Usage: go run cmd/lighter_test/main.go -wallet=0x... -apikey=... [-testnet] +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" + "time" + + lighterClient "github.com/elliottech/lighter-go/client" + lighterHTTP "github.com/elliottech/lighter-go/client/http" +) + +func main() { + // Parse command line flags + walletAddr := flag.String("wallet", "", "Ethereum wallet address") + apiKeyPrivateKey := flag.String("apikey", "", "API key private key (40 bytes hex)") + apiKeyIndex := flag.Int("apikeyindex", 0, "API key index (0-255)") + testnet := flag.Bool("testnet", false, "Use testnet instead of mainnet") + flag.Parse() + + if *walletAddr == "" || *apiKeyPrivateKey == "" { + fmt.Println("Usage: go run cmd/lighter_test/main.go -wallet=0x... -apikey=...") + fmt.Println("Options:") + fmt.Println(" -wallet Ethereum wallet address (required)") + fmt.Println(" -apikey API key private key, 40 bytes hex (required)") + fmt.Println(" -apikeyindex API key index, 0-255 (default: 0)") + fmt.Println(" -testnet Use testnet instead of mainnet") + os.Exit(1) + } + + fmt.Println("=== Lighter API Authentication Test ===") + fmt.Printf("Wallet: %s\n", *walletAddr) + fmt.Printf("API Key Index: %d\n", *apiKeyIndex) + fmt.Printf("Testnet: %v\n", *testnet) + fmt.Println() + + // Determine base URL + baseURL := "https://mainnet.zklighter.elliot.ai" + chainID := uint32(304) + if *testnet { + baseURL = "https://testnet.zklighter.elliot.ai" + chainID = uint32(300) + } + + // Create HTTP client + httpClient := lighterHTTP.NewClient(baseURL) + client := &http.Client{Timeout: 30 * time.Second} + + // Step 1: Get account info + fmt.Println("Step 1: Getting account info...") + accountInfo, err := getAccountByL1Address(client, baseURL, *walletAddr) + if err != nil { + fmt.Printf("ERROR: Failed to get account info: %v\n", err) + os.Exit(1) + } + fmt.Printf("SUCCESS: Account index = %d\n\n", accountInfo.AccountIndex) + + // Step 2: Create TxClient + fmt.Println("Step 2: Creating TxClient...") + txClient, err := lighterClient.NewTxClient( + httpClient, + *apiKeyPrivateKey, + accountInfo.AccountIndex, + uint8(*apiKeyIndex), + chainID, + ) + if err != nil { + fmt.Printf("ERROR: Failed to create TxClient: %v\n", err) + os.Exit(1) + } + fmt.Println("SUCCESS: TxClient created\n") + + // Step 3: Generate auth token + fmt.Println("Step 3: Generating auth token...") + deadline := time.Now().Add(1 * time.Hour) + authToken, err := txClient.GetAuthToken(deadline) + if err != nil { + fmt.Printf("ERROR: Failed to generate auth token: %v\n", err) + os.Exit(1) + } + fmt.Printf("SUCCESS: Auth token generated\n") + fmt.Printf("Token: %s...\n", authToken[:min(50, len(authToken))]) + fmt.Printf("Valid until: %s\n\n", deadline.Format(time.RFC3339)) + + // Step 4: Test GetActiveOrders API with auth query parameter + fmt.Println("Step 4: Testing GetActiveOrders API...") + encodedAuth := url.QueryEscape(authToken) + endpoint := fmt.Sprintf("%s/api/v1/accountActiveOrders?account_index=%d&market_id=0&auth=%s", + baseURL, accountInfo.AccountIndex, encodedAuth) + + fmt.Printf("Endpoint: %s...\n", endpoint[:min(120, len(endpoint))]) + + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + fmt.Printf("ERROR: Failed to create request: %v\n", err) + os.Exit(1) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + fmt.Printf("ERROR: Request failed: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + fmt.Printf("Status: %d\n", resp.StatusCode) + fmt.Printf("Response: %s\n\n", string(body)) + + // Parse response + var apiResp struct { + Code int `json:"code"` + Message string `json:"message"` + Orders []struct { + OrderID string `json:"order_id"` + Side string `json:"side"` + Type string `json:"type"` + Price string `json:"price"` + } `json:"orders"` + } + if err := json.Unmarshal(body, &apiResp); err != nil { + fmt.Printf("ERROR: Failed to parse response: %v\n", err) + os.Exit(1) + } + + if apiResp.Code != 200 { + fmt.Printf("API ERROR: code=%d, message=%s\n", apiResp.Code, apiResp.Message) + fmt.Println("\n=== DIAGNOSTIC INFO ===") + fmt.Println("If you see 'invalid signature', possible causes:") + fmt.Println("1. API key is not registered on-chain") + fmt.Println("2. API key private key is incorrect") + fmt.Println("3. API key index is wrong") + fmt.Println("4. Account index mismatch") + fmt.Println("\nTo fix:") + fmt.Println("- Go to app.lighter.xyz and register/verify your API key") + fmt.Println("- Make sure you're using the correct API key private key") + os.Exit(1) + } + + fmt.Printf("SUCCESS: Retrieved %d orders\n", len(apiResp.Orders)) + for i, order := range apiResp.Orders { + if i >= 5 { + fmt.Printf("... and %d more orders\n", len(apiResp.Orders)-5) + break + } + fmt.Printf(" Order %s: %s %s @ %s\n", order.OrderID, order.Side, order.Type, order.Price) + } + + // Step 5: Test GetTrades API (also needs auth) + fmt.Println("\nStep 5: Testing GetTrades API...") + tradesEndpoint := fmt.Sprintf("%s/api/v1/trades?account_index=%d&sort_by=timestamp&sort_dir=desc&limit=5&auth=%s", + baseURL, accountInfo.AccountIndex, encodedAuth) + + tradesReq, _ := http.NewRequest("GET", tradesEndpoint, nil) + tradesResp, err := client.Do(tradesReq) + if err != nil { + fmt.Printf("ERROR: Trades request failed: %v\n", err) + } else { + defer tradesResp.Body.Close() + tradesBody, _ := io.ReadAll(tradesResp.Body) + fmt.Printf("Status: %d\n", tradesResp.StatusCode) + if tradesResp.StatusCode == 200 { + fmt.Println("SUCCESS: GetTrades API working") + } else { + fmt.Printf("Response: %s\n", string(tradesBody)) + } + } + + fmt.Println("\n=== ALL TESTS PASSED ===") +} + +// AccountInfo represents Lighter account information +type AccountInfo struct { + AccountIndex int64 `json:"account_index"` + L1Address string `json:"l1_address"` +} + +// getAccountByL1Address gets account info by L1 wallet address +func getAccountByL1Address(client *http.Client, baseURL, walletAddr string) (*AccountInfo, error) { + endpoint := fmt.Sprintf("%s/api/v1/account?by=l1_address&value=%s", baseURL, walletAddr) + + req, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + req = req.WithContext(ctx) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + // Parse response - can be in "accounts" or "sub_accounts" field + var apiResp struct { + Code int `json:"code"` + Message string `json:"message"` + Accounts []AccountInfo `json:"accounts"` + SubAccounts []AccountInfo `json:"sub_accounts"` + } + + if err := json.Unmarshal(body, &apiResp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w, body: %s", err, string(body)) + } + + // Check main accounts first + if len(apiResp.Accounts) > 0 { + return &apiResp.Accounts[0], nil + } + + // Check sub-accounts + if len(apiResp.SubAccounts) > 0 { + return &apiResp.SubAccounts[0], nil + } + + return nil, fmt.Errorf("no account found for address: %s", walletAddr) +} diff --git a/scripts/test_lighter_orders.go b/scripts/test_lighter_orders.go new file mode 100644 index 00000000..e064cac2 --- /dev/null +++ b/scripts/test_lighter_orders.go @@ -0,0 +1,168 @@ +//go:build ignore + +// Test script to verify Lighter API authentication +// Run: go run scripts/test_lighter_orders.go +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "time" + + lighterClient "github.com/elliottech/lighter-go/client" + lighterHTTP "github.com/elliottech/lighter-go/client/http" +) + +func main() { + // Configuration - update these values + walletAddr := os.Getenv("LIGHTER_WALLET") + apiKeyPrivateKey := os.Getenv("LIGHTER_API_KEY") + + if walletAddr == "" || apiKeyPrivateKey == "" { + fmt.Println("Usage: LIGHTER_WALLET=0x... LIGHTER_API_KEY=... go run scripts/test_lighter_orders.go") + fmt.Println("Environment variables required:") + fmt.Println(" LIGHTER_WALLET - Ethereum wallet address") + fmt.Println(" LIGHTER_API_KEY - API key private key (40 bytes hex)") + os.Exit(1) + } + + fmt.Println("=== Lighter API Test ===") + fmt.Printf("Wallet: %s\n\n", walletAddr) + + baseURL := "https://mainnet.zklighter.elliot.ai" + chainID := uint32(304) + client := &http.Client{Timeout: 30 * time.Second} + + // Step 1: Get account info (no auth required) + fmt.Println("1. Getting account info...") + accountIndex, err := getAccountIndex(client, baseURL, walletAddr) + if err != nil { + fmt.Printf(" FAILED: %v\n", err) + os.Exit(1) + } + fmt.Printf(" OK: account_index = %d\n\n", accountIndex) + + // Step 2: Create TxClient and generate auth token + fmt.Println("2. Creating TxClient and generating auth token...") + httpClient := lighterHTTP.NewClient(baseURL) + txClient, err := lighterClient.NewTxClient(httpClient, apiKeyPrivateKey, accountIndex, 0, chainID) + if err != nil { + fmt.Printf(" FAILED: %v\n", err) + os.Exit(1) + } + + authToken, err := txClient.GetAuthToken(time.Now().Add(1 * time.Hour)) + if err != nil { + fmt.Printf(" FAILED: %v\n", err) + os.Exit(1) + } + fmt.Printf(" OK: auth token generated\n\n") + + // Step 3: Test GetActiveOrders with auth query parameter (NEW method) + fmt.Println("3. Testing GetActiveOrders with auth query parameter (FIXED)...") + encodedAuth := url.QueryEscape(authToken) + endpoint := fmt.Sprintf("%s/api/v1/accountActiveOrders?account_index=%d&market_id=0&auth=%s", + baseURL, accountIndex, encodedAuth) + + resp, err := client.Get(endpoint) + if err != nil { + fmt.Printf(" FAILED: %v\n", err) + os.Exit(1) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var result map[string]interface{} + json.Unmarshal(body, &result) + + if code, ok := result["code"].(float64); ok && code == 200 { + orders := result["orders"].([]interface{}) + fmt.Printf(" OK: Retrieved %d orders\n", len(orders)) + if len(orders) > 0 { + fmt.Println(" Sample orders:") + for i, o := range orders { + if i >= 3 { + fmt.Printf(" ... and %d more\n", len(orders)-3) + break + } + order := o.(map[string]interface{}) + fmt.Printf(" - ID: %v, Price: %v, Side: %v\n", + order["order_id"], order["price"], order["is_ask"]) + } + } + } else { + fmt.Printf(" FAILED: %s\n", string(body)) + fmt.Println("\n Possible causes:") + fmt.Println(" - API key not registered on-chain") + fmt.Println(" - API key private key incorrect") + fmt.Println(" - Account index mismatch") + os.Exit(1) + } + + // Step 4: Test GetActiveOrders with Authorization header (OLD method - for comparison) + fmt.Println("\n4. Testing GetActiveOrders with Authorization header (OLD method)...") + endpoint2 := fmt.Sprintf("%s/api/v1/accountActiveOrders?account_index=%d&market_id=0", + baseURL, accountIndex) + + req, _ := http.NewRequest("GET", endpoint2, nil) + req.Header.Set("Authorization", authToken) + req.Header.Set("Content-Type", "application/json") + + resp2, err := client.Do(req) + if err != nil { + fmt.Printf(" FAILED: %v\n", err) + } else { + defer resp2.Body.Close() + body2, _ := io.ReadAll(resp2.Body) + var result2 map[string]interface{} + json.Unmarshal(body2, &result2) + + if code, ok := result2["code"].(float64); ok && code == 200 { + orders := result2["orders"].([]interface{}) + fmt.Printf(" OK: Retrieved %d orders (both methods work!)\n", len(orders)) + } else { + fmt.Printf(" FAILED: %s\n", string(body2)) + fmt.Println(" ^ This is expected - Authorization header doesn't work consistently") + } + } + + fmt.Println("\n=== TEST COMPLETE ===") + fmt.Println("If test 3 passed, the fix is working correctly.") +} + +func getAccountIndex(client *http.Client, baseURL, walletAddr string) (int64, error) { + endpoint := fmt.Sprintf("%s/api/v1/account?by=l1_address&value=%s", baseURL, walletAddr) + resp, err := client.Get(endpoint) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var result struct { + Code int `json:"code"` + Accounts []struct { + AccountIndex int64 `json:"account_index"` + } `json:"accounts"` + SubAccounts []struct { + AccountIndex int64 `json:"account_index"` + } `json:"sub_accounts"` + } + + if err := json.Unmarshal(body, &result); err != nil { + return 0, fmt.Errorf("failed to parse: %w", err) + } + + if len(result.Accounts) > 0 { + return result.Accounts[0].AccountIndex, nil + } + if len(result.SubAccounts) > 0 { + return result.SubAccounts[0].AccountIndex, nil + } + + return 0, fmt.Errorf("no account found") +} diff --git a/trader/lighter_trader_v2_orders_test.go b/trader/lighter_trader_v2_orders_test.go new file mode 100644 index 00000000..7b84912f --- /dev/null +++ b/trader/lighter_trader_v2_orders_test.go @@ -0,0 +1,421 @@ +package trader + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGetActiveOrders_ParseResponse tests parsing of Lighter API response +func TestGetActiveOrders_ParseResponse(t *testing.T) { + // Mock response from Lighter API + mockResponse := `{ + "code": 200, + "message": "success", + "orders": [ + { + "order_id": "123456", + "order_index": 123456, + "market_index": 0, + "side": "ask", + "type": "limit", + "is_ask": true, + "price": "3150.50", + "initial_base_amount": "1.5", + "remaining_base_amount": "1.5", + "filled_base_amount": "0", + "status": "open", + "trigger_price": "", + "reduce_only": false, + "timestamp": 1736745600000, + "created_at": 1736745600000 + }, + { + "order_id": "123457", + "order_index": 123457, + "market_index": 0, + "side": "bid", + "type": "limit", + "is_ask": false, + "price": "3100.00", + "initial_base_amount": "2.0", + "remaining_base_amount": "2.0", + "filled_base_amount": "0", + "status": "open", + "trigger_price": "", + "reduce_only": false, + "timestamp": 1736745601000, + "created_at": 1736745601000 + }, + { + "order_id": "123458", + "order_index": 123458, + "market_index": 0, + "side": "ask", + "type": "stop_loss", + "is_ask": true, + "price": "0", + "initial_base_amount": "1.0", + "remaining_base_amount": "1.0", + "filled_base_amount": "0", + "status": "open", + "trigger_price": "3000.00", + "reduce_only": true, + "timestamp": 1736745602000, + "created_at": 1736745602000 + } + ] + }` + + // Parse the response + var apiResp struct { + Code int `json:"code"` + Message string `json:"message"` + Orders []OrderResponse `json:"orders"` + } + + err := json.Unmarshal([]byte(mockResponse), &apiResp) + require.NoError(t, err, "Should parse response without error") + + // Verify parsed data + assert.Equal(t, 200, apiResp.Code) + assert.Equal(t, 3, len(apiResp.Orders)) + + // Test first order (sell limit) + order1 := apiResp.Orders[0] + assert.Equal(t, "123456", order1.OrderID) + assert.True(t, order1.IsAsk, "First order should be ask (sell)") + assert.Equal(t, "3150.50", order1.Price) + assert.Equal(t, "1.5", order1.RemainingBaseAmount) + assert.False(t, order1.ReduceOnly) + + // Test second order (buy limit) + order2 := apiResp.Orders[1] + assert.Equal(t, "123457", order2.OrderID) + assert.False(t, order2.IsAsk, "Second order should be bid (buy)") + assert.Equal(t, "3100.00", order2.Price) + + // Test third order (stop-loss) + order3 := apiResp.Orders[2] + assert.Equal(t, "123458", order3.OrderID) + assert.Equal(t, "stop_loss", order3.Type) + assert.Equal(t, "3000.00", order3.TriggerPrice) + assert.True(t, order3.ReduceOnly) +} + +// TestGetActiveOrders_EmptyResponse tests handling of empty orders +func TestGetActiveOrders_EmptyResponse(t *testing.T) { + mockResponse := `{ + "code": 200, + "message": "success", + "orders": [] + }` + + var apiResp struct { + Code int `json:"code"` + Message string `json:"message"` + Orders []OrderResponse `json:"orders"` + } + + err := json.Unmarshal([]byte(mockResponse), &apiResp) + require.NoError(t, err) + assert.Equal(t, 200, apiResp.Code) + assert.Equal(t, 0, len(apiResp.Orders)) +} + +// TestGetActiveOrders_ErrorResponse tests handling of API error +func TestGetActiveOrders_ErrorResponse(t *testing.T) { + mockResponse := `{ + "code": 29500, + "message": "internal server error: invalid signature" + }` + + var apiResp struct { + Code int `json:"code"` + Message string `json:"message"` + Orders []OrderResponse `json:"orders"` + } + + err := json.Unmarshal([]byte(mockResponse), &apiResp) + require.NoError(t, err) + assert.Equal(t, 29500, apiResp.Code) + assert.Contains(t, apiResp.Message, "invalid signature") +} + +// TestConvertOrderResponseToOpenOrder tests conversion logic +func TestConvertOrderResponseToOpenOrder(t *testing.T) { + testCases := []struct { + name string + order OrderResponse + expectedSide string + expectedType string + expectedPosSide string + }{ + { + name: "Sell limit order (opening short)", + order: OrderResponse{ + OrderID: "1", + IsAsk: true, + Type: "limit", + Price: "3150.00", + RemainingBaseAmount: "1.0", + ReduceOnly: false, + }, + expectedSide: "SELL", + expectedType: "LIMIT", + expectedPosSide: "SHORT", + }, + { + name: "Buy limit order (opening long)", + order: OrderResponse{ + OrderID: "2", + IsAsk: false, + Type: "limit", + Price: "3100.00", + RemainingBaseAmount: "1.0", + ReduceOnly: false, + }, + expectedSide: "BUY", + expectedType: "LIMIT", + expectedPosSide: "LONG", + }, + { + name: "Sell stop-loss (closing long)", + order: OrderResponse{ + OrderID: "3", + IsAsk: true, + Type: "stop_loss", + TriggerPrice: "3000.00", + RemainingBaseAmount: "1.0", + ReduceOnly: true, + }, + expectedSide: "SELL", + expectedType: "STOP_MARKET", + expectedPosSide: "LONG", + }, + { + name: "Buy stop-loss (closing short)", + order: OrderResponse{ + OrderID: "4", + IsAsk: false, + Type: "stop_loss", + TriggerPrice: "3200.00", + RemainingBaseAmount: "1.0", + ReduceOnly: true, + }, + expectedSide: "BUY", + expectedType: "STOP_MARKET", + expectedPosSide: "SHORT", + }, + { + name: "Take profit (closing long)", + order: OrderResponse{ + OrderID: "5", + IsAsk: true, + Type: "take_profit", + TriggerPrice: "3500.00", + RemainingBaseAmount: "1.0", + ReduceOnly: true, + }, + expectedSide: "SELL", + expectedType: "TAKE_PROFIT_MARKET", + expectedPosSide: "LONG", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Convert side + side := "BUY" + if tc.order.IsAsk { + side = "SELL" + } + assert.Equal(t, tc.expectedSide, side) + + // Convert order type + orderType := "LIMIT" + if tc.order.Type == "market" { + orderType = "MARKET" + } else if tc.order.Type == "stop_loss" || tc.order.Type == "stop" { + orderType = "STOP_MARKET" + } else if tc.order.Type == "take_profit" { + orderType = "TAKE_PROFIT_MARKET" + } + assert.Equal(t, tc.expectedType, orderType) + + // Convert position side + positionSide := "LONG" + if tc.order.ReduceOnly { + if side == "BUY" { + positionSide = "SHORT" + } else { + positionSide = "LONG" + } + } else { + if side == "SELL" { + positionSide = "SHORT" + } + } + assert.Equal(t, tc.expectedPosSide, positionSide) + }) + } +} + +// TestGetActiveOrders_MockServer tests the full HTTP flow with a mock server +func TestGetActiveOrders_MockServer(t *testing.T) { + // Create mock server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify request path and auth parameter + assert.Contains(t, r.URL.Path, "/api/v1/accountActiveOrders") + + // Check that auth query parameter is present + authParam := r.URL.Query().Get("auth") + if authParam == "" { + // Return error if no auth parameter + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]interface{}{ + "code": 29500, + "message": "internal server error: invalid signature", + }) + return + } + + // Return success response + response := map[string]interface{}{ + "code": 200, + "message": "success", + "orders": []map[string]interface{}{ + { + "order_id": "123456", + "order_index": 123456, + "market_index": 0, + "side": "ask", + "type": "limit", + "is_ask": true, + "price": "3150.50", + "initial_base_amount": "1.5", + "remaining_base_amount": "1.5", + "filled_base_amount": "0", + "status": "open", + "trigger_price": "", + "reduce_only": false, + }, + }, + } + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + // Test request without auth - should fail + resp, err := http.Get(server.URL + "/api/v1/accountActiveOrders?account_index=123&market_id=0") + require.NoError(t, err) + defer resp.Body.Close() + + var errorResp struct { + Code int `json:"code"` + Message string `json:"message"` + } + json.NewDecoder(resp.Body).Decode(&errorResp) + assert.Equal(t, 29500, errorResp.Code) + + // Test request with auth - should succeed + resp2, err := http.Get(server.URL + "/api/v1/accountActiveOrders?account_index=123&market_id=0&auth=test_token") + require.NoError(t, err) + defer resp2.Body.Close() + + var successResp struct { + Code int `json:"code"` + Message string `json:"message"` + Orders []OrderResponse `json:"orders"` + } + json.NewDecoder(resp2.Body).Decode(&successResp) + assert.Equal(t, 200, successResp.Code) + assert.Equal(t, 1, len(successResp.Orders)) +} + +// TestAuthTokenFormat tests the auth token format +func TestAuthTokenFormat(t *testing.T) { + // Auth token format: timestamp:account_index:api_key_index:signature + // Example: 1768308847:687247:0:742e02... + + sampleToken := "1768308847:687247:0:742e02abc123" + + // The token should be URL encoded when used as query parameter + // Colons become %3A + expectedEncoded := "1768308847%3A687247%3A0%3A742e02abc123" + + // URL encode the token + encoded := url.QueryEscape(sampleToken) + + assert.Equal(t, expectedEncoded, encoded) +} + +// TestOrderResponseStruct tests that OrderResponse struct matches API response +func TestOrderResponseStruct(t *testing.T) { + // Real API response sample (from logs) + realResponse := `{ + "order_id": "4609885", + "order_index": 4609885, + "market_index": 0, + "side": "ask", + "type": "limit", + "is_ask": true, + "price": "3150.00", + "initial_base_amount": "0.0300", + "remaining_base_amount": "0.0300", + "filled_base_amount": "0", + "status": "open", + "trigger_price": "", + "reduce_only": false, + "timestamp": 1736745600000, + "created_at": 1736745600000 + }` + + var order OrderResponse + err := json.Unmarshal([]byte(realResponse), &order) + require.NoError(t, err) + + assert.Equal(t, "4609885", order.OrderID) + assert.Equal(t, int64(4609885), order.OrderIndex) + assert.Equal(t, 0, order.MarketIndex) + assert.Equal(t, "ask", order.Side) + assert.Equal(t, "limit", order.Type) + assert.True(t, order.IsAsk) + assert.Equal(t, "3150.00", order.Price) + assert.Equal(t, "0.0300", order.InitialBaseAmount) + assert.Equal(t, "0.0300", order.RemainingBaseAmount) + assert.Equal(t, "0", order.FilledBaseAmount) + assert.Equal(t, "open", order.Status) + assert.Equal(t, "", order.TriggerPrice) + assert.False(t, order.ReduceOnly) + assert.Equal(t, int64(1736745600000), order.Timestamp) + assert.Equal(t, int64(1736745600000), order.CreatedAt) +} + +// BenchmarkParseOrderResponse benchmarks response parsing +func BenchmarkParseOrderResponse(b *testing.B) { + mockResponse := `{ + "code": 200, + "message": "success", + "orders": [ + {"order_id": "1", "is_ask": true, "price": "3150.50", "remaining_base_amount": "1.5"}, + {"order_id": "2", "is_ask": false, "price": "3100.00", "remaining_base_amount": "2.0"}, + {"order_id": "3", "is_ask": true, "price": "3200.00", "remaining_base_amount": "0.5"} + ] + }` + + b.ResetTimer() + for i := 0; i < b.N; i++ { + var apiResp struct { + Code int `json:"code"` + Message string `json:"message"` + Orders []OrderResponse `json:"orders"` + } + json.Unmarshal([]byte(mockResponse), &apiResp) + } +}