CORE-647: Hyperliquid WS tickers — bbo bid/ask, allMids+allDexsAssetCtxs, scoped error handling#5
Closed
ntemireyeva wants to merge 11 commits into
Closed
CORE-647: Hyperliquid WS tickers — bbo bid/ask, allMids+allDexsAssetCtxs, scoped error handling#5ntemireyeva wants to merge 11 commits into
ntemireyeva wants to merge 11 commits into
Conversation
- Map dex-scoped allMids/allDexsAssetCtxs/webData2 error frames to
messageHash 'tickers:<dex>' and subscribeHash '<subType>:<dex>' when
the embedded subscription request carries a dex, matching the
dex-aware registration scheme used by watchTickers.
- Bound the embedded-JSON slice by the last closing brace instead of
taking everything after the first '{', so trailing text after the
request payload no longer breaks parsing.
- Guard the bbo/l2Book branches: log and return early when the
embedded subscription has no coin instead of synthesizing a bogus
market/messageHash from an undefined coin.
- Move the ExchangeError construction past the bracketIndex/parseJson
early-return checks so it isn't built and discarded on log-only
paths.
- drop sameTypeOnly (4th arg) from marketSymbols in watchBidsAsks/unWatchBidsAsks so a single call can mix swap+spot symbols, matching watchTickers' usage and HL's bbo channel (which has no type restriction) - add handleBidAskUnsubscription + a 'bbo' branch in handleSubscriptionResponse so an unsubscribe ack for bbo cleans client.subscriptions/this.bidsasks and resolves the 'unsubscribe:bidsasks' messageHash; previously unWatchBidsAsks never resolved
…utures on unwatch
…Go future-leak fix)
…n unwatch cleanup Fix round 4 (two-reviewer convergence on the multi-symbol subscribe loop): - watchBidsAsks now dict-checks client.subscriptions per coin before calling watchMultiple; steady-state calls (already-subscribed coins) do zero watchMultiple calls in the loop and exactly one final shared-future await. Frame-sending watchMultiple calls happen only for genuinely new subscriptions. Eliminates the per-call 542x Future.race wrapper churn on every steady-state hot-loop invocation. - handleBidAskUnsubscription now rejects the shared 'bidsasks' future with UnsubscribeError when the last remaining 'bbo:' subscription is torn down, so a concurrently pending watchBidsAsks settles instead of hanging forever. - unWatchBidsAsks JSDoc @returns corrected to describe the actual boolean result. Empirically verified (isolated Client/Future harness, not shipped) that a discarded per-coin wrapper future crashes the process outright if the shared future rejects while it's pending, and that this is the same class of risk every bare `this.spawn (...)` call already carries throughout this codebase (no exchange file has ever needed a closure-free way to silence a discarded promise). Kept the bounded, now-documented sequential discard for new- subscription bursts rather than introducing an unprecedented `.catch(...)` construct whose cross-language (Go/Python/PHP) transpilation is unverified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ta2 removed) Hyperliquid's 2026-07-11 network upgrade removed the webData2 WS channel, leaving watchTickers dark from birth on new connections. Rework the method onto the channels that still exist: - default path subscribes allMids (last/close, ~5s) + allDexsAssetCtxs (volumes/prevDayPx-derived fields, ~15s) under the shared 'tickers' messageHash; channel='allMids' opts out of the ctxs frame; channel='webData2' throws NotSupported. hip3 dex path unchanged (allMids-with-dex under 'tickers:'+dex). - steady-state discipline mirrors watchBidsAsks: subscribe frames are sent only for hashes absent from client.subscriptions; every call awaits ONE shared watchMultiple future (no per-call Future.race wrapper churn). - new handleAllDexsAssetCtxs zips main-dex ctxs by index against options['perpUniverse'] (captured in fetchSwapMarkets — ctx rows carry no coin identity); length mismatch drops the frame and logs instead of mis-assigning volumes. - new mergeWsTicker does field-level merges into this.tickers so an allMids-only resolve never nulls ctxs-owned fields (the two sources own disjoint field groups on one ordered socket). - new resolveWsCoin maps WS spot keys '@n' via options['spotIndexMap'] (captured in fetchSpotMarkets) before falling back to coinToMarketId; swept into parseWsBidAsk, handleBidAskUnsubscription and the l2Book error-mapper branch. Builder-dex '#NNNN' mids keys are skipped. - unWatchTickers unsubscribes both channels sequentially, each awaited on its own 'unsubscribe:'-prefixed hash; handleTickersUnsubscription rejects the shared tickers future only when BOTH default subscriptions are gone, and purges this.tickers entries only for the dex-scoped case (the default cache is shared with markets still fed by the surviving channel). - handleMessage routing: 'webData2' removed, 'allDexsAssetCtxs' added; handleErrorMessage keeps its webData2 attribution branch for historical frames. Live-verified against api.hyperliquid.xyz: perp merge invariant held over 60s (quoteVolume never nulled by 16 subsequent allMids resolves), spot HYPE/USDC last via @107, channel gating, and full unwatch/rewatch cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ional unsubscribe, method-aware aggregate errors
…allDexsAssetCtxs - ts/src/pro/test/test.hyperliquid.ts (new, npm run test-hyperliquid-ws): stub-client fixture tests feeding captured HL frame shapes into handleMessage() — bbo (perp + spot "@n"), allMids ("@n"/"#" keys + last-only merge invariant), allDexsAssetCtxs (happy + length-mismatch drop), and error-frame scoped reject incl. method-aware subscribe/unsubscribe hash attribution. - ts/src/pro/hyperliquid.ts: replaced retMsg.lastIndexOf ('}') with an equivalent forward scan — the Go transpiler has no mapping for String.lastIndexOf (unlike .indexOf) and emitted an uncompilable direct method call on the any-typed string; a loop counter seeded from a computed value also transpiles as `any`, so native i-- fails too, hence the plain literal-seeded forward loop. - go/v4/{hyperliquid.go,hyperliquid_wrapper.go,pro/hyperliquid.go,pro/hyperliquid_wrapper.go}: regenerated via transpileGO + buildGO. Confirms HandleErrorMessage passes a real ccxt.ExchangeError value into Reject (not a raw string — the Task 1 panic fix landing in Go) and that WatchBidsAsks/UnWatchBidsAsks/HandleBidAsk/ HandleAllDexsAssetCtxs/ResolveWsCoin all transpiled and compile cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Hyperliquid removed the
webData2WS channel in its 2026-07-11 network upgrade. This fork'swatchTickersstill subscribed to it, so every HL ticker stream dies at connect (the error frame also blanket-rejected all in-flight futures and could panic the generated Go client). This PR rebuilds HL ticker data on live channels and hardens the WS error path.What
watchBidsAsks(new): real best bid/ask via per-coinbbosubscriptions (perp + spot, mixed in one call). All subscriptions resolve through one sharedbidsasksfuture — deliberately NOT per-symbol futures: in the generated Go, per-symbolFutureRacewrappers never unlink losing subscribers, which leaks blocked goroutines in hot consumer loops (verified against a 5-minute 100-symbol concurrent-loop run: goroutine count flat). Steady-state calls allocate exactly one future.unWatchBidsAsksawaits each coin's ownunsubscribe:bbo:<coin>ack sequentially.watchTickersrework: default =allMids(last/mid, all coins incl. spot@Nindices via a new spot index map) +allDexsAssetCtxs(perpdayNtlVlm/dayBaseVlm/prevDayPx, zipped by index against the perp universe with a length guard — mismatched frames are dropped and a guarded forcedloadMarkets(true)self-heal re-captures the universe). Field-level merge intothis.tickersbuilds a fresh object per update (no in-place mutation — avoids map races in Go consumers).channel: 'allMids'subscribes mids only;channel: 'webData2'now throwsNotSupported(breaking for anyone passing it explicitly).ExchangeErrorinstances (never raw strings — the GoClient.Rejecttype-assertserr.(error)). Unattributable/unparseable frames log and reject nothing. Failed subscription keys are cleaned so retries re-send frames instead of hanging.npm run test-hyperliquid-ws, 30 assertions over captured live frames: bbo perp/spot parse, allMids merge invariants, ctxs length-mismatch drop, scoped/method-aware error attribution).test-types-goandtest-base-ws-gogreen. Live-verified againstwss://api.hyperliquid.xyz/wsthroughout (streaming, unwatch/rewatch cycles, mixed-type calls, dead-channel rejection).transpileGO); one TS accommodation:String.lastIndexOfhas no transpiler mapping, replaced with a forward scan (semantics identical).Notes for reviewers
ts/src/pro/hyperliquid.ts+ts/src/hyperliquid.tsare the human-authored changes; the 4go/files are transpiler output.watchBidsAsksresolves per bbo update with a one-entry dict; when filtering by a requested symbol subset the result can be an empty dict — consumers should loop.🤖 Generated with Claude Code