async-hyperliquid 0.4.3__tar.gz → 0.4.4__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.4}/PKG-INFO +2 -1
  2. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/pyproject.toml +2 -1
  3. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/_async_hyperliquid/actions.py +23 -45
  4. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/_async_hyperliquid/core.py +76 -39
  5. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/_async_hyperliquid/info.py +16 -28
  6. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/_async_hyperliquid/orders.py +71 -64
  7. async_hyperliquid-0.4.4/src/async_hyperliquid/async_api.py +85 -0
  8. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/exchange.py +14 -7
  9. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/info.py +5 -16
  10. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/signing.py +111 -70
  11. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/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.4}/LICENSE +0 -0
  14. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/README.md +0 -0
  15. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/__init__.py +0 -0
  16. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/_async_hyperliquid/__init__.py +0 -0
  17. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/async_hyperliquid.py +0 -0
  18. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/__init__.py +0 -0
  19. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/constants.py +0 -0
  20. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/decorators.py +0 -0
  21. {async_hyperliquid-0.4.3 → async_hyperliquid-0.4.4}/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.4
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.4"
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 = {
@@ -171,24 +199,31 @@ class AsyncHyperliquidCore(AsyncAPI):
171
199
 
172
200
  async def get_metas(self, perp_only: bool = False) -> Metas:
173
201
  metas: Metas = {"perp": {}, "spot": [], "dexs": {}} # type: ignore
174
- perp_meta = await self.info.get_perp_meta()
175
202
  if perp_only:
176
- metas["perp"] = perp_meta
203
+ metas["perp"] = await self.info.get_perp_meta()
177
204
  return metas
178
205
 
179
- metas["spot"] = await self.info.get_spot_meta()
206
+ perp_meta, spot_meta = await asyncio.gather(
207
+ self.info.get_perp_meta(), self.info.get_spot_meta()
208
+ )
209
+ metas["perp"] = perp_meta
210
+ metas["spot"] = spot_meta
180
211
  return metas
181
212
 
182
213
  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()
214
+ dexs, perp_meta, spot_meta = await asyncio.gather(
215
+ self.get_all_dex_name(),
216
+ self.info.get_perp_meta(),
217
+ self.info.get_spot_meta(),
218
+ )
219
+ dex_metas: dict[str, PerpMeta] = {}
220
+ if len(dexs) > 1:
221
+ dex_meta_results = await asyncio.gather(
222
+ *(self.info.get_perp_meta(dex) for dex in dexs[1:])
223
+ )
224
+ dex_metas = {
225
+ dex: meta for dex, meta in zip(dexs[1:], dex_meta_results, strict=True)
226
+ }
192
227
  return {"perp": perp_meta, "spot": spot_meta, "dexs": dex_metas}
193
228
 
194
229
  async def get_all_dex_name(self) -> list[str]:
@@ -212,7 +247,6 @@ class AsyncHyperliquidCore(AsyncAPI):
212
247
 
213
248
  async def get_coin_asset(self, coin: str) -> int:
214
249
  coin_name = await self.get_coin_name(coin)
215
-
216
250
  if coin_name not in self.coin_assets:
217
251
  raise ValueError(f"Coin {coin}({coin_name}) not found")
218
252
 
@@ -224,7 +258,10 @@ class AsyncHyperliquidCore(AsyncAPI):
224
258
 
225
259
  async def get_coin_sz_decimals(self, coin: str) -> int:
226
260
  coin_name = await self.get_coin_name(coin)
227
- asset = await self.get_coin_asset(coin_name)
261
+ if coin_name not in self.coin_assets:
262
+ raise ValueError(f"Coin {coin}({coin_name}) not found")
263
+
264
+ asset = self.coin_assets[coin_name]
228
265
  return self.asset_sz_decimals[asset]
229
266
 
230
267
  async def get_token_info(self, coin: str) -> SpotTokenMeta:
@@ -2,11 +2,7 @@ import asyncio
2
2
  import warnings
3
3
  from typing import Literal
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,
@@ -50,10 +46,15 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
50
46
  await self.init_metas()
51
47
  spot_data = None
52
48
  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()
49
+ if is_all:
50
+ spot_data, perp_data = await asyncio.gather(
51
+ self.info.get_spot_meta_ctx(), self.info.get_perp_meta_ctx()
52
+ )
53
+ else:
54
+ if is_spot:
55
+ spot_data = await self.info.get_spot_meta_ctx()
56
+ if is_perp:
57
+ perp_data = await self.info.get_perp_meta_ctx()
57
58
 
58
59
  is_perp = is_perp or is_all
59
60
  is_spot = is_spot or is_all
@@ -102,9 +103,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
102
103
  address = self.address
103
104
  return await self.info.get_spot_clearinghouse_state(address)
104
105
 
105
- async def get_account_state(
106
- self, address: str | None = None
107
- ) -> AccountState:
106
+ async def get_account_state(self, address: str | None = None) -> AccountState:
108
107
  if not address:
109
108
  address = self.address
110
109
 
@@ -127,9 +126,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
127
126
 
128
127
  return account_state
129
128
 
130
- async def get_account_portfolio(
131
- self, address: str | None = None
132
- ) -> Portfolio:
129
+ async def get_account_portfolio(self, address: str | None = None) -> Portfolio:
133
130
  if not address:
134
131
  address = self.address
135
132
 
@@ -158,9 +155,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
158
155
  start_time: int | None = None,
159
156
  end_time: int | None = None,
160
157
  ) -> list[UserDeposit]:
