mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
go-hyperliquid v0.26.0 crashed at startup with
panic: runtime error: index out of range [479] with length 464
github.com/sonirico/go-hyperliquid.NewInfo (info.go:75)
NewExchange -> NewHyperliquidTrader -> AutoTrader.NewAutoTrader
-> TraderManager.LoadTradersFromStore -> main.main
The library's NewInfo built the spot-asset map by indexing
`spotMeta.Tokens[spotInfo.Tokens[0]]` directly, but Hyperliquid recently
added spot tokens whose Tokens[0] value (a logical token *index*, not an
array position) was larger than the Tokens slice length. With every
restart the backend panicked before the API server bound, so the
frontend's `/api/*` proxy got connection refused on every poll and the
dashboard rendered "全是 error" toasts.
v0.36 fixes the panic by building `tokensByIndex map[int]SpotTokenInfo`
and looking up by logical index instead of position. Adopting v0.36
required two small adaptations to our wrapper:
- trader/hyperliquid/trader.go: NewExchange grew an extra `perpDexs
*MixedArray` argument. Passing `nil` keeps the existing
"auto-fetch on first use" behavior.
- trader/hyperliquid/trader_sync.go: `Info.NameToAsset(coin) int` was
renamed to `Info.CoinToAsset(coin) (int, bool)` with an `ok` flag.
refreshMetaIfNeeded now treats `!ok || assetID == 0` as "needs
refresh" (the same semantic as the old `assetID == 0`).
Verified: backend rebuilds cleanly, container is healthy, all
Hyperliquid traders load, AI cycles execute, and Hyperliquid order
sync receives the full historical trade window.
152 lines
4.2 KiB
Go
152 lines
4.2 KiB
Go
package hyperliquid
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"nofx/logger"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// refreshMetaIfNeeded refreshes meta information when invalid (triggered when Asset ID is 0).
|
|
//
|
|
// NOTE: go-hyperliquid v0.27 renamed `NameToAsset(coin) int` to
|
|
// `CoinToAsset(coin) (int, bool)` — the second return is `ok` for whether
|
|
// the coin exists in the meta cache. We treat `!ok` (or zero asset for a
|
|
// perp) as "need refresh" the same way the old code did.
|
|
func (t *HyperliquidTrader) refreshMetaIfNeeded(coin string) error {
|
|
if assetID, ok := t.exchange.Info().CoinToAsset(coin); ok && assetID != 0 {
|
|
return nil // Meta is normal, no refresh needed
|
|
}
|
|
|
|
logger.Infof("⚠️ Asset ID for %s is 0, attempting to refresh Meta information...", coin)
|
|
|
|
// Refresh Meta information
|
|
meta, err := t.exchange.Info().Meta(t.ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to refresh Meta information: %w", err)
|
|
}
|
|
|
|
// Concurrency safe: Use write lock to protect meta field update
|
|
t.metaMutex.Lock()
|
|
t.meta = meta
|
|
t.metaMutex.Unlock()
|
|
|
|
logger.Infof("✅ Meta information refreshed, contains %d assets", len(meta.Universe))
|
|
|
|
// Verify Asset ID after refresh
|
|
assetID, ok := t.exchange.Info().CoinToAsset(coin)
|
|
if !ok || assetID == 0 {
|
|
return fmt.Errorf("❌ Even after refreshing Meta, Asset ID for %s is still 0. Possible reasons:\n"+
|
|
" 1. This coin is not listed on Hyperliquid\n"+
|
|
" 2. Coin name is incorrect (should be BTC not BTCUSDT)\n"+
|
|
" 3. API connection issue", coin)
|
|
}
|
|
|
|
logger.Infof("✅ Asset ID check passed after refresh: %s -> %d", coin, assetID)
|
|
return nil
|
|
}
|
|
|
|
// fetchXyzMeta fetches metadata for xyz dex assets (stocks, forex, commodities)
|
|
func (t *HyperliquidTrader) fetchXyzMeta() error {
|
|
// Build request for xyz dex meta
|
|
reqBody := map[string]string{
|
|
"type": "meta",
|
|
"dex": "xyz",
|
|
}
|
|
|
|
jsonBody, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
apiURL := "https://api.hyperliquid.xyz/info"
|
|
|
|
req, err := http.NewRequestWithContext(t.ctx, "POST", apiURL, bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{Timeout: 30 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("xyz dex meta API error (status %d): %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var meta xyzDexMeta
|
|
if err := json.Unmarshal(body, &meta); err != nil {
|
|
return fmt.Errorf("failed to parse response: %w", err)
|
|
}
|
|
|
|
t.xyzMetaMutex.Lock()
|
|
t.xyzMeta = &meta
|
|
t.xyzMetaMutex.Unlock()
|
|
|
|
logger.Infof("✅ xyz dex meta fetched, contains %d assets", len(meta.Universe))
|
|
return nil
|
|
}
|
|
|
|
// getXyzSzDecimals gets quantity precision for xyz dex asset
|
|
func (t *HyperliquidTrader) getXyzSzDecimals(coin string) int {
|
|
t.xyzMetaMutex.RLock()
|
|
defer t.xyzMetaMutex.RUnlock()
|
|
|
|
if t.xyzMeta == nil {
|
|
logger.Infof("⚠️ xyz meta information is empty, using default precision 2")
|
|
return 2 // Default precision for stocks/forex
|
|
}
|
|
|
|
// The meta API returns names with xyz: prefix, so ensure we match correctly
|
|
lookupName := coin
|
|
if !strings.HasPrefix(lookupName, "xyz:") {
|
|
lookupName = "xyz:" + lookupName
|
|
}
|
|
|
|
// Find corresponding asset in xyzMeta.Universe
|
|
for _, asset := range t.xyzMeta.Universe {
|
|
if asset.Name == lookupName {
|
|
return asset.SzDecimals
|
|
}
|
|
}
|
|
|
|
logger.Infof("⚠️ Precision information not found for %s, using default precision 2", lookupName)
|
|
return 2 // Default precision for stocks/forex
|
|
}
|
|
|
|
// getXyzAssetIndex gets the asset index for an xyz dex asset
|
|
func (t *HyperliquidTrader) getXyzAssetIndex(baseCoin string) int {
|
|
t.xyzMetaMutex.RLock()
|
|
defer t.xyzMetaMutex.RUnlock()
|
|
|
|
if t.xyzMeta == nil {
|
|
return -1
|
|
}
|
|
|
|
// The meta API returns names with xyz: prefix, so ensure we match correctly
|
|
lookupName := baseCoin
|
|
if !strings.HasPrefix(lookupName, "xyz:") {
|
|
lookupName = "xyz:" + lookupName
|
|
}
|
|
|
|
for i, asset := range t.xyzMeta.Universe {
|
|
if asset.Name == lookupName {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|