mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
test: add Lighter API authentication tests and diagnostic tools
This commit is contained in:
233
cmd/lighter_test/main.go
Normal file
233
cmd/lighter_test/main.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
168
scripts/test_lighter_orders.go
Normal file
168
scripts/test_lighter_orders.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
421
trader/lighter_trader_v2_orders_test.go
Normal file
421
trader/lighter_trader_v2_orders_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user