161
- return await self.get_latest_ledgers(
162
- "deposit", address, start_time, end_time
163
- ) # type: ignore
158
+ return await self.get_latest_ledgers("deposit", address, start_time, end_time) # type: ignore
164
159
 
165
160
  async def get_latest_withdraws(
166
161
  self,
@@ -168,9 +163,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
168
163
  start_time: int | None = None,
169
164
  end_time: int | None = None,
170
165
  ) -> list[UserWithdraw]:
171
- return await self.get_latest_ledgers(
172
- "withdraw", address, start_time, end_time
173
- ) # type: ignore
166
+ return await self.get_latest_ledgers("withdraw", address, start_time, end_time) # type: ignore
174
167
 
175
168
  async def get_latest_transfers(
176
169
  self,
@@ -183,10 +176,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
183
176
  ) # type: ignore
184
177
 
185
178
  async def get_user_open_orders(
186
- self,
187
- address: str | None = None,
188
- is_frontend: bool = False,
189
- dex: str = "",
179
+ self, address: str | None = None, is_frontend: bool = False, dex: str = ""
190
180
  ) -> UserOpenOrders:
191
181
  if not address:
192
182
  address = self.address
@@ -226,9 +216,7 @@ class AsyncHyperliquidInfoClient(AsyncHyperliquidCore):
226
216
  positions.extend(result)
227
217
  return positions
228
218
 
229
- async def get_user_abstraction(
230
- self, address: str | None = None
231
- ) -> Abstraction:
219
+ async def get_user_abstraction(self, address: str | None = None) -> Abstraction:
232
220
  if not address:
233
221
  address = self.address
234
222
  return await self.info.get_user_abstraction(address)
@@ -1,10 +1,7 @@
1
+ import asyncio
1
2
  import math
2
3
 
3
- from async_hyperliquid.utils.constants import (
4
- PERP_DEX_OFFSET,
5
- SPOT_OFFSET,
6
- USD_FACTOR,
7
- )
4
+ from async_hyperliquid.utils.constants import PERP_DEX_OFFSET, SPOT_OFFSET, USD_FACTOR
8
5
  from async_hyperliquid.utils.miscs import get_coin_dex, round_float, round_px
9
6
  from async_hyperliquid.utils.signing import encode_order, orders_to_action
10
7
  from async_hyperliquid.utils.types import (
@@ -24,9 +21,13 @@ from .info import AsyncHyperliquidInfoClient
24
21
 
25
22
  class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
26
23
  async def _round_sz_px(self, coin: str, sz: float, px: float):
27
- asset = await self.get_coin_asset(coin)
24
+ coin_name = await self.get_coin_name(coin)
25
+ if coin_name not in self.coin_assets:
26
+ raise ValueError(f"Coin {coin}({coin_name}) not found")
27
+
28
+ asset = self.coin_assets[coin_name]
29
+ sz_decimals = self.asset_sz_decimals[asset]
28
30
  is_spot = asset >= SPOT_OFFSET and asset < PERP_DEX_OFFSET
29
- sz_decimals = await self.get_coin_sz_decimals(coin)
30
31
  px_decimals = (6 if not is_spot else 8) - sz_decimals
31
32
  return asset, round_float(sz, sz_decimals), round_px(px, px_decimals)
32
33
 
@@ -146,41 +147,44 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
146
147
  slippage: float = 0.05,
147
148
  builder: OrderBuilder | None = None,
148
149
  ):
149
- reqs = []
150
150
  if is_market:
151
151
  reqs = await self._get_batch_market_orders(orders, slippage)
152
152
  else:
153
- for o in orders:
154
- asset, sz, px = await self._round_sz_px(
155
- o["coin"], o["sz"], o["px"]
156
- )
157
- req = {**o, "asset": asset, "sz": sz, "px": px}
158
- reqs.append(req)
153
+ reqs = await self._get_batch_limit_orders(orders)
159
154
 
160
155
  return await self.place_orders(reqs, grouping=grouping, builder=builder)
161
156
 
