From 133ef51de8f1b27b720b3683122da5c6cd8875ac Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Thu, 11 Jun 2026 21:33:55 +0800 Subject: [PATCH] fix(hyperliquid): stop SDK init panic from crashing trader creation with 500 go-hyperliquid's NewExchange auto-fetches meta/spotMeta/perpDexs and panics if any of those API calls fail (NewInfo: panic(err)). The quick trade flow constructs a probe trader inside POST /api/traders, so any transient Hyperliquid API hiccup crashed the request with a recovered panic and a bare 500. Wrap the constructor in initExchangeClient, which converts the panic into an error that surfaces through the existing exchange-probe validation path as an honest, retryable message. --- trader/hyperliquid/client_init.go | 22 +++++++++++++++ trader/hyperliquid/client_init_test.go | 37 ++++++++++++++++++++++++++ trader/hyperliquid/trader.go | 27 ++++++++++++------- 3 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 trader/hyperliquid/client_init.go create mode 100644 trader/hyperliquid/client_init_test.go diff --git a/trader/hyperliquid/client_init.go b/trader/hyperliquid/client_init.go new file mode 100644 index 00000000..374b4249 --- /dev/null +++ b/trader/hyperliquid/client_init.go @@ -0,0 +1,22 @@ +package hyperliquid + +import ( + "fmt" + + hl "github.com/sonirico/go-hyperliquid" +) + +// initExchangeClient runs the SDK exchange constructor and converts its +// panic-on-failure behavior into an error. go-hyperliquid's NewExchange +// auto-fetches meta/spotMeta/perpDexs when they are passed as nil and panics +// if any of those API calls fail (NewInfo: panic(err)), so a transient +// Hyperliquid API hiccup would otherwise crash the calling HTTP handler +// with a 500. +func initExchangeClient(build func() *hl.Exchange) (ex *hl.Exchange, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("hyperliquid client initialization failed: %v", r) + } + }() + return build(), nil +} diff --git a/trader/hyperliquid/client_init_test.go b/trader/hyperliquid/client_init_test.go new file mode 100644 index 00000000..8154af60 --- /dev/null +++ b/trader/hyperliquid/client_init_test.go @@ -0,0 +1,37 @@ +package hyperliquid + +import ( + "errors" + "strings" + "testing" + + hl "github.com/sonirico/go-hyperliquid" +) + +func TestInitExchangeClientConvertsPanicToError(t *testing.T) { + // The SDK constructor panics when its automatic meta fetch fails + // (go-hyperliquid info.go NewInfo: panic(err)). The wrapper must turn + // that into an error instead of crashing the HTTP handler. + _, err := initExchangeClient(func() *hl.Exchange { + panic(errors.New("failed to fetch meta: API error 0: ")) + }) + if err == nil { + t.Fatal("expected error when the SDK constructor panics, got nil") + } + if !strings.Contains(err.Error(), "failed to fetch meta") { + t.Errorf("error should carry the panic cause, got %q", err.Error()) + } +} + +func TestInitExchangeClientPassesThroughSuccess(t *testing.T) { + want := &hl.Exchange{} + got, err := initExchangeClient(func() *hl.Exchange { + return want + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != want { + t.Fatal("wrapper must return the constructed exchange unchanged") + } +} diff --git a/trader/hyperliquid/trader.go b/trader/hyperliquid/trader.go index e6ab93e8..3214de5a 100644 --- a/trader/hyperliquid/trader.go +++ b/trader/hyperliquid/trader.go @@ -143,16 +143,23 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool, // v0.36 fixed the spot-meta indexing panic that crashed earlier versions // when Hyperliquid added new spot tokens whose Tokens[0] index pointed // past the Tokens array end. - exchange := hyperliquid.NewExchange( - ctx, - privateKey, - apiURL, - nil, // Meta — fetched automatically - "", // vault address (empty for personal account) - walletAddr, // wallet address - nil, // SpotMeta — fetched automatically - nil, // perpDexs — fetched automatically - ) + // The constructor still panics if any auto-fetch fails, so it runs inside + // initExchangeClient which converts the panic into a returned error. + exchange, err := initExchangeClient(func() *hyperliquid.Exchange { + return hyperliquid.NewExchange( + ctx, + privateKey, + apiURL, + nil, // Meta — fetched automatically + "", // vault address (empty for personal account) + walletAddr, // wallet address + nil, // SpotMeta — fetched automatically + nil, // perpDexs — fetched automatically + ) + }) + if err != nil { + return nil, fmt.Errorf("Hyperliquid API is temporarily unavailable, please retry: %w", err) + } logger.Infof("✓ Hyperliquid trader initialized successfully (testnet=%v, wallet=%s)", testnet, walletAddr)