package exchange import ( "context" "crypto/ecdsa" "encoding/hex" "encoding/json" "fmt" "math" "strconv" "strings" "sync" "time" "github.com/ethereum/go-ethereum/crypto" hl "github.com/sonirico/go-hyperliquid" ) // HyperLiquidTrade handles order placement on HyperLiquid using the SDK. type HyperLiquidTrade struct { exchange *hl.Exchange info *hl.Info privateKey *ecdsa.PrivateKey mainAddress string // main account address nonceMu sync.Mutex lastNonce int64 configured bool } func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperLiquidTrade, error) { if privateKeyHex == "" { return &HyperLiquidTrade{}, nil } // Parse ECDSA private key (supports 0x prefix) keyHex := strings.TrimPrefix(privateKeyHex, "0x") keyBytes, err := hex.DecodeString(keyHex) if err != nil { return nil, fmt.Errorf("decode private key: %w", err) } privKey, err := crypto.ToECDSA(keyBytes) if err != nil { return nil, fmt.Errorf("convert to ECDSA: %w", err) } t := &HyperLiquidTrade{ privateKey: privKey, mainAddress: mainAddress, configured: true, } // Initialize SDK info (fetch meta for exchange) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() t.info = hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil) if t.info == nil { return nil, fmt.Errorf("failed to create HL Info") } // Wait briefly for meta to be fetched (Info fetches meta in constructor) time.Sleep(2 * time.Second) return t, nil } func (h *HyperLiquidTrade) initExchange() error { if h.exchange != nil { return nil } if !h.configured { return fmt.Errorf("HyperLiquid not configured") } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() meta, err := h.fetchMeta(ctx) if err != nil { return fmt.Errorf("fetch meta: %w", err) } h.exchange = hl.NewExchange( ctx, h.privateKey, hl.TestnetAPIURL, meta, "", // vault h.mainAddress, // account address nil, // spot meta nil, // perp dex ) return nil } func (h *HyperLiquidTrade) fetchMeta(ctx context.Context) (*hl.Meta, error) { // Use info endpoint to get meta resp, err := h.postInfo(ctx, map[string]any{"type": "meta"}) if err != nil { return nil, err } // Parse as Meta struct var meta hl.Meta if err := json.Unmarshal(resp, &meta); err != nil { return nil, fmt.Errorf("parse meta: %w", err) } return &meta, nil } func (h *HyperLiquidTrade) postInfo(ctx context.Context, payload map[string]any) ([]byte, error) { // Simple HTTP POST to info endpoint return nil, fmt.Errorf("not implemented via SDK - use Info directly") } func (h *HyperLiquidTrade) IsConfigured() bool { return h.configured } // PlaceMarketOrder places a market (IOC) order on HyperLiquid. func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, error) { if !h.configured { return "", fmt.Errorf("HyperLiquid not configured") } if err := h.initExchange(); err != nil { return "", fmt.Errorf("init exchange: %w", err) } isBuy := side == "buy" // Parse size from string to float64 size, err := strconv.ParseFloat(sz, 64) if err != nil { return "", fmt.Errorf("parse size %s: %w", sz, err) } // Get current price for slippage calculation ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() allMids, err := h.fetchAllMids(ctx) if err != nil { return "", fmt.Errorf("fetch prices: %w", err) } priceStr, ok := allMids[coin] if !ok { return "", fmt.Errorf("coin %s not found in allMids", coin) } midPx, _ := strconv.ParseFloat(priceStr, 64) // Slippage price: buy = ask (mid * 1.02), sell = bid (mid * 0.98) var limitPx float64 if isBuy { limitPx = midPx * 2.0 // aggressive buy } else { limitPx = midPx * 0.5 // aggressive sell } result, err := h.exchange.MarketOpen(ctx, coin, isBuy, size, &limitPx, 0.05, nil, nil) if err != nil { return "", fmt.Errorf("market order: %w", err) } // Serialize response respJSON, _ := json.Marshal(result) return string(respJSON), nil } func (h *HyperLiquidTrade) fetchAllMids(ctx context.Context) (map[string]string, error) { // Try to get allMids via the SDK's Info if available if h.info != nil { mids, err := h.info.AllMids(ctx) if err != nil { return nil, err } return mids, nil } return nil, fmt.Errorf("info not initialized") } // GetHLSize calculates size for a given USD amount on HyperLiquid. func GetHLSize(coin string, amountUSD, price float64) string { sz := amountUSD / price switch coin { case "DOGE": sz = math.Floor(sz) if sz < 1 { sz = 1 } return fmt.Sprintf("%.0f", sz) case "LINK": sz = math.Floor(sz*10) / 10 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) case "ONDO": sz = math.Floor(sz) if sz < 1 { sz = 1 } return fmt.Sprintf("%.0f", sz) case "OP": sz = math.Floor(sz*10) / 10 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) case "WIF": sz = math.Floor(sz) if sz < 1 { sz = 1 } return fmt.Sprintf("%.0f", sz) case "ARB": sz = math.Floor(sz*10) / 10 if sz < 0.1 { sz = 0.1 } return fmt.Sprintf("%.1f", sz) default: return fmt.Sprintf("%.4f", sz) } }