157
+ async def _get_batch_limit_orders(self, orders: BatchPlaceOrderRequest):
158
+ rounded_orders = await asyncio.gather(
159
+ *(self._round_sz_px(o["coin"], o["sz"], o["px"]) for o in orders)
160
+ )
161
+ return [
162
+ {**order, "asset": asset, "sz": sz, "px": px}
163
+ for order, (asset, sz, px) in zip(orders, rounded_orders)
164
+ ]
165
+
162
166
  async def _get_batch_market_orders(
163
167
  self, orders: BatchPlaceOrderRequest, slippage: float = 0.05
164
168
  ):
165
- reqs = []
166
169
  dexs = list(set(get_coin_dex(o["coin"]) for o in orders))
167
170
  all_mids = await self.get_dexs_mids(dexs)
168
171
  order_type = limit_order_type(LimitTif.IOC)
169
- for o in orders:
170
- coin = o["coin"]
171
- market_price = all_mids[coin]
172
- slippage_factor = (1 + slippage) if o["is_buy"] else (1 - slippage)
173
- px = market_price * slippage_factor
174
- asset, sz, px = await self._round_sz_px(coin, o["sz"], px)
175
- req = {
176
- **o,
177
- "asset": asset,
178
- "sz": sz,
179
- "px": px,
180
- "order_type": order_type,
181
- }
182
- reqs.append(req)
183
- return reqs
172
+ quoted_prices = []
173
+ for order in orders:
174
+ market_price = all_mids[order["coin"]]
175
+ slippage_factor = (1 + slippage) if order["is_buy"] else (1 - slippage)
176
+ quoted_prices.append(market_price * slippage_factor)
177
+
178
+ rounded_orders = await asyncio.gather(
179
+ *(
180
+ self._round_sz_px(order["coin"], order["sz"], quoted_price)
181
+ for order, quoted_price in zip(orders, quoted_prices)
182
+ )
183
+ )
184
+ return [
185
+ {**order, "asset": asset, "sz": sz, "px": px, "order_type": order_type}
186
+ for order, (asset, sz, px) in zip(orders, rounded_orders)
187
+ ]
184
188
 
185
189
  async def cancel_order(self, coin: str, oid: int):
186
190
  return await self.cancel_orders([(coin, int(oid))])
@@ -189,11 +193,14 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
189
193
  return await self.cancel_orders(cancels)
190
194
 
191
195
  async def cancel_orders(self, cancels: BatchCancelRequest):
196
+ assets = await asyncio.gather(
197
+ *(self.get_coin_asset(coin) for coin, _ in cancels)
198
+ )
192
199
  action = {
193
200
  "type": "cancel",
194
201
  "cancels": [
195
- {"a": await self.get_coin_asset(coin), "o": oid}
196
- for coin, oid in cancels
202
+ {"a": asset, "o": oid}
203
+ for asset, (_, oid) in zip(assets, cancels, strict=True)
197
204
  ],
198
205
  }
199
206
 
@@ -205,14 +212,14 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
205
212
  return await self.batch_cancel_by_cloid([(coin, cloid)])
206
213
 
207
214
  async def batch_cancel_by_cloid(self, cancels: list[tuple[str, Cloid]]):
215
+ assets = await asyncio.gather(
216
+ *(self.get_coin_asset(coin) for coin, _ in cancels)
217
+ )
208
218
  action = {
209
219
  "type": "cancelByCloid",
210
220
  "cancels": [
211
- {
212
- "asset": await self.get_coin_asset(coin),
213
- "cloid": cloid.to_raw(),
214
- }
215
- for coin, cloid in cancels
221
+ {"asset": asset, "cloid": cloid.to_raw()}
222
+ for asset, (_, cloid) in zip(assets, cancels, strict=True)
216
223
  ],
217
224
  }
218
225
 
@@ -255,9 +262,7 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
255
262
  async def batch_modify_orders(self, modify_req: list[dict]):
256
263
  modifies = [
257
264
  {
258
- "oid": m["oid"].to_raw()
259
- if isinstance(m["oid"], Cloid)
260
- else m["oid"],
265
+ "oid": m["oid"].to_raw() if isinstance(m["oid"], Cloid) else m["oid"],
261
266
  "order": encode_order(m["order"]),
262
267
  }
263
268
  for m in modify_req
@@ -267,9 +272,7 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
267
272
  action, vault=self.vault, expires=self.expires
268
273
  )
269
274
 
