From 23c58aaace3388df3eead96ac52513c0a6db8d2a Mon Sep 17 00:00:00 2001 From: RWCS-LTD Date: Sat, 30 May 2026 00:59:56 +0000 Subject: [PATCH] Fix KeyError when spot token index is missing from tokens list When the HL API briefly returns a universe pair whose token index is not present in spot_meta["tokens"], Info.__init__ raises KeyError and every client crashes at startup. Observed in production 2026-05-29: HL added spot pair @367 referencing token index 479 while the tokens list held 464 entries. This took down every running trader/poller using this SDK for ~54 minutes until HL repaired the response shape. The fix: - Skip universe pairs whose base or quote indices are not in `token_by_index`. This lets Info() initialize for all valid pairs and keeps clients alive through any future universe/tokens drift on the API side. New valid pairs come back into scope automatically as soon as their tokens land in the list. tests/test_info_spot_tokens.py reproduces the scenario with a synthetic spot_meta and asserts Info() no longer raises while still registering the valid pair. --- hyperliquid/info.py | 7 ++++++ tests/test_info_spot_tokens.py | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/test_info_spot_tokens.py diff --git a/hyperliquid/info.py b/hyperliquid/info.py index 360d0588..11733961 100644 --- a/hyperliquid/info.py +++ b/hyperliquid/info.py @@ -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"] diff --git a/tests/test_info_spot_tokens.py b/tests/test_info_spot_tokens.py new file mode 100644 index 00000000..fc0ea688 --- /dev/null +++ b/tests/test_info_spot_tokens.py @@ -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