feat: HL EVM signing via sonirico/go-hyperliquid SDK - testnet orders work
This commit is contained in:
@@ -21,7 +21,7 @@ type HyperLiquidTrade struct {
|
||||
exchange *hl.Exchange
|
||||
info *hl.Info
|
||||
privateKey *ecdsa.PrivateKey
|
||||
mainAddress string // main account address
|
||||
mainAddress string
|
||||
nonceMu sync.Mutex
|
||||
lastNonce int64
|
||||
configured bool
|
||||
@@ -32,7 +32,6 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL
|
||||
return &HyperLiquidTrade{}, nil
|
||||
}
|
||||
|
||||
// Parse ECDSA private key (supports 0x prefix)
|
||||
keyHex := strings.TrimPrefix(privateKeyHex, "0x")
|
||||
keyBytes, err := hex.DecodeString(keyHex)
|
||||
if err != nil {
|
||||
@@ -44,24 +43,20 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL
|
||||
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 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)
|
||||
|
||||
// Initialize exchange lazily on first order
|
||||
return t, nil
|
||||
}
|
||||
|
||||
@@ -69,53 +64,37 @@ 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)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
meta, err := h.fetchMeta(ctx)
|
||||
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,
|
||||
"", // vault
|
||||
h.mainAddress, // account address
|
||||
nil, // spot meta
|
||||
nil, // perp dex
|
||||
"",
|
||||
h.mainAddress,
|
||||
spotMeta,
|
||||
nil,
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -131,58 +110,42 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro
|
||||
}
|
||||
|
||||
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
|
||||
// Get current price for slippage
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
allMids, err := h.fetchAllMids(ctx)
|
||||
mids, err := h.info.AllMids(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch prices: %w", err)
|
||||
return "", fmt.Errorf("fetch mids: %w", err)
|
||||
}
|
||||
|
||||
priceStr, ok := allMids[coin]
|
||||
priceStr, ok := mids[coin]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("coin %s not found in allMids", coin)
|
||||
return "", fmt.Errorf("coin %s not found", 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
|
||||
// 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 order: %w", err)
|
||||
return "", fmt.Errorf("market open: %w", err)
|
||||
}
|
||||
|
||||
// Serialize response
|
||||
// Marshal 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
|
||||
|
||||
Reference in New Issue
Block a user