async-hyperliquid 0.1.0a1__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.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.3
2
+ Name: async-hyperliquid
3
+ Version: 0.1.0a1
4
+ Summary: Async Hyperliquid client using aiohttp
5
+ Author: oneforalone
6
+ Author-email: oneforalone@proton.me
7
+ Requires-Python: >=3.10,<4
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: aiohttp (>=3.11.13,<4.0.0)
14
+ Requires-Dist: eth-account (>=0.13.5,<0.14.0)
15
+ Requires-Dist: eth-utils (>=5.2.0,<6.0.0)
16
+ Requires-Dist: logging (>=0.4.9.6,<0.5.0.0)
17
+ Requires-Dist: msgpack (>=1.1.0,<2.0.0)
18
+ Project-URL: Documentation, https://github.com/oneforalone/async-hyperliquid
19
+ Project-URL: Homepage, https://github.com/oneforalone/async-hyperliquid
20
+ Project-URL: Issues, https://github.com/oneforalone/async-hyperliquid/issues
21
+ Project-URL: Repository, https://github.com/oneforalone/async-hyperliquid
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Async Hyperliquid
25
+
26
+ use `aiohttp` to interactive with Hyperliquid
27
+
@@ -0,0 +1,3 @@
1
+ # Async Hyperliquid
2
+
3
+ use `aiohttp` to interactive with Hyperliquid
File without changes
@@ -0,0 +1,46 @@
1
+ import logging
2
+ from types import TracebackType
3
+ from typing import Optional
4
+ from traceback import TracebackException
5
+
6
+ from aiohttp import ClientSession
7
+
8
+ from async_hyper.utils.constants import MAINNET_API_URL
9
+
10
+
11
+ class AsyncAPI:
12
+ def __init__(
13
+ self, endpoint: str, base_url: str = None, session: ClientSession = None
14
+ ):
15
+ self.endpoint = endpoint
16
+ self.base_url = base_url or MAINNET_API_URL
17
+ self.session = session
18
+ self.logger = logging.getLogger(__name__)
19
+
20
+ # for async with AsyncAPI() as api usage
21
+ async def __aenter__(self) -> "AsyncAPI":
22
+ return self
23
+
24
+ async def __aexit__(
25
+ self,
26
+ exc_type: Exception,
27
+ exc_val: TracebackException,
28
+ traceback: TracebackType,
29
+ ) -> None:
30
+ await self.close()
31
+
32
+ async def close(self) -> None:
33
+ if self.session and not self.session.closed:
34
+ await self.session.close()
35
+
36
+ async def post(self, payloads: Optional[dict] = None) -> dict:
37
+ payloads = payloads or {}
38
+ req_path = f"{self.base_url}/{self.endpoint.value}"
39
+ self.logger.info(f"POST {req_path} {payloads}")
40
+ async with self.session.post(req_path, json=payloads) as resp:
41
+ resp.raise_for_status()
42
+ try:
43
+ return await resp.json()
44
+ except Exception as e:
45
+ self.logger.error(f"Error parsing JSON response: {e}")
46
+ return await resp.text()
@@ -0,0 +1,239 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ from aiohttp import ClientSession, ClientTimeout
4
+ from eth_account import Account
5
+
6
+ from async_hyper.async_api import AsyncAPI
7
+ from async_hyper.utils.miscs import get_timestamp_ms
8
+ from async_hyper.utils.types import (
9
+ Cloid,
10
+ LimitOrder,
11
+ EncodedOrder,
12
+ OrderBuilder,
13
+ OrderRequest,
14
+ )
15
+ from async_hyper.info_endpoint import InfoAPI
16
+ from async_hyper.utils.signing import (
17
+ sign_action,
18
+ encode_order,
19
+ orders_to_action,
20
+ )
21
+ from async_hyper.utils.constants import MAINNET_API_URL, TESTNET_API_URL
22
+ from async_hyper.exchange_endpoint import ExchangeAPI
23
+
24
+
25
+ class AsyncHyper(AsyncAPI):
26
+ def __init__(self, address: str, api_key: str, is_mainnet: bool = True):
27
+ self.address = address
28
+ self.is_mainnet = is_mainnet
29
+ self.account = Account.from_key(api_key)
30
+ self.session = ClientSession(timeout=ClientTimeout(connect=3))
31
+ self.base_url = MAINNET_API_URL if is_mainnet else TESTNET_API_URL
32
+ self._info = InfoAPI(self.base_url, self.session)
33
+ self._exchange = ExchangeAPI(
34
+ self.account, self.session, self.base_url, address=self.address
35
+ )
36
+ self.metas: Optional[Dict[str, Any]] = None
37
+
38
+ def _init_coin_assets(self):
39
+ self.coin_assets = {}
40
+ for asset, asset_info in enumerate(self.metas["perps"]["universe"]):
41
+ self.coin_assets[asset_info["name"]] = asset
42
+
43
+ for asset_info in self.metas["spots"]["universe"]:
44
+ asset = asset_info["index"] + 10_000
45
+ self.coin_assets[asset_info["name"]] = asset
46
+
47
+ def _init_coin_names(self):
48
+ self.coin_names = {}
49
+ for asset_info in self.metas["perps"]["universe"]:
50
+ self.coin_names[asset_info["name"]] = asset_info["name"]
51
+
52
+ for asset_info in self.metas["spots"]["universe"]:
53
+ self.coin_names[asset_info["name"]] = asset_info["name"]
54
+ # For token pairs
55
+ base, quote = asset_info["tokens"]
56
+ base_info = self.metas["spots"]["tokens"][base]
57
+ quote_info = self.metas["spots"]["tokens"][quote]
58
+ base_name = (
59
+ base_info["name"] if base_info["name"] != "UBTC" else "BTC"
60
+ )
61
+ quote_name = quote_info["name"]
62
+ name = f"{base_name}/{quote_name}"
63
+ if name not in self.coin_names:
64
+ self.coin_names[name] = asset_info["name"]
65
+
66
+ def _init_asset_sz_decimals(self):
67
+ self.asset_sz_decimals = {}
68
+ for asset, asset_info in enumerate(self.metas["perps"]["universe"]):
69
+ self.asset_sz_decimals[asset] = asset_info["szDecimals"]
70
+
71
+ for asset_info in self.metas["spots"]["universe"]:
72
+ asset = asset_info["index"] + 10_000
73
+ base, _quote = asset_info["tokens"]
74
+ base_info = self.metas["spots"]["tokens"][base]
75
+ self.asset_sz_decimals[asset] = base_info["szDecimals"]
76
+
77
+ async def get_metas(self, perp_only: bool = False) -> dict:
78
+ perp_meta = await self._info.get_perp_meta()
79
+ if perp_only:
80
+ return {"perps": perp_meta, "spots": []}
81
+
82
+ spot_meta = await self._info.get_spot_meta()
83
+
84
+ return {"perps": perp_meta, "spots": spot_meta}
85
+
86
+ async def init_metas(self):
87
+ if not hasattr(self, "metas") or not self.metas:
88
+ self.metas = await self.get_metas()
89
+
90
+ if not self.metas.get("perps") or not self.metas["perps"]:
91
+ self.metas["perps"] = await self._info.get_perp_meta()
92
+
93
+ if not self.metas.get("spots") or not self.metas["spots"]:
94
+ self.metas["spots"] = await self._info.get_spot_meta()
95
+
96
+ if not hasattr(self, "coin_assets") or not self.coin_assets:
97
+ self._init_coin_assets()
98
+
99
+ if not hasattr(self, "coin_names") or not self.coin_names:
100
+ self._init_coin_names()
101
+
102
+ if not hasattr(self, "asset_sz_decimals") or not self.asset_sz_decimals:
103
+ self._init_asset_sz_decimals()
104
+
105
+ async def get_coin_name(self, coin: str) -> str:
106
+ await self.init_metas()
107
+
108
+ if coin not in self.coin_names:
109
+ raise ValueError(f"Coin {coin} not found")
110
+
111
+ return self.coin_names[coin]
112
+
113
+ async def get_coin_asset(self, coin: str) -> int:
114
+ await self.init_metas()
115
+
116
+ if coin not in self.coin_assets:
117
+ raise ValueError(f"Coin {coin} not found")
118
+
119
+ return self.coin_assets[coin]
120
+
121
+ async def get_coin_sz_decimals(self, coin: str) -> int:
122
+ await self.init_metas()
123
+
124
+ asset = await self.get_coin_asset(coin)
125
+
126
+ return self.asset_sz_decimals[asset]
127
+
128
+ async def get_token_id(self, coin: str) -> str:
129
+ coin_name = await self.get_coin_name(coin)
130
+ spot_metas: Dict[str, Any] = self.metas["spots"]
131
+ for coin_info in spot_metas["universe"]:
132
+ if coin_name == coin_info["name"]:
133
+ base, _quote = coin_info["tokens"]
134
+ return spot_metas["tokens"][base]["tokenId"]
135
+
136
+ return None
137
+
138
+ async def update_leverage(
139
+ self, leverage: int, coin: str, is_cross: bool = True
140
+ ):
141
+ nonce = get_timestamp_ms()
142
+ action = {
143
+ "type": "updateLeverage",
144
+ "asset": await self.get_coin_asset(coin),
145
+ "isCross": is_cross,
146
+ "leverage": leverage,
147
+ }
148
+ sig = sign_action(self.account, action, None, nonce, True)
149
+
150
+ return await self._exchange.post_action(action, sig, nonce)
151
+
152
+ async def place_orders(
153
+ self, orders: List[OrderRequest], builder: Optional[OrderBuilder] = None
154
+ ):
155
+ print(orders)
156
+ encoded_orders: List[EncodedOrder] = []
157
+ for order in orders:
158
+ asset = await self.get_coin_asset(order["coin"])
159
+ print(asset)
160
+ encoded_orders.append(encode_order(order, asset))
161
+
162
+ nonce = get_timestamp_ms()
163
+ if builder:
164
+ builder["b"] = builder["b"].lower()
165
+ action = orders_to_action(encoded_orders, builder)
166
+
167
+ # TODO: the third arg is vault_address, which is None for now
168
+ sig = sign_action(self.account, action, None, nonce, self.is_mainnet)
169
+
170
+ return await self._exchange.post_action(action, sig, nonce)
171
+
172
+ async def _slippage_price(
173
+ self, coin: str, is_buy: bool, slippage: float, px: float
174
+ ) -> float:
175
+ coin_name = await self.get_coin_name(coin)
176
+ if not px:
177
+ all_mids = await self._info.get_all_mids()
178
+ px = float(all_mids[coin_name])
179
+
180
+ asset = await self.get_coin_asset(coin)
181
+ is_spot = asset >= 10_000
182
+ sz_decimals = await self.get_coin_sz_decimals(coin)
183
+ px *= (1 + slippage) if is_buy else (1 - slippage)
184
+ return round(
185
+ float(f"{px:.5g}"), (6 if not is_spot else 8) - sz_decimals
186
+ )
187
+
188
+ async def place_order(
189
+ self,
190
+ coin: str,
191
+ is_buy: bool,
192
+ sz: float,
193
+ px: float,
194
+ is_market: bool = True,
195
+ *,
196
+ order_type: dict = LimitOrder.IOC.value,
197
+ reduce_only: bool = False,
198
+ cloid: Optional[Cloid] = None,
199
+ slippage: float = 0.01, # Default slippage is 1%
200
+ builder: Optional[OrderBuilder] = None,
201
+ ):
202
+ if is_market:
203
+ px = self._slippage_price(coin, is_buy, slippage, px)
204
+ # Market order is an aggressive Limit Order IoC
205
+ order_type = LimitOrder.IOC
206
+ reduce_only = False
207
+
208
+ name = await self.get_coin_name(coin)
209
+
210
+ order_req = {
211
+ "coin": name,
212
+ "is_buy": is_buy,
213
+ "sz": sz,
214
+ "limit_px": px,
215
+ "order_type": order_type,
216
+ "reduce_only": reduce_only,
217
+ "cloid": cloid,
218
+ }
219
+
220
+ if cloid:
221
+ order_req["cloid"] = cloid
222
+
223
+ return await self.place_orders([order_req], builder=builder)
224
+
225
+ async def cancel_order(self):
226
+ # TODO: implement cancel order
227
+ pass
228
+
229
+ async def modify_order(self):
230
+ # TODO: implement modify order
231
+ pass
232
+
233
+ async def close_all_positions(self):
234
+ # TODO: implement close all positions
235
+ pass
236
+
237
+ async def close_position(self, coin: str):
238
+ # TODO: implement close position
239
+ pass
@@ -0,0 +1,36 @@
1
+ from typing import Optional
2
+
3
+ from aiohttp import ClientSession
4
+ from eth_account import Account
5
+
6
+ from async_hyper.async_api import AsyncAPI
7
+ from async_hyper.utils.types import Endpoint
8
+
9
+
10
+ class ExchangeAPI(AsyncAPI):
11
+ def __init__(
12
+ self,
13
+ account: Account,
14
+ session: ClientSession,
15
+ base_url: Optional[str] = None,
16
+ address: str = None,
17
+ ):
18
+ self.account = account
19
+ self.address = address or account.address
20
+ super().__init__(Endpoint.EXCHANGE, base_url, session)
21
+
22
+ async def post_action(
23
+ self, action: dict, signature: str, nonce: int
24
+ ) -> dict:
25
+ assert self.endpoint == Endpoint.EXCHANGE, (
26
+ "only exchange endpoint supports action"
27
+ )
28
+
29
+ # TODO: to support vault address
30
+ payloads = {
31
+ "action": action,
32
+ "nonce": nonce,
33
+ "signature": signature,
34
+ "vaultAddress": None, # vault_address if action["type"] != "usdClassTransfer" else None
35
+ }
36
+ return await self.post(payloads)
@@ -0,0 +1,207 @@
1
+ from typing import Any, Dict, List
2
+
3
+ from aiohttp import ClientSession
4
+
5
+ from async_hyper.async_api import AsyncAPI
6
+ from async_hyper.utils.types import (
7
+ Depth,
8
+ Order,
9
+ Endpoint,
10
+ RateLimit,
11
+ FilledOrder,
12
+ FundingRate,
13
+ OrderStatus,
14
+ UserFunding,
15
+ TokenDetails,
16
+ SpotDeployState,
17
+ PerpMetaResponse,
18
+ SpotMetaResponse,
19
+ ClearinghouseState,
20
+ PerpMetaCtxResponse,
21
+ SpotMetaCtxResponse,
22
+ SpotClearinghouseState,
23
+ )
24
+
25
+
26
+ class InfoAPI(AsyncAPI):
27
+ def __init__(self, base_url: str, session: ClientSession):
28
+ super().__init__(Endpoint.INFO, base_url, session)
29
+
30
+ async def get_all_mids(self) -> List[Dict[str, int]]:
31
+ payloads = {"type": "allMids"}
32
+ return await self.post(payloads)
33
+
34
+ async def get_user_open_orders(
35
+ self, address: str, is_frontend: bool = False
36
+ ) -> List[Order]:
37
+ payloads = {
38
+ "type": "frontendOpenOrders" if is_frontend else "openOrders",
39
+ "user": address,
40
+ }
41
+ return await self.post(payloads)
42
+
43
+ async def get_user_fills(
44
+ self,
45
+ address: str,
46
+ start_time: int = None,
47
+ end_time: int = None,
48
+ aggregated: bool = False,
49
+ ) -> List[FilledOrder]:
50
+ payloads = {
51
+ "type": "userFillsByTime" if start_time else "userFills",
52
+ "user": address,
53
+ "aggregateByTime": aggregated,
54
+ }
55
+ if start_time:
56
+ payloads["startTime"] = start_time
57
+ payloads["endTime"] = end_time
58
+ return await self.post(payloads)
59
+
60
+ async def get_user_rate_limit(self, address: str) -> RateLimit:
61
+ payloads = {"type": "userRateLimit", "user": address}
62
+ return await self.post(payloads)
63
+
64
+ async def get_order_status(
65
+ self, order_id: str, address: str
66
+ ) -> OrderStatus:
67
+ payloads = {"type": "orderStatus", "user": address, "oid": order_id}
68
+ return await self.post(payloads)
69
+
70
+ async def get_depth(
71
+ self, coin: str, level: int = None, mantissa: int = None
72
+ ) -> List[Depth]:
73
+ payloads = {"type": "l2Book", "coin": coin}
74
+ if level:
75
+ payloads["nSigFigs"] = level
76
+ if level and level == 5 and mantissa:
77
+ payloads["mantissa"] = mantissa
78
+ return await self.post(payloads)
79
+
80
+ async def get_candles(self, **data):
81
+ # TODO: to implement
82
+ # https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#candle-snapshot
83
+ pass
84
+
85
+ async def check_user_builder_fee(self, user: str, builder: str) -> None:
86
+ payloads = {"type": "maxBuilderFee", "user": user, "builder": builder}
87
+ return await self.post(payloads)
88
+
89
+ async def get_user_order_history(
90
+ self, address: str
91
+ ) -> List[Dict[str, Any]]:
92
+ payloads = {"type": "historicalOrders", "user": address}
93
+ return await self.post(payloads)
94
+
95
+ async def get_user_twap_fills(self, address: str) -> List[Dict[str, Any]]:
96
+ payloads = {"type": "userTwapSliceFills", "user": address}
97
+ return await self.post(payloads)
98
+
99
+ async def get_user_subaccounts(self, address: str) -> List[Dict[str, Any]]:
100
+ payloads = {"type": "subAccounts", "user": address}
101
+ return await self.post(payloads)
102
+
103
+ async def get_vault_info(
104
+ self, address: str, user: str = None
105
+ ) -> Dict[str, Any]:
106
+ payloads = {"type": "vaultDetails", "vaultAddress": address}
107
+ if user:
108
+ payloads["user"] = user
109
+ return await self.post(payloads)
110
+
111
+ async def get_user_vault_deposits(
112
+ self, address: str
113
+ ) -> List[Dict[str, Any]]:
114
+ payloads = {"type": "userVaultEquities", "user": address}
115
+ return await self.post(payloads)
116
+
117
+ async def get_user_role(self, address: str) -> Dict[str, str]:
118
+ payloads = {"type": "userRole", "user": address}
119
+ return await self.post(payloads)
120
+
121
+ async def get_user_staking(self, address: str) -> List[Dict[str, Any]]:
122
+ payloads = {"type": "delegations", "user": address}
123
+ return await self.post(payloads)
124
+
125
+ async def get_user_staking_summary(self, address: str) -> Dict[str, Any]:
126
+ payloads = {"type": "delegatorSummary", "user": address}
127
+ return await self.post(payloads)
128
+
129
+ async def get_user_staking_history(
130
+ self, address: str
131
+ ) -> List[Dict[str, Any]]:
132
+ payloads = {"type": "delegatorHistory", "user": address}
133
+ return await self.post(payloads)
134
+
135
+ async def get_user_staking_rewards(
136
+ self, address: str
137
+ ) -> List[Dict[str, Any]]:
138
+ payloads = {"type": "delegatorRewards", "user": address}
139
+ return await self.post(payloads)
140
+
141
+ async def get_perp_meta(self) -> PerpMetaResponse:
142
+ payloads = {"type": "meta"}
143
+ return await self.post(payloads)
144
+
145
+ async def get_perp_meta_ctx(self) -> PerpMetaCtxResponse:
146
+ payloads = {"type": "metaAndAssetCtxs"}
147
+ return await self.post(payloads)
148
+
149
+ async def get_positions(self, address: str) -> ClearinghouseState:
150
+ payloads = {"type": "clearinghouseState", "user": address}
151
+ return await self.post(payloads)
152
+
153
+ async def get_user_funding(
154
+ self,
155
+ address: str,
156
+ start_time: int,
157
+ end_time: int = None,
158
+ is_funding: bool = True,
159
+ ) -> List[UserFunding]:
160
+ payloads = {
161
+ "type": "userFunding"
162
+ if is_funding
163
+ else "userNonFundingLedgerUpdates",
164
+ "user": address,
165
+ "startTime": start_time,
166
+ "endTime": end_time,
167
+ }
168
+ return await self.post(payloads)
169
+
170
+ async def get_funding_rate(
171
+ self, coin: str, start_time: int, end_time: int = None
172
+ ) -> List[FundingRate]:
173
+ payloads = {
174
+ "type": "fundingHistory",
175
+ "coin": coin,
176
+ "startTime": start_time,
177
+ "endTime": end_time,
178
+ }
179
+ return await self.post(payloads)
180
+
181
+ async def get_predicted_funding(self) -> List[Any]:
182
+ payloads = {"type": "predictedFundings"}
183
+ return await self.post(payloads)
184
+
185
+ async def get_perps_at_open_interest_cap(self) -> List[str]:
186
+ payloads = {"type": "perpsAtOpenInterestCap"}
187
+ return await self.post(payloads)
188
+
189
+ async def get_spot_meta(self) -> SpotMetaResponse:
190
+ payloads = {"type": "spotMeta"}
191
+ return await self.post(payloads)
192
+
193
+ async def get_spot_meta_ctx(self) -> SpotMetaCtxResponse:
194
+ payloads = {"type": "spotMetaAndAssetCtxs"}
195
+ return await self.post(payloads)
196
+
197
+ async def get_user_balances(self, address: str) -> SpotClearinghouseState:
198
+ payloads = {"type": "spotClearinghouseState", "user": address}
199
+ return await self.post(payloads)
200
+
201
+ async def get_spot_deploy_state(self, address: str) -> SpotDeployState:
202
+ payloads = {"type": "spotDeployState", "user": address}
203
+ return await self.post(payloads)
204
+
205
+ async def get_token_info(self, token_id: str) -> TokenDetails:
206
+ payloads = {"type": "tokenDetails", "tokenId": token_id}
207
+ return await self.post(payloads)
@@ -0,0 +1,2 @@
1
+ MAINNET_API_URL = "https://api.hyperliquid.xyz"
2
+ TESTNET_API_URL = "https://api.hyperliquid-testnet.xyz"
@@ -0,0 +1,28 @@
1
+ import time
2
+ from typing import Any
3
+ from decimal import Decimal
4
+
5
+
6
+ def get_timestamp_ms() -> int:
7
+ return int(time.time() * 1000)
8
+
9
+
10
+ def is_numeric(n: Any) -> bool:
11
+ try:
12
+ Decimal(n)
13
+ return True
14
+ except ValueError:
15
+ return False
16
+
17
+
18
+ def convert_to_numeric(data: Any) -> Any:
19
+ if isinstance(data, dict):
20
+ for k, v in data.items():
21
+ if isinstance(v, str) and is_numeric(v):
22
+ data[k] = Decimal(v)
23
+ else:
24
+ data[k] = convert_to_numeric(v)
25
+ elif isinstance(data, list):
26
+ for i in data:
27
+ convert_to_numeric(i)
28
+ return data
@@ -0,0 +1,127 @@
1
+ from typing import List
2
+ from decimal import Decimal
3
+
4
+ import msgpack
5
+ from eth_utils import keccak, to_hex
6
+ from eth_account import Account
7
+ from eth_account.messages import encode_typed_data
8
+
9
+ from async_hyper.utils.types import (
10
+ OrderType,
11
+ OrderAction,
12
+ EncodedOrder,
13
+ OrderBuilder,
14
+ OrderRequest,
15
+ SignedAction,
16
+ )
17
+
18
+
19
+ def address_to_bytes(address: str) -> bytes:
20
+ return bytes.fromhex(address[2:] if address.startswith("0x") else address)
21
+
22
+
23
+ def hash_action(action, vault, nonce) -> bytes:
24
+ data = msgpack.packb(action)
25
+ data += nonce.to_bytes(8, "big")
26
+
27
+ if vault is None:
28
+ data += b"\x00"
29
+ else:
30
+ data += b"\x01"
31
+ data += bytes.fromhex(vault.removeprefix("0x"))
32
+ return keccak(data)
33
+
34
+
35
+ def sign_action(
36
+ wallet: Account,
37
+ action: dict,
38
+ active_pool: str,
39
+ nonce: int,
40
+ is_mainnet: bool,
41
+ ) -> SignedAction:
42
+ h = hash_action(action, active_pool, nonce)
43
+ msg = {"source": "a" if is_mainnet else "b", "connectionId": h}
44
+ data = {
45
+ "domain": {
46
+ "chainId": 1337,
47
+ "name": "Exchange",
48
+ "verifyingContract": "0x0000000000000000000000000000000000000000",
49
+ "version": "1",
50
+ },
51
+ "types": {
52
+ "Agent": [
53
+ {"name": "source", "type": "string"},
54
+ {"name": "connectionId", "type": "bytes32"},
55
+ ],
56
+ "EIP712Domain": [
57
+ {"name": "name", "type": "string"},
58
+ {"name": "version", "type": "string"},
59
+ {"name": "chainId", "type": "uint256"},
60
+ {"name": "verifyingContract", "type": "address"},
61
+ ],
62
+ },
63
+ "primaryType": "Agent",
64
+ "message": msg,
65
+ }
66
+ encodes = encode_typed_data(full_message=data)
67
+ signed = wallet.sign_message(encodes)
68
+ return {
69
+ "r": to_hex(signed["r"]),
70
+ "s": to_hex(signed["s"]),
71
+ "v": signed["v"],
72
+ }
73
+
74
+
75
+ def round_float(x: float) -> str:
76
+ rounded = f"{x:.8f}"
77
+ if abs(float(rounded) - x) >= 1e-12:
78
+ raise ValueError("round_float causes rounding", x)
79
+
80
+ if rounded == "-0":
81
+ rounded = "0"
82
+ normalized = Decimal(rounded).normalize()
83
+ return f"{normalized:f}"
84
+
85
+
86
+ def ensure_order_type(order_type: OrderType) -> OrderType:
87
+ if "limit" in order_type:
88
+ return {"limit": order_type["limit"]}
89
+ elif "trigger" in order_type:
90
+ return {
91
+ "trigger": {
92
+ "isMarket": order_type["trigger"]["isMarket"],
93
+ "triggerPx": round_float(order_type["trigger"]["triggerPx"]),
94
+ "tpsl": order_type["trigger"]["tpsl"],
95
+ }
96
+ }
97
+
98
+ raise ValueError("Invalid order type", order_type)
99
+
100
+
101
+ def encode_order(order: OrderRequest, asset: int) -> EncodedOrder:
102
+ encoded_order: EncodedOrder = {
103
+ "a": asset,
104
+ "b": order["is_buy"],
105
+ "p": round_float(order["limit_px"]),
106
+ "s": round_float(order["sz"]),
107
+ "r": order["reduce_only"],
108
+ "t": ensure_order_type(order["order_type"]),
109
+ }
110
+
111
+ if order["cloid"] is not None:
112
+ encoded_order["c"] = order["cloid"].to_raw()
113
+
114
+ return encoded_order
115
+
116
+
117
+ def orders_to_action(
118
+ encoded_orders: List[EncodedOrder], builder: OrderBuilder = None
119
+ ) -> OrderAction:
120
+ action: OrderAction = {
121
+ "type": "order",
122
+ "orders": encoded_orders,
123
+ "grouping": "na",
124
+ }
125
+ if builder:
126
+ action["builder"] = builder
127
+ return action
@@ -0,0 +1,330 @@
1
+ from enum import Enum
2
+ from typing import Any, List, Tuple, Union, Literal, Optional, TypedDict
3
+
4
+ from typing_extensions import NotRequired
5
+
6
+
7
+ class Cloid:
8
+ def __init__(self, raw_cloid: str):
9
+ self._raw_cloid: str = raw_cloid
10
+ self._validate()
11
+
12
+ def _validate(self) -> None:
13
+ if not self._raw_cloid[:2] == "0x":
14
+ raise TypeError("cloid is not a hex string")
15
+ if not len(self._raw_cloid[2:]) == 32:
16
+ raise TypeError("cloid is not 16 bytes")
17
+
18
+ def __str__(self) -> str:
19
+ return str(self._raw_cloid)
20
+
21
+ def __repr__(self) -> str:
22
+ return str(self._raw_cloid)
23
+
24
+ @staticmethod
25
+ def from_int(cloid: int) -> "Cloid":
26
+ return Cloid(f"{cloid:#034x}")
27
+
28
+ @staticmethod
29
+ def from_str(cloid: str) -> "Cloid":
30
+ return Cloid(cloid)
31
+
32
+ def to_raw(self) -> str:
33
+ return self._raw_cloid
34
+
35
+
36
+ class SignedAction(TypedDict):
37
+ r: str
38
+ s: str
39
+ v: int
40
+
41
+
42
+ class LimitOrderOptions(TypedDict):
43
+ tif: Literal["Alo", "Ioc", "Gtc"]
44
+
45
+
46
+ class LimitOrderType(TypedDict):
47
+ limit: LimitOrderOptions
48
+
49
+
50
+ class LimitOrder(Enum):
51
+ ALO = {"limit": {"tif": "Alo"}}
52
+ IOC = {"limit": {"tif": "Ioc"}}
53
+ GTC = {"limit": {"tif": "Gtc"}}
54
+
55
+
56
+ class TriggerOrderOptions(TypedDict):
57
+ isMarket: bool
58
+ triggerPx: str
59
+ tpsl: Literal["tp", "sl"]
60
+
61
+
62
+ class TriggerOrderType(TypedDict):
63
+ trigger: TriggerOrderOptions
64
+
65
+
66
+ OrderType = Union[LimitOrderType, TriggerOrderType]
67
+
68
+
69
+ class OrderRequest(TypedDict):
70
+ coin: str
71
+ is_buy: bool
72
+ sz: float
73
+ limit_px: float
74
+ reduce_only: bool
75
+ order_type: OrderType
76
+ cloid: NotRequired[Cloid]
77
+
78
+
79
+ class EncodedOrder(TypedDict):
80
+ a: int # asset universe index
81
+ b: bool # is_buy
82
+ p: str # limit_px
83
+ s: str # size
84
+ r: bool # reduce_only
85
+ t: OrderType # order type
86
+ c: NotRequired[Cloid] # cloid
87
+
88
+
89
+ class OrderBuilder(TypedDict):
90
+ b: str # builder address
91
+ f: float
92
+
93
+
94
+ class OrderAction(TypedDict):
95
+ type: Literal["order"]
96
+ orders: List[EncodedOrder]
97
+ grouping: Literal["na", "normalTpsl", "positionTpsl"]
98
+ builder: NotRequired[OrderBuilder]
99
+
100
+
101
+ class Endpoint(str, Enum):
102
+ INFO = "info"
103
+ EXCHANGE = "exchange"
104
+
105
+
106
+ class Order(TypedDict):
107
+ # TODO: add order type
108
+ pass
109
+
110
+
111
+ class FilledOrder(Order):
112
+ # TODO: add filled order type
113
+ pass
114
+
115
+
116
+ class RateLimit(TypedDict):
117
+ cumVlm: str
118
+ nRequestsUsed: int
119
+ nRequestsCap: int
120
+
121
+
122
+ class OrderStatus(TypedDict):
123
+ # TODO: add order status
124
+ pass
125
+
126
+
127
+ class L2Book(TypedDict):
128
+ px: str
129
+ sz: str
130
+ n: int
131
+
132
+
133
+ class Depth(TypedDict):
134
+ bids: List[L2Book]
135
+ asks: List[L2Book]
136
+
137
+
138
+ class PerpMeta(TypedDict):
139
+ name: str
140
+ szDecimals: int
141
+ maxLeverage: int
142
+ onlyIsolated: NotRequired[bool]
143
+ isDelisted: NotRequired[bool]
144
+
145
+
146
+ class PerpMetaResponse(TypedDict):
147
+ universe: List[PerpMeta]
148
+
149
+
150
+ class PerpMetaCtx(TypedDict):
151
+ dayNtlVlm: str
152
+ funding: str
153
+ impactPxs: List[str]
154
+ markPx: str
155
+ midPx: str
156
+ openInterest: str
157
+ oraclePx: str
158
+ premium: str
159
+ prevDayPx: str
160
+
161
+
162
+ PerpMetaCtxResponse = List[Union[PerpMetaResponse, List[PerpMetaCtx]]]
163
+
164
+
165
+ class AssetFunding(TypedDict):
166
+ allTime: str
167
+ sinceChange: str
168
+ sinceOpen: str
169
+
170
+
171
+ class AssetLeverage(TypedDict):
172
+ rawUsd: str
173
+ type: str
174
+ value: int
175
+
176
+
177
+ class Position(TypedDict):
178
+ coin: str
179
+ cumFunding: AssetFunding
180
+ entryPx: str
181
+ leverage: AssetLeverage
182
+ liquidationPx: str
183
+ marginUsed: str
184
+ maxLeverage: int
185
+ positionValue: str
186
+ returnOnEquity: str
187
+ szi: str
188
+ unrealizedPnl: str
189
+
190
+
191
+ class AssetPosition(TypedDict):
192
+ type: str
193
+ position: Position
194
+
195
+
196
+ class MarginSummary(TypedDict):
197
+ accountValue: str
198
+ totalMarginUsed: str
199
+ totalNtlPos: str
200
+ totalRawUsd: str
201
+
202
+
203
+ class ClearinghouseState(TypedDict):
204
+ assetPositions: List[AssetPosition]
205
+ crosssMaintenanceMarginUsed: str
206
+ crossMarginSummary: MarginSummary
207
+ marginSummary: MarginSummary
208
+ time: int
209
+ withdrawable: str
210
+
211
+
212
+ class UserFundingDelta(TypedDict):
213
+ coin: str
214
+ fundingRate: str
215
+ szi: str
216
+ type: str
217
+ usdc: str
218
+
219
+
220
+ class UserFunding(TypedDict):
221
+ delta: UserFundingDelta
222
+ hash: str
223
+ time: int
224
+
225
+
226
+ class FundingRate(TypedDict):
227
+ coin: str
228
+ fundingRate: str
229
+ premium: str
230
+ time: int
231
+
232
+
233
+ class Token(TypedDict):
234
+ name: str
235
+ index: int
236
+ isCanonical: bool
237
+
238
+
239
+ class TokenPairs(Token):
240
+ tokens: Tuple[int, int]
241
+
242
+
243
+ class SpotMeta(Token):
244
+ szDecimals: int
245
+ weiDecimals: int
246
+ tokenId: str
247
+ evmContract: Optional[str]
248
+ fullName: Optional[str]
249
+
250
+
251
+ class SpotMetaResponse(TypedDict):
252
+ tokens: List[SpotMeta]
253
+ universe: List[TokenPairs]
254
+
255
+
256
+ class SpotMetaCtx(TypedDict):
257
+ dayNtlVlm: str
258
+ markPx: str
259
+ midPx: str
260
+ prevDayPx: str
261
+
262
+
263
+ SpotMetaCtxResponse = List[Union[SpotMetaResponse, List[SpotMetaCtx]]]
264
+
265
+
266
+ class TokenBalance(TypedDict):
267
+ coin: str
268
+ token: int
269
+ hold: str
270
+ total: str
271
+ entryNtl: str
272
+
273
+
274
+ class SpotClearinghouseState(TypedDict):
275
+ balances: List[TokenBalance]
276
+
277
+
278
+ class GasAuction(TypedDict):
279
+ startTimeSeconds: int
280
+ durationSeconds: int
281
+ startGas: str
282
+ currentGas: Optional[str]
283
+ endGas: str
284
+
285
+
286
+ class DeployStateSpec(TypedDict):
287
+ name: str
288
+ szDecimals: int
289
+ weiDecimals: int
290
+
291
+
292
+ class DeployState(TypedDict):
293
+ token: int
294
+ spec: DeployStateSpec
295
+ fullName: str
296
+ spots: List[int]
297
+ maxSupply: int
298
+ hyperliquidityGenesisBalance: str
299
+ totalGenesisBalanceWei: str
300
+ userGenesisBalances: List[Tuple[str, str]]
301
+ existingTokenGenesisBalances: List[Tuple[int, str]]
302
+
303
+
304
+ class SpotDeployState(TypedDict):
305
+ states: List[DeployState]
306
+ gasAuction: GasAuction
307
+
308
+
309
+ class TokenGenesis(TypedDict):
310
+ userBalances: List[Tuple[str, str]]
311
+ existingTokenBalances: List[Any]
312
+
313
+
314
+ class TokenDetails(TypedDict):
315
+ name: str
316
+ maxSupply: str
317
+ totalSupply: str
318
+ circulatingSupply: str
319
+ szDecimals: int
320
+ weiDecimals: int
321
+ midPx: str
322
+ markPx: str
323
+ prevDayPx: str
324
+ genesis: List[TokenGenesis]
325
+ deployer: str
326
+ deployGas: str
327
+ deployTime: str
328
+ seededUsdc: str
329
+ nonCirculatingUserBalances: List[Any]
330
+ futureEmissions: str
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "async-hyperliquid"
3
+ version = "0.1.0a1"
4
+ description = "Async Hyperliquid client using aiohttp"
5
+ authors = [{ name = "oneforalone", email = "oneforalone@proton.me" }]
6
+ readme = "README.md"
7
+ requires-python = ">=3.10,<4"
8
+ dependencies = [
9
+ "aiohttp (>=3.11.13,<4.0.0)",
10
+ "logging (>=0.4.9.6,<0.5.0.0)",
11
+ "msgpack (>=1.1.0,<2.0.0)",
12
+ "eth-account (>=0.13.5,<0.14.0)",
13
+ "eth-utils (>=5.2.0,<6.0.0)",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://github.com/oneforalone/async-hyperliquid"
18
+ Documentation = "https://github.com/oneforalone/async-hyperliquid"
19
+ Repository = "https://github.com/oneforalone/async-hyperliquid"
20
+ Issues = "https://github.com/oneforalone/async-hyperliquid/issues"
21
+ # Changelog = "https://github.com/oneforalone/async-hyperliquid/blob/master/CHANGELOG.md"
22
+
23
+ [tool.poetry]
24
+ packages = [{ include = "async_hyper" }]
25
+
26
+
27
+ [tool.poetry.group.dev.dependencies]
28
+ pytest = "^8.3.5"
29
+ pytest-asyncio = "^0.25.3"
30
+ pytest-ruff = "^0.4.1"
31
+ pytest-cov = "^6.0.0"
32
+ pre-commit = "^4.1.0"
33
+ python-dotenv = "^1.0.1"
34
+
35
+ [build-system]
36
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
37
+ build-backend = "poetry.core.masonry.api"
38
+
39
+ [tool.pytest.ini_options]
40
+ asyncio_mode = "auto"
41
+ addopts = "--verbose --capture=no"
42
+ asyncio_default_fixture_loop_scope = "session"