async-hyperliquid 0.4.3__tar.gz → 0.4.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/PKG-INFO +2 -1
  2. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/pyproject.toml +2 -1
  3. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/_async_hyperliquid/actions.py +23 -45
  4. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/_async_hyperliquid/core.py +83 -39
  5. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/_async_hyperliquid/info.py +60 -32
  6. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/_async_hyperliquid/orders.py +71 -64
  7. async_hyperliquid-0.4.5/src/async_hyperliquid/async_api.py +85 -0
  8. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/exchange.py +14 -7
  9. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/info.py +7 -18
  10. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/utils/signing.py +111 -70
  11. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/utils/types.py +1 -5
  12. async_hyperliquid-0.4.3/src/async_hyperliquid/async_api.py +0 -51
  13. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/LICENSE +0 -0
  14. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/README.md +0 -0
  15. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/__init__.py +0 -0
  16. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/_async_hyperliquid/__init__.py +0 -0
  17. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/async_hyperliquid.py +0 -0
  18. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/utils/__init__.py +0 -0
  19. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/utils/constants.py +0 -0
  20. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/utils/decorators.py +0 -0
  21. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.5}/src/async_hyperliquid/utils/miscs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: async-hyperliquid
3
- Version: 0.4.3
3
+ Version: 0.4.5
4
4
  Summary: Async Hyperliquid client using aiohttp
5
5
  Keywords: dex,hyperliquid,async,aiohttp,trading,cryptocurrency,defi
6
6
  Author: Yuki
@@ -48,6 +48,7 @@ Requires-Dist: msgpack>=1.1.0,<2.0.0
48
48
  Requires-Dist: eth-account>=0.13.5,<0.14.0
49
49
  Requires-Dist: eth-utils>=5.2.0,<6.0.0
50
50
  Requires-Dist: hl-web3>=0.1.0
51
+ Requires-Dist: coincurve>=21.0.0
51
52
  Requires-Python: >=3.10, <4
52
53
  Project-URL: Changelog, https://github.com/traderfiapp/async-hyperliquid/blob/master/CHANGELOG.md
53
54
  Project-URL: Documentation, https://github.com/traderfiapp/async-hyperliquid
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "async-hyperliquid"
3
- version = "0.4.3"
3
+ version = "0.4.5"
4
4
  description = "Async Hyperliquid client using aiohttp"
5
5
  authors = [{ name = "Yuki", email = "yuqi.lyle@gmail.com" }]
6
6
  readme = "README.md"
@@ -40,6 +40,7 @@ dependencies = [
40
40
  "eth-account (>=0.13.5,<0.14.0)",
41
41
  "eth-utils (>=5.2.0,<6.0.0)",
42
42
  "hl-web3>=0.1.0",
43
+ "coincurve>=21.0.0",
43
44
  ]
44
45
 
45
46
  [dependency-groups]
@@ -2,13 +2,9 @@ import math
2
2
  import re
3
3
  import warnings
4
4
 
5
- from async_hyperliquid.utils.constants import (
6
- HYPE_FACTOR,
7
- MAINNET_API_URL,
8
- USD_FACTOR,
9
- )
5
+ from async_hyperliquid.utils.constants import HYPE_FACTOR, MAINNET_API_URL, USD_FACTOR
10
6
  from async_hyperliquid.utils.decorators import private_key_required