270
- async def update_leverage(
271
- self, leverage: int, coin: str, is_cross: bool = True
272
- ):
275
+ async def update_leverage(self, leverage: int, coin: str, is_cross: bool = True):
273
276
  action = {
274
277
  "type": "updateLeverage",
275
278
  "asset": await self.get_coin_asset(coin),
@@ -355,29 +358,33 @@ class AsyncHyperliquidOrdersClient(AsyncHyperliquidInfoClient):
355
358
  async def close_dex_positions(self, dex: str):
356
359
  return await self.close_all_positions(dexs=[dex])
357
360
 
358
- async def close_position(self, coin: str):
359
- dex = get_coin_dex(coin)
360
- positions = await self.get_dex_positions(dex=dex)
361
- target = {}
361
+ async def close_positions(self, coins: list[str]):
362
+ if not coins:
363
+ return None
364
+
365
+ positions = await self.get_all_positions(
366
+ dexs=sorted({get_coin_dex(coin) for coin in coins})
367
+ )
368
+ targets = {coin: None for coin in coins}
362
369
  for position in positions:
363
- if coin == position["coin"]:
364
- target = position
370
+ coin = position["coin"]
371
+ if coin in targets:
372
+ targets[coin] = position
365
373
 
366
- if not target:
374
+ orders = []
375
+ for coin in coins:
376
+ target = targets[coin]
377
+ if target is None:
378
+ continue
379
+ size = float(target["szi"])
380
+ orders.append(
381
+ {"coin": coin, "is_buy": size < 0, "sz": abs(size), "px": 0, "ro": True}
382
+ )
383
+
384
+ if not orders:
367
385
  return None
368
386
 
369
- size = float(target["szi"])
370
- price = await self.get_mid_price(coin)
371
- if not price:
372
- raise ValueError(f"Failed to retrieve market price for {coin}")
373
-
374
- close_order = {
375
- "coin": coin,
376
- "is_buy": size < 0,
377
- "sz": abs(size),
378
- "px": price,
379
- "is_market": True,
380
- "ro": True,
381
- }
387
+ return await self.batch_place_orders(orders, is_market=True)
382
388
 
383
- return await self.place_order(**close_order) # type: ignore
389
+ async def close_position(self, coin: str):
390
+ return await self.close_positions([coin])
@@ -0,0 +1,85 @@
1
+ import logging
2
+ from types import TracebackType
3
+ from typing import Any
4
+ from traceback import TracebackException
5
+
6
+ from aiohttp import ClientSession
7
+
8
+ from async_hyperliquid.utils.types import Endpoint
9
+ from async_hyperliquid.utils.constants import MAINNET_API_URL
10
+
11
+ logger = logging.getLogger(__name__)
12
+ _REDACTED = "<redacted>"
13
+ _SENSITIVE_PAYLOAD_KEYS = frozenset({"signature", "signatures"})
14
+
15
+
16
+ def _redact_payload(payload: Any) -> Any:
17
+ if isinstance(payload, dict):
18
+ redacted: dict[str, Any] = {}
19
+ for key, value in payload.items():
20
+ if key in _SENSITIVE_PAYLOAD_KEYS:
21
+ redacted[key] = _REDACTED
22
+ elif key == "action" and isinstance(value, dict):
23
+ redacted[key] = {
24
+ "type": value.get("type"),
25
+ "keys": sorted(value.keys()),
26
+ }
27
+ else:
28
+ redacted[key] = _redact_payload(value)
29
+ return redacted
30
+
31
+ if isinstance(payload, list):
32
+ return [_redact_payload(item) for item in payload]
33
+
34
+ return payload
35
+
36
+
37
+ class AsyncAPI:
38
+ def __init__(
39
+ self,
40
+ endpoint: Endpoint,
41
+ base_url: str | None = None,
42
+ session: ClientSession = None, # type: ignore
43
+ *,
44
+ owns_session: bool = True,
45
+ ):
46
+ self.endpoint = endpoint
47
+ self.base_url = (base_url or MAINNET_API_URL).rstrip("/")
48
+ self.session = session
49
+ self._owns_session = owns_session
50
+ self._request_url = f"{self.base_url}/{self.endpoint.value}"
51
+
52
+ # for async with AsyncAPI() as api usage
53
+ async def __aenter__(self) -> "AsyncAPI":
54
+ return self
55
+
56
+ async def __aexit__(
57
+ self, exc_type: Exception, exc_val: TracebackException, traceback: TracebackType
58
+ ) -> None:
59
+ await self.close()
60
+
61
+ async def close(self) -> None:
62
+ if (
63
+ getattr(self, "_owns_session", True)
64
+ and self.session
65
+ and not self.session.closed
66
+ ):
67
+ await self.session.close()
68
+
69
+ async def post(self, payload: dict | None = None) -> Any:
70
+ if self.session is None:
71
+ raise RuntimeError("ClientSession is not initialized")
72
+
73
+ payload = payload or {}
74
+ if logger.isEnabledFor(logging.DEBUG):
75
+ logger.debug("POST %s %s", self._request_url, _redact_payload(payload))
76
+
77
+ async with self.session.post(self._request_url, json=payload) as resp:
78
+ resp.raise_for_status()
79
+ try:
80
+ return await resp.json()
81
+ except Exception as e:
82
+ logger.error(
83
+ "Error parsing JSON response from %s: %s", self._request_url, e
84
+ )
85
+ return await resp.text()
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from collections.abc import Callable
2
3
  from typing import Any
3
4
 
4
5
  from aiohttp import ClientSession
@@ -8,10 +9,7 @@ from async_hyperliquid.async_api import AsyncAPI
8
9
  from async_hyperliquid.utils.miscs import get_timestamp_ms
9
10
  from async_hyperliquid.utils.types import Endpoint, SignType
10
11
  from async_hyperliquid.utils.signing import sign_action, sign_multi_sig_action
11
- from async_hyperliquid.utils.constants import (
12
- MAINNET_API_URL,
13
- SIGNATURE_CHAIN_ID,
14
- )
12
+ from async_hyperliquid.utils.constants import MAINNET_API_URL, SIGNATURE_CHAIN_ID
15
13
 
16
14
  logger = logging.getLogger(__name__)
17
15
 
@@ -23,11 +21,13 @@ class ExchangeAPI(AsyncAPI):
23
21
  session: ClientSession,
24
22
  base_url: str | None = None,
25
23
  address: str | None = None,
24
+ nonce_factory: Callable[[], int] | None = None,
26
25
  ):
