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" ) 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("to ECDSA: %w", err) } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() info := hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil) return &HyperLiquidTrade{ privateKey: privKey, mainAddress: mainAddress, info: info, configured: true, }, nil } func (h *HyperLiquidTrade) initExchange() error { if h.exchange != nil { return nil } if !h.configured { return fmt.Errorf("HL 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("meta: %w", err) } spotMeta, err := h.info.SpotMeta(ctx) if err != nil { return fmt.Errorf("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 } func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, error) { if !h.configured { return "", fmt.Errorf("HL not configured") } if err := h.initExchange(); err != nil { return "", fmt.Errorf("init: %w", err) } isBuy := side == "buy" size, _ := strconv.ParseFloat(sz, 64) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() mids, err := h.info.AllMids(ctx) if err != nil { return "", fmt.Errorf("mids: %w", err) } priceStr, ok := mids[coin] if !ok { return "", fmt.Errorf("coin %s not found", coin) } midPx, _ := strconv.ParseFloat(priceStr, 64) 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) } respJSON, _ := json.Marshal(result) return string(respJSON), nil } func (h *HyperLiquidTrade) GetBalance() (float64, error) { if !h.configured { return 0, fmt.Errorf("HL not configured") } ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() // HL testnet USDC is on spot, not perp. Use SpotUserState. state, err := h.info.SpotUserState(ctx, h.mainAddress) if err != nil { return 0, fmt.Errorf("spot user state: %w", err) } for _, b := range state.Balances { if b.Coin == "USDC" { total, _ := strconv.ParseFloat(b.Total, 64) hold, _ := strconv.ParseFloat(b.Hold, 64) return total - hold, nil } } return 0, fmt.Errorf("USDC balance not found in spot state") } 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 "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) } }