11
- from async_hyperliquid.utils.miscs import get_timestamp_ms, round_token_amount
7
+ from async_hyperliquid.utils.miscs import round_token_amount
12
8
  from async_hyperliquid.utils.signing import (
13
9
  sign_approve_agent_action,
14
10
  sign_approve_builder_fee_action,
@@ -41,7 +37,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
41
37
 
42
38
  @private_key_required
43
39
  async def usd_transfer(self, amount: float, dest: str):
44
- nonce = get_timestamp_ms()
40
+ nonce = self.next_nonce()
45
41
  action = {
46
42
  "type": "usdSend",
47
43
  "amount": round_token_amount(amount, 2),
@@ -59,7 +55,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
59
55
  token_id = token_info["tokenId"]
60
56
  wei_decimals = token_info["weiDecimals"]
61
57
  token = f"{token_name}:{token_id}"
62
- nonce = get_timestamp_ms()
58
+ nonce = self.next_nonce()
63
59
  action = {
64
60
  "type": "spotSend",
65
61
  "destination": dest,
@@ -72,7 +68,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
72
68
 
73
69
  @private_key_required
74
70
  async def initiate_withdrawal(self, amount: float):
75
- nonce = get_timestamp_ms()
71
+ nonce = self.next_nonce()
76
72
  action = {
77
73
  "type": "withdraw3",
78
74
  "amount": round_token_amount(amount, 2),
@@ -84,7 +80,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
84
80
 
85
81
  @private_key_required
86
82
  async def usd_class_transfer(self, amount: float, to_perp: bool = False):
87
- nonce = get_timestamp_ms()
83
+ nonce = self.next_nonce()
88
84
  action = {
89
85
  "type": "usdClassTransfer",
90
86
  "amount": round_token_amount(amount, 2),
@@ -111,7 +107,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
111
107
  token_id = token_info["tokenId"]
112
108
  wei_decimals = token_info["weiDecimals"]
113
109
  token = f"{token_name}:{token_id}"
114
- nonce = get_timestamp_ms()
110
+ nonce = self.next_nonce()
115
111
  action = {
116
112
  "type": "sendAsset",
117
113
  "token": token,
@@ -128,7 +124,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
128
124
  @private_key_required
129
125
  async def staking_deposit(self, amount: float):
130
126
  amount_in_wei = int(math.floor(amount * HYPE_FACTOR))
131
- nonce = get_timestamp_ms()
127
+ nonce = self.next_nonce()
132
128
  action = {"type": "cDeposit", "wei": amount_in_wei, "nonce": nonce}
133
129
  sig = sign_staking_deposit_action(self.account, action, self.is_mainnet)
134
130
  return await self.exchange.post_action_with_sig(action, sig, nonce)
@@ -136,11 +132,9 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
136
132
  @private_key_required
137
133
  async def staking_withdraw(self, amount: float):
138
134
  amount_in_wei = int(math.floor(amount * HYPE_FACTOR))
139
- nonce = get_timestamp_ms()
135
+ nonce = self.next_nonce()
140
136
  action = {"type": "cWithdraw", "wei": amount_in_wei, "nonce": nonce}
141
- sig = sign_staking_withdraw_action(
142
- self.account, action, self.is_mainnet
143
- )
137
+ sig = sign_staking_withdraw_action(self.account, action, self.is_mainnet)
144
138
  return await self.exchange.post_action_with_sig(action, sig, nonce)
145
139
 
146
140
  @private_key_required
@@ -148,7 +142,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
148
142
  self, validator: str, amount: float, is_undelegate: bool = False
149
143
  ):
150
144
  amount_in_wei = int(math.floor(amount * HYPE_FACTOR))
151
- nonce = get_timestamp_ms()
145
+ nonce = self.next_nonce()
152
146
  action = {
153
147
  "type": "tokenDelegate",
154
148
  "validator": validator,
@@ -160,9 +154,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
160
154
  return await self.exchange.post_action_with_sig(action, sig, nonce)
161
155
 
162
156
  @private_key_required
163
- async def vault_transfer(
164
- self, vault: str, amount: float, is_deposit: bool = True
165
- ):
157
+ async def vault_transfer(self, vault: str, amount: float, is_deposit: bool = True):
166
158
  usd_amount = int(math.floor(amount * USD_FACTOR))
167
159
  action = {
168
160
  "type": "vaultTransfer",
@@ -173,7 +165,7 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
173
165
  return await self.exchange.post_action(action)
174
166
 
175
167
  async def approve_agent(self, agent: str, name: str | None = None):
176
- nonce = get_timestamp_ms()
168
+ nonce = self.next_nonce()
177
169
  action = {
178
170
  "type": "approveAgent",
179
171
  "agentAddress": agent,
@@ -187,26 +179,20 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
187
179
  return await self.exchange.post_action_with_sig(action, sig, nonce)
188
180
 
189
181
  async def approve_builder_fee(self, max_fee_rate: float, builder: str):
190
- nonce = get_timestamp_ms()
182
+ nonce = self.next_nonce()
191
183
  action = {
192
184
  "type": "approveBuilderFee",
193
185
  "maxFeeRate": f"{max_fee_rate:.3%}",
194
186
  "builder": builder,
195
187
  "nonce": nonce,
196
188
  }
197
- sig = sign_approve_builder_fee_action(
198
- self.account, action, self.is_mainnet
199
- )
189
+ sig = sign_approve_builder_fee_action(self.account, action, self.is_mainnet)
200
190
  return await self.exchange.post_action_with_sig(action, sig, nonce)
201
191
 
202
192
  async def convert_to_multi_sig_user(self, users: list[str], threshold: int):
203
- nonce = get_timestamp_ms()
193
+ nonce = self.next_nonce()
204
194
  signers = {"authorizedUsers": sorted(users), "threshold": threshold}
205
- action = {
206
- "type": "convertToMultiSigUser",
207
- "signers": signers,
208
- "nonce": nonce,
209
- }
195
+ action = {"type": "convertToMultiSigUser", "signers": signers, "nonce": nonce}
210
196
  sig = sign_convert_to_multi_sig_user_action(
211
197
  self.account, action, self.is_mainnet
212
198
  )
@@ -220,16 +206,14 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
220
206
  action = {"type": "evmUserModify", "usingBigBlocks": enable}
221
207
  return await self.exchange.post_action(action)
222
208
 
223
- async def user_dex_abstraction(
224
- self, user: str | None = None, enabled: bool = True
225
- ):
209
+ async def user_dex_abstraction(self, user: str | None = None, enabled: bool = True):
226
210
  warnings.warn(
227
211
  "user_dex_abstraction is deprecated and may be removed in a "
228
212
  "future release.",
229
213
  DeprecationWarning,
230
214
  stacklevel=2,
231
215
  )
232
- nonce = get_timestamp_ms()
216
+ nonce = self.next_nonce()
233
217
  if user is None:
234
218
  user = self.address
235
219
  action = {
@@ -238,30 +222,24 @@ class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
238
222
  "enabled": enabled,
239
223
  "nonce": nonce,
240
224
  }
241
- sig = sign_user_dex_abstraction_action(
242
- self.account, action, self.is_mainnet
243
- )
225
+ sig = sign_user_dex_abstraction_action(self.account, action, self.is_mainnet)
244
226
  return await self.exchange.post_action_with_sig(action, sig, nonce)
245
227
 
246
228
  async def user_set_abstraction(
247
229
  self, abstraction: UserSetAbstraction, user: str | None = None
248
230
  ):
249
- nonce = get_timestamp_ms()
250
231
  if user is None:
251
232
  user = self.address
252
233
  if re.fullmatch(r"0x[a-fA-F0-9]{40}", user) is None:
253
- raise ValueError(
254
- f"user must be a 42-char hex address, got: {user!r}"
255
- )
234
+ raise ValueError(f"user must be a 42-char hex address, got: {user!r}")
235
+ nonce = self.next_nonce()
256
236
  action = {
257
237
  "type": "userSetAbstraction",
258
238
  "user": user.lower(),
259
239
  "abstraction": abstraction,
260
240
  "nonce": nonce,
261
241
  }
262
- sig = sign_user_set_abstraction_action(
263
- self.account, action, self.is_mainnet
264
- )
242
+ sig = sign_user_set_abstraction_action(self.account, action, self.is_mainnet)
265
243
  return await self.exchange.post_action_with_sig(action, sig, nonce)
266
244
 
267
245
  async def agent_enable_dex_abstraction(self):
@@ -1,27 +1,24 @@
1
1
  import asyncio
2
+ from threading import Lock
2
3
 
3
- from aiohttp import ClientSession, ClientTimeout
4
+ from aiohttp import TCPConnector, BaseConnector, ClientSession, ClientTimeout
4
5
  from eth_account import Account
5
- from eth_account.signers.local import LocalAccount
6
- from hl_web3.exchange import Exchange as EVMExchange
7
6
  from hl_web3.info import Info as EVMInfo
7
+ from hl_web3.exchange import Exchange as EVMExchange
8
8
  from hl_web3.utils.constants import HL_RPC_URL, HL_TESTNET_RPC_URL
9
+ from eth_account.signers.local import LocalAccount
9
10
 
10
- from async_hyperliquid.async_api import AsyncAPI
11
- from async_hyperliquid.exchange import ExchangeAPI
12
11
  from async_hyperliquid.info import InfoAPI
12
+ from async_hyperliquid.exchange import ExchangeAPI
13
+ from async_hyperliquid.async_api import AsyncAPI
14
+ from async_hyperliquid.utils.miscs import get_timestamp_ms
15
+ from async_hyperliquid.utils.types import Metas, PerpMeta, SpotMeta, SpotTokenMeta
13
16
  from async_hyperliquid.utils.constants import (
17
+ SPOT_OFFSET,
14
18
  MAINNET_API_URL,
15
19
  PERP_DEX_OFFSET,
16
- SPOT_OFFSET,
17
20
  TESTNET_API_URL,
18
21
  )
19
- from async_hyperliquid.utils.types import (
20
- Metas,
21
- PerpMeta,
22
- SpotMeta,
23
- SpotTokenMeta,
24
- )
25
22
 
26
23
 
27
24
  class AsyncHyperliquidCore(AsyncAPI):
@@ -54,15 +51,27 @@ class AsyncHyperliquidCore(AsyncAPI):
54
51
  private_key: str | None = None,
55
52
  vault: str | None = None,
56
53
  perp_dexs: list[str] = [""],
54
+ session: ClientSession | None = None,
55
+ timeout: ClientTimeout | None = None,
56
+ connector: BaseConnector | None = None,
57
57
  ):
58
58
  self.address = address
59
59
  self.is_mainnet = is_mainnet
60
60
  self.account = Account.from_key(api_key)
61
- self.session = ClientSession(timeout=ClientTimeout(connect=3))
61
+ self._nonce_lock = Lock()
62
+ self._last_nonce = 0
63
+ self._owns_session = session is None
64
+ self.session = session or self._build_session(
65
+ timeout=timeout, connector=connector
66
+ )
62
67
  self.base_url = MAINNET_API_URL if is_mainnet else TESTNET_API_URL
63
68
  self.info = InfoAPI(self.base_url, self.session)
64
69
  self.exchange = ExchangeAPI(
65
- self.account, self.session, self.base_url, address=self.address
70
+ self.account,
71
+ self.session,
72
+ self.base_url,
73
+ address=self.address,
74
+ nonce_factory=self.next_nonce,
66
75
  )
67
76
 
68
77
  self.coin_assets = {}
@@ -81,6 +90,25 @@ class AsyncHyperliquidCore(AsyncAPI):
81
90
  def set_expires(self, expires: int | None) -> None:
82
91
  self.expires = expires
83
92
 
93
+ def _build_session(
94
+ self, *, timeout: ClientTimeout | None, connector: BaseConnector | None
95
+ ) -> ClientSession:
96
+ resolved_timeout = timeout or ClientTimeout(
97
+ connect=3, sock_connect=3, sock_read=10
98
+ )
99
+ resolved_connector = connector or TCPConnector(
100
+ ttl_dns_cache=300, enable_cleanup_closed=True
101
+ )
102
+ return ClientSession(timeout=resolved_timeout, connector=resolved_connector)
103
+
104
+ def next_nonce(self) -> int:
105
+ with self._nonce_lock:
106
+ nonce = get_timestamp_ms()
107
+ if nonce <= self._last_nonce:
108
+ nonce = self._last_nonce + 1
109
+ self._last_nonce = nonce
110
+ return nonce
111
+
84
112
  def _init_evm_client(
85
113
  self, private_key: str | None, rpc_url: str | None = None
86
114
  ) -> None:
@@ -91,9 +119,7 @@ class AsyncHyperliquidCore(AsyncAPI):
91
119
 
92
120
  if private_key is None:
93
121
  if self.account.address != self.address:
94
- raise ValueError(
95
- "EVM Exchange client can not init without private key"
96
- )
122
+ raise ValueError("EVM Exchange client can not init without private key")
97
123
  private_key = self.account.key.hex()
98
124
 
99
125
  self.evm_exchange = EVMExchange(rpc_url, private_key)
@@ -107,7 +133,8 @@ class AsyncHyperliquidCore(AsyncAPI):
107
133
  self.asset_sz_decimals[asset] = info["szDecimals"]
108
134
 
109
135
  def _init_spot_meta(self, meta: SpotMeta) -> None:
110
- total_tokens = len(meta["tokens"])
136
+ tokens = meta["tokens"]
137
+ total_tokens = len(tokens)
111
138
  for info in meta["universe"]:
112
139
  asset = info["index"] + SPOT_OFFSET
113
140
  asset_name = info["name"]
@@ -116,19 +143,20 @@ class AsyncHyperliquidCore(AsyncAPI):
116
143
  self.coin_names[asset_name] = asset_name
117
144
 
118
145
  base, quote = info["tokens"]
119
- if base >= total_tokens or quote >= total_tokens:
120
- print("Unreconized token index for: ", info)
146
+ if not 0 <= base < total_tokens or not 0 <= quote < total_tokens:
121
147
  continue
122
148
 
123
- base_info = meta["tokens"][base]
149
+ base_info = tokens[base]
124
150
  base_name = base_info["name"]
125
- quote_name = meta["tokens"][quote]["name"]
151
+ quote_info = tokens[quote]
152
+ quote_name = quote_info["name"]
126
153
  name = f"{base_name}/{quote_name}"
127
- if name not in self.coin_names:
128
- self.coin_names[name] = asset_name
154
+ self.coin_names.setdefault(name, asset_name)
155
+ self.coin_names.setdefault(quote_name, quote_name)
129
156
 
130
157
  self.asset_sz_decimals[asset] = base_info["szDecimals"]
131
- self.spot_tokens[asset_name] = meta["tokens"][base]
158
+ self.spot_tokens[asset_name] = base_info
159
+ self.spot_tokens.setdefault(quote_name, quote_info)
132
160
 
133
161
  def _update_coin_symbols(self) -> None:
134
162
  self.coin_symbols = {
@@ -136,6 +164,13 @@ class AsyncHyperliquidCore(AsyncAPI):
136
164
  }
137
165
 
138
166
  async def init_metas(self) -> None:
167
+ # TODO: Add HIP-4 outcome meta initialization from the spot info
168
+ # `outcomeMeta` endpoint once outcomes move beyond testnet-only rollout.
169
+ # Outcome asset IDs do not follow the current perp/spot offset scheme:
170
+ # `encoding = 10 * outcome + side`, coin names use `#{encoding}`, token
171
+ # names use `+{encoding}`, and `asset_id = 100_000_000 + encoding`, so
172
+ # this path will need dedicated outcome mappings instead of reusing the
173
+ # existing perp/spot logic.
139
174
  meta_task = self.info.get_perp_meta()
140
175
  spot_meta_task = self.info.get_spot_meta()
141
176
  all_dex_names_task = self.get_all_dex_name()
@@ -171,24 +206,31 @@ class AsyncHyperliquidCore(AsyncAPI):
171
206
 
172
207
  async def get_metas(self, perp_only: bool = False) -> Metas:
173
208
  metas: Metas = {"perp": {}, "spot": [], "dexs": {}} # type: ignore
174
- perp_meta = await self.info.get_perp_meta()
175
209
  if perp_only:
176
- metas["perp"] = perp_meta
210
+ metas["perp"] = await self.info.get_perp_meta()
177
211
  return metas
178
212
 
179
- metas["spot"] = await self.info.get_spot_meta()
213
+ perp_meta, spot_meta = await asyncio.gather(
214
+ self.info.get_perp_meta(), self.info.get_spot_meta()
215
+ )
216
+ metas["perp"] = perp_meta
217
+ metas["spot"] = spot_meta
180
218
  return metas
181
219
 
182
220
  async def get_all_metas(self) -> Metas:
183
- dexs = await self.get_all_dex_name()
184
- dex_metas = {}
185
-
186
- for dex in dexs[1:]:
187
- meta = await self.info.get_perp_meta(dex)
188
- dex_metas[dex] = meta
189
-
190
- spot_meta = await self.info.get_spot_meta()
191
- perp_meta = await self.info.get_perp_meta()
221
+ dexs, perp_meta, spot_meta = await asyncio.gather(
222
+ self.get_all_dex_name(),
223
+ self.info.get_perp_meta(),
224
+ self.info.get_spot_meta(),
225
+ )
226
+ dex_metas: dict[str, PerpMeta] = {}
227
+ if len(dexs) > 1:
228
+ dex_meta_results = await asyncio.gather(
229
+ *(self.info.get_perp_meta(dex) for dex in dexs[1:])
230
+ )
231
+ dex_metas = {
232
+ dex: meta for dex, meta in zip(dexs[1:], dex_meta_results, strict=True)
233
+ }
192
234
  return {"perp": perp_meta, "spot": spot_meta, "dexs": dex_metas}
193
235
 
194
236
  async def get_all_dex_name(self) -> list[str]:
@@ -212,7 +254,6 @@ class AsyncHyperliquidCore(AsyncAPI):
212
254
 
213
255
  async def get_coin_asset(self, coin: str) -> int:
214
256
  coin_name = await self.get_coin_name(coin)
215
-
216
257
  if coin_name not in self.coin_assets:
217
258
  raise ValueError(f"Coin {coin}({coin_name}) not found")
218
259
 
@@ -224,7 +265,10 @@ class AsyncHyperliquidCore(AsyncAPI):
224
265
 
225
266
  async def get_coin_sz_decimals(self, coin: str) -> int:
226
267
  coin_name = await self.get_coin_name(coin)
227
- asset = await self.get_coin_asset(coin_name)
268
+ if coin_name not in self.coin_assets:
269
+ raise ValueError(f"Coin {coin}({coin_name}) not found")
270
+
271
+ asset = self.coin_assets[coin_name]
228
272
  return self.asset_sz_decimals[asset]
229
273
 
230
274
  async def get_token_info(self, coin: str) -> SpotTokenMeta:
@@ -1,21 +1,21 @@
1
1
  import asyncio
2
2
  import warnings
3
- from typing import Literal
3
+ from typing import Literal, cast
4
4
 
5
- from async_hyperliquid.utils.constants import (
6
- ONE_HOUR_MS,
7
- PERP_DEX_OFFSET,
8
- SPOT_OFFSET,
9
- )
5
+ from async_hyperliquid.utils.constants import ONE_HOUR_MS, PERP_DEX_OFFSET, SPOT_OFFSET
10
6
  from async_hyperliquid.utils.miscs import get_coin_dex, get_timestamp_ms
11
7
  from async_hyperliquid.utils.types import (
12
8
  Abstraction,
13
9
  AccountState,
14
10
  ClearinghouseState,
15
11
  OrderWithStatus,
12
+ PerpMeta,
13
+ PerpMetaCtxItem,
16
14
  Portfolio,
17
15
  Position,
16
+ SpotMeta,
18
17
  SpotClearinghouseState,
18
+ SpotMetaCtxItem,
19
19
  UserDeposit,
20
20
  UserNonFundingDelta,
21
21
  UserOpenOrders,
@@ -27,13 +27,49 @@ from .core import AsyncHyperliquidCore
27
27
 
28
28
 
29
29
  class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
30
+ async def _get_perp_mark_price(self, coin_name: str, dex: str = "") -> float:
31
+ meta_ctx = await self.info.get_perp_meta_ctx(dex)
32
+ meta = cast(PerpMeta, meta_ctx[0])
33
+ asset_ctxs = cast(list[PerpMetaCtxItem], meta_ctx[1])
34
+
35
+ for idx, info in enumerate(meta["universe"]):
36
+ if info["name"] == coin_name:
37
+ return float(asset_ctxs[idx]["markPx"])
38
+
39
+ raise ValueError(f"Coin {coin_name} not found in perp dex '{dex}'")
40
+
41
+ async def _get_spot_mark_price(self, coin_name: str) -> float:
42
+ meta_ctx = await self.info.get_spot_meta_ctx()
43
+ meta = cast(SpotMeta, meta_ctx[0])
44
+ asset_ctxs = cast(list[SpotMetaCtxItem], meta_ctx[1])
45
+
46
+ for info in meta["universe"]:
47
+ if info["name"] == coin_name:
48
+ asset_index = info["index"]
49
+ return float(asset_ctxs[asset_index]["markPx"])
50
+
51
+ raise ValueError(f"Coin {coin_name} not found in spot metadata")
52
+
53
+ async def get_mark_price(self, coin: str) -> float:
54
+ if ":" in coin:
55
+ return await self._get_perp_mark_price(coin, get_coin_dex(coin))
56
+
57
+ coin_name = await self.get_coin_name(coin)
58
+ if coin_name not in self.coin_assets:
59
+ raise ValueError(f"Coin {coin}({coin_name}) not found")
60
+
61
+ asset = self.coin_assets[coin_name]
62
+ is_spot_asset = SPOT_OFFSET <= asset < PERP_DEX_OFFSET
63
+ if is_spot_asset:
64
+ return await self._get_spot_mark_price(coin_name)
65
+
66
+ return await self._get_perp_mark_price(coin_name, get_coin_dex(coin_name))
67
+
30
68
  async def get_market_price(self, coin: str) -> float:
31
69
  warnings.warn(
32
70
  "get_market_price is deprecated and will remove in the future, use get_mid_price instead"
33
71
  )
34
- coin_name = await self.get_coin_name(coin)
35
- market_prices = await self.get_all_market_prices()
36
- return market_prices[coin_name]
72
+ return await self.get_mark_price(coin)
37
73
 
38
74
  async def get_all_market_prices(
39
75
  self, market: Literal["spot", "perp", "all"] = "all"
@@ -50,10 +86,15 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
50
86
  await self.init_metas()
51
87
  spot_data = None
52
88
  perp_data = None
53
- if is_spot or is_all:
54
- spot_data = await self.info.get_spot_meta_ctx()
55
- if is_perp or is_all:
56
- perp_data = await self.info.get_perp_meta_ctx()
89
+ if is_all:
90
+ spot_data, perp_data = await asyncio.gather(
91
+ self.info.get_spot_meta_ctx(), self.info.get_perp_meta_ctx()
92
+ )
93
+ else:
94
+ if is_spot:
95
+ spot_data = await self.info.get_spot_meta_ctx()
96
+ if is_perp:
97
+ perp_data = await self.info.get_perp_meta_ctx()
57
98
 
58
99
  is_perp = is_perp or is_all
59
100
  is_spot = is_spot or is_all
@@ -102,9 +143,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
102
143
  address = self.address
103
144
  return await self.info.get_spot_clearinghouse_state(address)
104
145
 
105
- async def get_account_state(
106
- self, address: str | None = None
107
- ) -> AccountState:
146
+ async def get_account_state(self, address: str | None = None) -> AccountState:
108
147
  if not address:
109
148
  address = self.address
110
149
 
@@ -127,9 +166,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
127
166
 
128
167
  return account_state
129
168
 
130
- async def get_account_portfolio(
131
- self, address: str | None = None
132
- ) -> Portfolio:
169
+ async def get_account_portfolio(self, address: str | None = None) -> Portfolio:
133
170
  if not address:
134
171
  address = self.address
135
172
 
@@ -158,9 +195,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
158
195
  start_time: int | None = None,
159
196
  end_time: int | None = None,
160
197
  ) -> list[UserDeposit]:
161
- return await self.get_latest_ledgers(
162
- "deposit", address, start_time, end_time
163
- ) # type: ignore
198
+ return await self.get_latest_ledgers("deposit", address, start_time, end_time) # type: ignore
164
199
 
165
200
  async def get_latest_withdraws(
166
201
  self,
@@ -168,9 +203,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
168
203
  start_time: int | None = None,
169
204
  end_time: int | None = None,
170
205
  ) -> list[UserWithdraw]:
171
- return await self.get_latest_ledgers(
172
- "withdraw", address, start_time, end_time
173
- ) # type: ignore
206
+ return await self.get_latest_ledgers("withdraw", address, start_time, end_time) # type: ignore
174
207
 
175
208
  async def get_latest_transfers(
176
209
  self,
@@ -183,10 +216,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
183
216
  ) # type: ignore
184
217
 
185
218
  async def get_user_open_orders(
186
- self,
187
- address: str | None = None,
188
- is_frontend: bool = False,
189
- dex: str = "",
219
+ self, address: str | None = None, is_frontend: bool = False, dex: str = ""
190
220
  ) -> UserOpenOrders:
191
221
  if not address:
192
222
  address = self.address
@@ -226,9 +256,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
226
256
  positions.extend(result)
227
257
  return positions
228
258
 
229
- async def get_user_abstraction(
230
- self, address: str | None = None
231
- ) -> Abstraction:
259
+ async def get_user_abstraction(self, address: str | None = None) -> Abstraction:
232
260
  if not address:
233
261
  address = self.address
234
262
  return await self.info.get_user_abstraction(address)