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 nonceMu sync.Mutex lastNonce int64 configured bool } func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperLiquidTrade, error) { if privateKeyHex == "" { return &HyperLiquidTrade{}, nil } 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) } // Initialize SDK Info (auto-fetches meta + spotMeta) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() info := hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil) t := &HyperLiquidTrade{ privateKey: privKey, mainAddress: mainAddress, info: info, configured: true, } // Initialize exchange lazily on first order 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(), 15*time.Second) defer cancel() meta, err := h.info.Meta(ctx) if err != nil { return fmt.Errorf("fetch meta: %w", err) } spotMeta, err := h.info.SpotMeta(ctx) if err != nil { return fmt.Errorf("fetch spot meta: %w", err) } h.exchange = hl.NewExchange( ctx, h.privateKey, hl.TestnetAPIURL, meta, "", h.mainAddress, spotMeta, nil, ) return nil } 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" size, err := strconv.ParseFloat(sz, 64) if err != nil { return "", fmt.Errorf("parse size %s: %w", sz, err) } // Get current price for slippage ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() mids, err := h.info.AllMids(ctx) if err != nil { return "", fmt.Errorf("fetch mids: %w", err) } priceStr, ok := mids[coin] if !ok { return "", fmt.Errorf("coin %s not found", coin) } midPx, _ := strconv.ParseFloat(priceStr, 64) // Aggressive IOC: buy above market, sell below limitPx := midPx * 2.0 if !isBuy { limitPx = midPx * 0.5 } result, err := h.exchange.MarketOpen(ctx, coin, isBuy, size, &limitPx, 0.05, nil, nil) if err != nil { return "", fmt.Errorf("market open: %w", err) } // Marshal response respJSON, _ := json.Marshal(result) return string(respJSON), nil } // 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) } }