package exchange import ( "encoding/hex" "encoding/json" "fmt" "io" "math" "math/big" "net/http" "strings" "sync" "time" "crypto/ed25519" "crypto/sha512" ) // HLOrderAction represents a HyperLiquid order action. type HLOrderAction struct { Type string `json:"type"` Order HLOrder `json:"order"` Grouping string `json:"grouping"` BrokerCode int `json:"brokerCode"` } type HLOrder struct { Coin string `json:"coin"` IsBuy bool `json:"isBuy"` Sz string `json:"sz"` LimitPx string `json:"limitPx"` OrderType string `json:"orderType"` ReduceOnly bool `json:"reduceOnly"` } // HLSignedAction wraps the action with signature. type HLSignedAction struct { Action HLOrderAction `json:"action"` Nonce int64 `json:"nonce"` Signature string `json:"signature"` VaultAddress string `json:"vaultAddress,omitempty"` } type HLOrderResponse struct { Response *json.RawMessage `json:"response"` Data *json.RawMessage `json:"data"` } // HyperLiquidTrade handles order placement on HyperLiquid. type HyperLiquidTrade struct { PrivateKey ed25519.PrivateKey Address string client *http.Client lastNonce int64 nonceMu sync.Mutex // protect lastNonce++ (Issue #4) } func NewHyperLiquidTrade(privateKeyHex, address string) (*HyperLiquidTrade, error) { if privateKeyHex == "" { return &HyperLiquidTrade{client: &http.Client{Timeout: 10 * time.Second}}, nil } keyBytes, err := hex.DecodeString(privateKeyHex) if err != nil { return nil, fmt.Errorf("decode private key: %w", err) } if len(keyBytes) != ed25519.PrivateKeySize { return nil, fmt.Errorf("invalid private key length: %d (expected %d)", len(keyBytes), ed25519.PrivateKeySize) } return &HyperLiquidTrade{ PrivateKey: ed25519.PrivateKey(keyBytes), Address: address, client: &http.Client{Timeout: 10 * time.Second}, }, nil } func (h *HyperLiquidTrade) IsConfigured() bool { return h.PrivateKey != nil && h.Address != "" } // PlaceMarketOrder places a market order on HyperLiquid. // coin: "BTC", side: "buy" or "sell", sz: order size in coin units (e.g. "0.001") func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, error) { if !h.IsConfigured() { return "", fmt.Errorf("HyperLiquid not configured") } isBuy := side == "buy" action := HLOrderAction{ Type: "order", Order: HLOrder{ Coin: coin, IsBuy: isBuy, Sz: sz, LimitPx: "1000000", // high limit price for market orders OrderType: "IOC", ReduceOnly: false, }, Grouping: "na", BrokerCode: 0, } // Generate nonce (thread-safe) h.nonceMu.Lock() h.lastNonce++ nonce := time.Now().UnixMilli()*1_000_000 + h.lastNonce%1_000_000 h.nonceMu.Unlock() // Sign the action sig, err := h.signAction(action, nonce) if err != nil { return "", fmt.Errorf("sign: %w", err) } signed := HLSignedAction{ Action: action, Nonce: nonce, Signature: sig, } bodyJSON, _ := json.Marshal(signed) req, err := http.NewRequest("POST", "https://api.hyperliquid.xyz/exchange", strings.NewReader(string(bodyJSON))) if err != nil { return "", fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := h.client.Do(req) if err != nil { return "", fmt.Errorf("http request: %w", err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) // Parse response var response json.RawMessage if err := json.Unmarshal(respBody, &response); err != nil { return "", fmt.Errorf("parse response: %s", string(respBody)) } // HL returns response as array, e.g. [{"response": {"type": "order", "data": ...}}] var results []map[string]json.RawMessage if err := json.Unmarshal(respBody, &results); err != nil { // Maybe returns single object return string(respBody), nil } if len(results) == 0 { return "", fmt.Errorf("empty response: %s", string(respBody)) } respJSON, _ := json.Marshal(results[0]) return string(respJSON), nil } // signAction generates an Ed25519 signature for a HyperLiquid action. // The signing hash is SHA512(action_json + nonce). func (h *HyperLiquidTrade) signAction(action HLOrderAction, nonce int64) (string, error) { actionJSON, err := json.Marshal(action) if err != nil { return "", err } // HL signs: hash = sha512(action_json + nonce) nonceBig := big.NewInt(nonce) msg := string(actionJSON) + nonceBig.String() hash := sha512.Sum512([]byte(msg)) sig := ed25519.Sign(h.PrivateKey, hash[:]) return "0x" + hex.EncodeToString(sig), nil } // GetHLSize calculates size for a given USD amount on HyperLiquid. // Uses szDecimals precision from HL's contract universe. // Returns size as a decimal string complying with HL precision. // Uses math.Floor to round DOWN to the nearest valid step, consistent with GetBitgetSize. func GetHLSize(coin string, amountUSD, price float64) string { sz := amountUSD / price // raw coin count switch coin { case "DOGE": sz = math.Floor(sz) // step=1, szDecimals=0 if sz < 1 { sz = 1 } return fmt.Sprintf("%.0f", sz) case "LINK": sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) case "ONDO": sz = math.Floor(sz) // step=1, szDecimals=0 if sz < 1 { sz = 1 } return fmt.Sprintf("%.0f", sz) case "OP": sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) case "WIF": sz = math.Floor(sz) // step=1, szDecimals=0 if sz < 1 { sz = 1 } return fmt.Sprintf("%.0f", sz) case "ARB": sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) default: return fmt.Sprintf("%.4f", sz) } }