Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions hyperliquid/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ def __init__(
self.coin_to_asset[spot_info["name"]] = asset
self.name_to_coin[spot_info["name"]] = spot_info["name"]
base, quote = spot_info["tokens"]
# Skip pairs whose token indices are not in the tokens list.
# The HL API can return a universe/tokens pair that is briefly out
# of sync — a new spot pair referencing a token index not yet
# present in the tokens list. Without this guard, Info() raises
# KeyError and every client crashes until HL repairs the response.
if base not in token_by_index or quote not in token_by_index:
continue
base_info = token_by_index[base]
quote_info = token_by_index[quote]
self.asset_to_sz_decimals[asset] = base_info["szDecimals"]
Expand Down
40 changes: 40 additions & 0 deletions tests/test_info_spot_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Regression test for Info.__init__ bounds check on spot_meta tokens.

Background: when the HL API briefly returns a universe pair whose token
index is not in `spot_meta["tokens"]`, Info.__init__ raises (KeyError after
the dict-based refactor; IndexError before it) and every client crashes at
startup. The fix skips such pairs so Info() can initialize for all valid
pairs and stays useful until HL repairs the response.
"""

from hyperliquid.info import Info
from hyperliquid.utils.types import Meta, SpotMeta

EMPTY_META: Meta = {"universe": []}


def test_info_init_skips_universe_pair_with_unknown_token_index():
# Tokens carry indices 0 and 1.
# Universe has one valid pair (BTC/USDC) and one malformed pair pointing at
# token index 5, which does not exist. Pre-patch this raised at startup.
spot_meta: SpotMeta = {
"tokens": [
{"name": "BTC", "szDecimals": 5, "weiDecimals": 8, "index": 0,
"tokenId": "0x" + "00" * 16, "isCanonical": True,
"evmContract": None, "fullName": None, "deployerTradingFeeShare": "0.0"},
{"name": "USDC", "szDecimals": 8, "weiDecimals": 8, "index": 1,
"tokenId": "0x" + "11" * 16, "isCanonical": True,
"evmContract": None, "fullName": None, "deployerTradingFeeShare": "0.0"},
],
"universe": [
{"name": "BTC_USDC", "tokens": [0, 1], "index": 0, "isCanonical": True},
{"name": "@367", "tokens": [5, 1], "index": 1, "isCanonical": False},
],
}

# Should not raise. Valid pair is registered; malformed pair is skipped.
info = Info(skip_ws=True, meta=EMPTY_META, spot_meta=spot_meta)
assert info.coin_to_asset["BTC_USDC"] == 10000
# Only valid pairs contribute to asset_to_sz_decimals.
assert info.asset_to_sz_decimals[10000] == 5
assert 10001 not in info.asset_to_sz_decimals