27
26
  self.account = account
28
27
  self.address = address or account.address
29
28
  self.is_mainnet = base_url == MAINNET_API_URL
30
- super().__init__(Endpoint.EXCHANGE, base_url, session)
29
+ self._next_nonce = nonce_factory or get_timestamp_ms
30
+ super().__init__(Endpoint.EXCHANGE, base_url, session, owns_session=False)
31
31
 
32
32
  async def multi_sig(
33
33
  self,
@@ -65,7 +65,7 @@ class ExchangeAPI(AsyncAPI):
65
65
  assert self.endpoint == Endpoint.EXCHANGE, (
66
66
  "only exchange endpoint supports action"
67
67
  )
68
- nonce = get_timestamp_ms()
68
+ nonce = self._next_nonce()
69
69
  # TODO: support multi sig
70
70
  signature = sign_action(
71
71
  self.account, action, vault, nonce, self.is_mainnet, expires
@@ -88,5 +88,12 @@ class ExchangeAPI(AsyncAPI):
88
88
  payload["vaultAddress"] = vault
89
89
  if expires:
90
90
  payload["expiresAfter"] = expires
91
- logger.debug(f"Post action payload: {payload}")
91
+ if logger.isEnabledFor(logging.DEBUG):
92
+ logger.debug(
93
+ "Post action type=%s nonce=%s vault=%s expires=%s",
94
+ action.get("type"),
95
+ nonce,
96
+ bool(vault),
97
+ expires,
98
+ )
92
99
  return await self.post(payload)
@@ -45,7 +45,7 @@ from async_hyperliquid.utils.types import (
45
45
 
46
46
  class InfoAPI(AsyncAPI):
47
47
  def __init__(self, base_url: str, session: ClientSession):
48
- super().__init__(Endpoint.INFO, base_url, session)
48
+ super().__init__(Endpoint.INFO, base_url, session, owns_session=False)
49
49
 
50
50
  async def get_all_mids(self, dex: str = "") -> dict[str, int]:
51
51
  payload = {"type": "allMids", "dex": dex}
@@ -86,12 +86,7 @@ class InfoAPI(AsyncAPI):
86
86
  async def get_order_status(
87
87
  self, order_id: str | int, address: str, dex: str = ""
88
88
  ) -> OrderWithStatus:
89
- payload = {
90
- "type": "orderStatus",
91
- "user": address,
92
- "oid": order_id,
93
- "dex": dex,
94
- }
89
+ payload = {"type": "orderStatus", "user": address, "oid": order_id, "dex": dex}
95
90
  return await self.post(payload)
96
91
 
97
92
  async def get_depth(
@@ -132,9 +127,7 @@ class InfoAPI(AsyncAPI):
132
127
  payload = {"type": "subAccounts", "user": address}
133
128
  return await self.post(payload)
134
129
 
135
- async def get_vault_info(
136
- self, address: str, user: str | None = None
137
- ) -> VaultInfo:
130
+ async def get_vault_info(self, address: str, user: str | None = None) -> VaultInfo:
138
131
  payload = {"type": "vaultDetails", "vaultAddress": address}
139
132
  if user:
140
133
  payload["user"] = user
@@ -219,9 +212,7 @@ class InfoAPI(AsyncAPI):
219
212
  is_funding: bool = True,
220
213
  ) -> UserFundings:
221
214
  payload = {
222
- "type": "userFunding"
223
- if is_funding
224
- else "userNonFundingLedgerUpdates",
215
+ "type": "userFunding" if is_funding else "userNonFundingLedgerUpdates",
225
216
  "user": address,
226
217
  "startTime": start_time,
227
218
  "endTime": end_time,
@@ -266,9 +257,7 @@ class InfoAPI(AsyncAPI):
266
257
  payload = {"type": "spotMetaAndAssetCtxs"}
267
258
  return await self.post(payload)
268
259
 
269
- async def get_user_token_balances(
270
- self, address: str
271
- ) -> SpotClearinghouseState:
260
+ async def get_user_token_balances(self, address: str) -> SpotClearinghouseState:
272
261
  return await self.get_spot_clearinghouse_state(address)
273
262
 
274
263
  async def get_spot_clearinghouse_state(
@@ -37,6 +37,37 @@ from async_hyperliquid.utils.constants import (
37
37
  CONVERT_TO_MULTI_SIG_USER_SIGN_TYPES,
38
38
  )
39
39
 
40
+ _EIP712_DOMAIN_FIELDS = [
41
+ {"name": "name", "type": "string"},
42
+ {"name": "version", "type": "string"},
43
+ {"name": "chainId", "type": "uint256"},
44
+ {"name": "verifyingContract", "type": "address"},
45
+ ]
46
+ _ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
47
+ _EXCHANGE_AGENT_DOMAIN = {
48
+ "chainId": 1337,
49
+ "name": "Exchange",
50
+ "verifyingContract": _ZERO_ADDRESS,
51
+ "version": "1",
52
+ }
53
+ _EXCHANGE_AGENT_MESSAGE_TYPES = {
54
+ "Agent": [
55
+ {"name": "source", "type": "string"},
56
+ {"name": "connectionId", "type": "bytes32"},
57
+ ]
58
+ }
59
+ _EXCHANGE_AGENT_PAYLOAD_BASE = {
60
+ "domain": _EXCHANGE_AGENT_DOMAIN,
61
+ "types": {
62
+ "Agent": _EXCHANGE_AGENT_MESSAGE_TYPES["Agent"],
63
+ "EIP712Domain": _EIP712_DOMAIN_FIELDS,
64
+ },
65
+ "primaryType": "Agent",
66
+ }
67
+ _USER_SIGNED_PAYLOAD_BASE_CACHE: dict[tuple[str, int, int], dict[str, Any]] = {}
68
+ _USER_SIGNED_DOMAIN_CACHE: dict[int, dict[str, Any]] = {}
69
+ _USER_SIGNED_MESSAGE_TYPES_CACHE: dict[tuple[str, int], dict[str, List[dict]]] = {}
70
+
40
71
 
41
72
  def address_to_bytes(address: str) -> bytes:
42
73
  return bytes.fromhex(address[2:] if address.startswith("0x") else address)
@@ -64,11 +95,67 @@ def hash_action(
64
95
  def sign_inner(wallet: LocalAccount, data: dict) -> SignedAction:
65
96
  encodes = encode_typed_data(full_message=data)
66
97
  signed = wallet.sign_message(encodes)
67
- return {
68
- "r": to_hex(signed["r"]),
69
- "s": to_hex(signed["s"]),
70
- "v": signed["v"],
71
- }
98
+ return {"r": to_hex(signed["r"]), "s": to_hex(signed["s"]), "v": signed["v"]}
99
+
100
+
101
+ def sign_typed_data(
102
+ wallet: LocalAccount,
103
+ domain_data: dict[str, Any],
104
+ message_types: dict[str, Any],
105
+ message_data: dict[str, Any],
106
+ ) -> SignedAction:
107
+ encodes = encode_typed_data(
108
+ domain_data=domain_data, message_types=message_types, message_data=message_data
109
+ )
110
+ signed = wallet.sign_message(encodes)
111
+ return {"r": to_hex(signed["r"]), "s": to_hex(signed["s"]), "v": signed["v"]}
112
+
113
+
114
+ def _user_signed_domain(chain_id: int) -> dict[str, Any]:
115
+ payload = _USER_SIGNED_DOMAIN_CACHE.get(chain_id)
116
+ if payload is None:
117
+ payload = {
118
+ "name": "HyperliquidSignTransaction",
119
+ "version": "1",
120
+ "chainId": chain_id,
121
+ "verifyingContract": _ZERO_ADDRESS,
122
+ }
123
+ _USER_SIGNED_DOMAIN_CACHE[chain_id] = payload
124
+ return payload
125
+
126
+
127
+ def _user_signed_message_types(
128
+ primary_type: str, payload_types: List[dict]
129
+ ) -> dict[str, List[dict]]:
130
+ cache_key = (primary_type, id(payload_types))
131
+ payload = _USER_SIGNED_MESSAGE_TYPES_CACHE.get(cache_key)
132
+ if payload is None:
133
+ payload = {primary_type: payload_types}
134
+ _USER_SIGNED_MESSAGE_TYPES_CACHE[cache_key] = payload
135
+ return payload
136
+
137
+
138
+ def _user_signed_payload_base(
139
+ primary_type: str, payload_types: List[dict], chain_id: int
140
+ ):
141
+ cache_key = (primary_type, id(payload_types), chain_id)
142
+ payload = _USER_SIGNED_PAYLOAD_BASE_CACHE.get(cache_key)
143
+ if payload is None:
144
+ payload = {
145
+ "domain": {
146
+ "name": "HyperliquidSignTransaction",
147
+ "version": "1",
148
+ "chainId": chain_id,
149
+ "verifyingContract": _ZERO_ADDRESS,
150
+ },
151
+ "types": {
152
+ primary_type: payload_types,
153
+ "EIP712Domain": _EIP712_DOMAIN_FIELDS,
154
+ },
155
+ "primaryType": primary_type,
156
+ }
157
+ _USER_SIGNED_PAYLOAD_BASE_CACHE[cache_key] = payload
158
+ return payload
72
159
 
73
160
 
74
161
  def sign_action(
@@ -81,29 +168,9 @@ def sign_action(
81
168
  ) -> SignedAction:
82
169
  h = hash_action(action, active_pool, nonce, expires)
83
170
  msg = {"source": "a" if is_mainnet else "b", "connectionId": h}
84
- data = {
85
- "domain": {
86
- "chainId": 1337,
87
- "name": "Exchange",
88
- "verifyingContract": "0x0000000000000000000000000000000000000000",
89
- "version": "1",
90
- },
91
- "types": {
92
- "Agent": [
93
- {"name": "source", "type": "string"},
94
- {"name": "connectionId", "type": "bytes32"},
95
- ],
96
- "EIP712Domain": [
97
- {"name": "name", "type": "string"},
98
- {"name": "version", "type": "string"},
99
- {"name": "chainId", "type": "uint256"},
100
- {"name": "verifyingContract", "type": "address"},
101
- ],
102
- },
103
- "primaryType": "Agent",
104
- "message": msg,
105
- }
106
- return sign_inner(wallet, data)
171
+ return sign_typed_data(
172
+ wallet, _EXCHANGE_AGENT_DOMAIN, _EXCHANGE_AGENT_MESSAGE_TYPES, msg
173
+ )
107
174
 
108
175
 
109
176
  def round_float(x: float) -> str:
@@ -124,9 +191,7 @@ def ensure_order_type(order_type: OrderType) -> OrderType:
124
191
  return {
125
192
  "trigger": {
126
193
  "isMarket": order_type["trigger"]["isMarket"],
127
- "triggerPx": round_float(
128
- float(order_type["trigger"]["triggerPx"])
129
- ),
194
+ "triggerPx": round_float(float(order_type["trigger"]["triggerPx"])),
130
195
  "tpsl": order_type["trigger"]["tpsl"],
131
196
  }
132
197
  }
@@ -168,22 +233,7 @@ def orders_to_action(
168
233
  def user_signed_payload(primary_type, payload_types, action):
169
234
  chain_id = int(action["signatureChainId"], 16)
170
235
  return {
171
- "domain": {
172
- "name": "HyperliquidSignTransaction",
173
- "version": "1",
174
- "chainId": chain_id,
175
- "verifyingContract": "0x0000000000000000000000000000000000000000",
176
- },
177
- "types": {
178
- primary_type: payload_types,
179
- "EIP712Domain": [
180
- {"name": "name", "type": "string"},
181
- {"name": "version", "type": "string"},
182
- {"name": "chainId", "type": "uint256"},
183
- {"name": "verifyingContract", "type": "address"},
184
- ],
185
- },
186
- "primaryType": primary_type,
236
+ **_user_signed_payload_base(primary_type, payload_types, chain_id),
187
237
  "message": action,
188
238
  }
189
239
 
@@ -197,8 +247,13 @@ def sign_user_signed_action(
197
247
  ):
198
248
  action["signatureChainId"] = SIGNATURE_CHAIN_ID
199
249
  action["hyperliquidChain"] = "Mainnet" if is_mainnet else "Testnet"
200
- data = user_signed_payload(primary_type, payload_types, action)
201
- return sign_inner(wallet, data)
250
+ chain_id = int(action["signatureChainId"], 16)
251
+ return sign_typed_data(
252
+ wallet,
253
+ _user_signed_domain(chain_id),
254
+ _user_signed_message_types(primary_type, payload_types),
255
+ action,
256
+ )
202
257
 
203
258
 
204
259
  def sign_usd_transfer_action(wallet: LocalAccount, action, is_mainnet: bool):
@@ -211,9 +266,7 @@ def sign_usd_transfer_action(wallet: LocalAccount, action, is_mainnet: bool):
211
266
  )
212
267
 
213
268
 
214
- def sign_spot_transfer_action(
215
- wallet: LocalAccount, action: dict, is_mainnet: bool
216
- ):
269
+ def sign_spot_transfer_action(wallet: LocalAccount, action: dict, is_mainnet: bool):
217
270
  return sign_user_signed_action(
218
271
  wallet,
219
272
  action,
@@ -233,9 +286,7 @@ def sign_withdraw_action(wallet: LocalAccount, action: dict, is_mainnet: bool):
233
286
  )
234
287
 
235
288
 
236
- def sign_usd_class_transfer_action(
237
- wallet: LocalAccount, action: Any, is_mainnet: bool
238
- ):
289
+ def sign_usd_class_transfer_action(wallet: LocalAccount, action: Any, is_mainnet: bool):
239
290
  return sign_user_signed_action(
240
291
  wallet,
241
292
  action,
@@ -245,9 +296,7 @@ def sign_usd_class_transfer_action(
245
296
  )
246
297
 
247
298
 
248
- def sign_send_asset_action(
249
- wallet: LocalAccount, action: dict, is_mainnet: bool
250
- ):
299
+ def sign_send_asset_action(wallet: LocalAccount, action: dict, is_mainnet: bool):
251
300
  return sign_user_signed_action(
252
301
  wallet,
253
302
  action,
@@ -257,9 +306,7 @@ def sign_send_asset_action(
257
306
  )
258
307
 
259
308
 
260
- def sign_staking_deposit_action(
261
- wallet: LocalAccount, action: dict, is_mainnet: bool
262
- ):
309
+ def sign_staking_deposit_action(wallet: LocalAccount, action: dict, is_mainnet: bool):
263
310
  return sign_user_signed_action(
264
311
  wallet,
265
312
  action,
@@ -269,9 +316,7 @@ def sign_staking_deposit_action(
269
316
  )
270
317
 
271
318
 
272
- def sign_staking_withdraw_action(
273
- wallet: LocalAccount, action: dict, is_mainnet: bool
274
- ):
319
+ def sign_staking_withdraw_action(wallet: LocalAccount, action: dict, is_mainnet: bool):
275
320
  return sign_user_signed_action(
276
321
  wallet,
277
322
  action,
@@ -281,9 +326,7 @@ def sign_staking_withdraw_action(
281
326
  )
282
327
 
283
328
 
284
- def sign_token_delegate_action(
285
- wallet: LocalAccount, action: dict, is_mainnet: bool
286
- ):
329
+ def sign_token_delegate_action(wallet: LocalAccount, action: dict, is_mainnet: bool):
287
330
  return sign_user_signed_action(
288
331
  wallet,
289
332
  action,
@@ -293,9 +336,7 @@ def sign_token_delegate_action(
293
336
  )
294
337
 
295
338
 
296
- def sign_approve_agent_action(
297
- wallet: LocalAccount, action: dict, is_mainnet: bool
298
- ):
339
+ def sign_approve_agent_action(wallet: LocalAccount, action: dict, is_mainnet: bool):
299
340
  return sign_user_signed_action(
300
341
  wallet,
301
342
  action,
@@ -690,11 +690,7 @@ class UserVaultWithdraw(TypedDict):
690
690
 
691
691
 
692
692
  UserNonFundingDelta = (
693
- UserDeposit
694
- | UserWithdraw
695
- | UserTransfer
696
- | UserVaultDeposit
697
- | UserVaultWithdraw
693
+ UserDeposit | UserWithdraw | UserTransfer | UserVaultDeposit | UserVaultWithdraw
698
694
  )
699
695
 
700
696
 
@@ -1,51 +0,0 @@
1
- import logging
2
- from types import TracebackType
3
- from typing import Any
4
- from traceback import TracebackException
5
-
6
- from aiohttp import ClientSession
7
-
8
- from async_hyperliquid.utils.types import Endpoint
9
- from async_hyperliquid.utils.constants import MAINNET_API_URL
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- class AsyncAPI:
15
- def __init__(
16
- self,
17
- endpoint: Endpoint,
18
- base_url: str | None = None,
19
- session: ClientSession = None, # type: ignore
20
- ):
21
- self.endpoint = endpoint
22
- self.base_url = base_url or MAINNET_API_URL
23
- self.session = session
24
-
25
- # for async with AsyncAPI() as api usage
26
- async def __aenter__(self) -> "AsyncAPI":
27
- return self
28
-
29
- async def __aexit__(
30
- self,
31
- exc_type: Exception,
32
- exc_val: TracebackException,
33
- traceback: TracebackType,
34
- ) -> None:
35
- await self.close()
36
-
37
- async def close(self) -> None:
38
- if self.session and not self.session.closed:
39
- await self.session.close()
40
-
41
- async def post(self, payload: dict | None = None) -> Any:
42
- payload = payload or {}
43
- req_path = f"{self.base_url}/{self.endpoint.value}"
44
- logger.debug(f"POST {req_path} {payload}")
45
- async with self.session.post(req_path, json=payload) as resp:
46
- resp.raise_for_status()
47
- try:
48
- return await resp.json()
49
- except Exception as e:
50
- logger.error(f"Error parsing JSON response: {e}")
51
- return await resp.text()