async-hyperliquid 0.1.0__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,140 @@
1
+ Metadata-Version: 2.3
2
+ Name: async-hyperliquid
3
+ Version: 0.1.0
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.12,<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: msgpack (>=1.1.0,<2.0.0)
17
+ Project-URL: Documentation, https://github.com/oneforalone/async-hyperliquid
18
+ Project-URL: Homepage, https://github.com/oneforalone/async-hyperliquid
19
+ Project-URL: Issues, https://github.com/oneforalone/async-hyperliquid/issues
20
+ Project-URL: Repository, https://github.com/oneforalone/async-hyperliquid
21
+ Description-Content-Type: text/markdown
22
+
23
+ # Async Hyperliquid
24
+
25
+ An asynchronous Python client for interacting with the Hyperliquid API using `aiohttp`.
26
+
27
+ ## Overview
28
+
29
+ This library provides an easy-to-use asynchronous interface for the Hyperliquid cryptocurrency exchange, supporting both mainnet and testnet environments. It handles API interactions, request signing, and data processing for both perpetual futures and spot trading.
30
+
31
+ ## Features
32
+
33
+ - Asynchronous API communication using `aiohttp`
34
+ - Support for both mainnet and testnet environments
35
+ - Message signing for authenticated endpoints
36
+ - Trading operations for both perpetual futures and spot markets
37
+ - Comprehensive type hints for better IDE integration
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ # Using pip
43
+ pip install async-hyperliquid
44
+
45
+ # Using Poetry
46
+ poetry add async-hyperliquid
47
+ ```
48
+
49
+ ## Quick Start
50
+
51
+ ```python
52
+ import asyncio
53
+ import os
54
+ from async_hyperliquid import AsyncHyper
55
+
56
+ async def main():
57
+ # Initialize the client
58
+ address = os.getenv("HYPER_ADDRESS")
59
+ api_key = os.getenv("HYPER_API_KEY")
60
+ client = AsyncHyper(address, api_key, is_mainnet=True)
61
+
62
+ # Initialize metadata (required before making other calls)
63
+ await client.init_metas()
64
+
65
+ # Place a market order
66
+ response = await client.place_order(
67
+ coin="BTC",
68
+ is_buy=True,
69
+ sz=0.001,
70
+ px=0, # For market orders, price is ignored
71
+ is_market=True
72
+ )
73
+
74
+ print(response)
75
+
76
+ # Clean up
77
+ await client.close()
78
+
79
+ if __name__ == "__main__":
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ## Environment Variables
84
+
85
+ Create a `.env.local` file with the following variables:
86
+
87
+ ```
88
+ HYPER_ADDRESS=your_ethereum_address
89
+ HYPER_API_KEY=your_ethereum_private_key
90
+ ```
91
+
92
+ ## API Reference
93
+
94
+ ### AsyncHyper
95
+
96
+ The main client class that provides methods for interacting with the Hyperliquid API.
97
+
98
+ #### Initialization
99
+
100
+ ```python
101
+ client = AsyncHyper(address, api_key, is_mainnet=True)
102
+ ```
103
+
104
+ #### Methods
105
+
106
+ - `init_metas()`: Initialize metadata (required before using other methods)
107
+ - `update_leverage(leverage, coin, is_cross=True)`: Update leverage for a specific coin
108
+ - `place_order(coin, is_buy, sz, px, is_market=True, **kwargs)`: Place a single order
109
+ - `place_orders(orders, builder=None)`: Place multiple orders at once
110
+
111
+ ### Additional Modules
112
+
113
+ - `InfoAPI`: Access market information endpoints
114
+ - `ExchangeAPI`: Access exchange operation endpoints
115
+ - `utils`: Various utility functions and constants
116
+
117
+ ## Testing
118
+
119
+ Tests use pytest and pytest-asyncio. To run tests:
120
+
121
+ ```bash
122
+ # Run all tests
123
+ pytest
124
+
125
+ # Run with coverage
126
+ pytest --cov=async_hyperliquid
127
+ ```
128
+
129
+ ## License
130
+
131
+ MIT
132
+
133
+ ## Acknowledgements
134
+
135
+ This library is a community-developed project and is not officially affiliated with Hyperliquid.
136
+
137
+ ## TODO
138
+
139
+ See the TODO.md file for upcoming features and improvements.
140
+
@@ -0,0 +1,117 @@
1
+ # Async Hyperliquid
2
+
3
+ An asynchronous Python client for interacting with the Hyperliquid API using `aiohttp`.
4
+
5
+ ## Overview
6
+
7
+ This library provides an easy-to-use asynchronous interface for the Hyperliquid cryptocurrency exchange, supporting both mainnet and testnet environments. It handles API interactions, request signing, and data processing for both perpetual futures and spot trading.
8
+
9
+ ## Features
10
+
11
+ - Asynchronous API communication using `aiohttp`
12
+ - Support for both mainnet and testnet environments
13
+ - Message signing for authenticated endpoints
14
+ - Trading operations for both perpetual futures and spot markets
15
+ - Comprehensive type hints for better IDE integration
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ # Using pip
21
+ pip install async-hyperliquid
22
+
23
+ # Using Poetry
24
+ poetry add async-hyperliquid
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```python
30
+ import asyncio
31
+ import os
32
+ from async_hyperliquid import AsyncHyper
33
+
34
+ async def main():
35
+ # Initialize the client
36
+ address = os.getenv("HYPER_ADDRESS")
37
+ api_key = os.getenv("HYPER_API_KEY")
38
+ client = AsyncHyper(address, api_key, is_mainnet=True)
39
+
40
+ # Initialize metadata (required before making other calls)
41
+ await client.init_metas()
42
+
43
+ # Place a market order
44
+ response = await client.place_order(
45
+ coin="BTC",
46
+ is_buy=True,
47
+ sz=0.001,
48
+ px=0, # For market orders, price is ignored
49
+ is_market=True
50
+ )
51
+
52
+ print(response)
53
+
54
+ # Clean up
55
+ await client.close()
56
+
57
+ if __name__ == "__main__":
58
+ asyncio.run(main())
59
+ ```
60
+
61
+ ## Environment Variables
62
+
63
+ Create a `.env.local` file with the following variables:
64
+
65
+ ```
66
+ HYPER_ADDRESS=your_ethereum_address
67
+ HYPER_API_KEY=your_ethereum_private_key
68
+ ```
69
+
70
+ ## API Reference
71
+
72
+ ### AsyncHyper
73
+
74
+ The main client class that provides methods for interacting with the Hyperliquid API.
75
+
76
+ #### Initialization
77
+
78
+ ```python
79
+ client = AsyncHyper(address, api_key, is_mainnet=True)
80
+ ```
81
+
82
+ #### Methods
83
+
84
+ - `init_metas()`: Initialize metadata (required before using other methods)
85
+ - `update_leverage(leverage, coin, is_cross=True)`: Update leverage for a specific coin
86
+ - `place_order(coin, is_buy, sz, px, is_market=True, **kwargs)`: Place a single order
87
+ - `place_orders(orders, builder=None)`: Place multiple orders at once
88
+
89
+ ### Additional Modules
90
+
91
+ - `InfoAPI`: Access market information endpoints
92
+ - `ExchangeAPI`: Access exchange operation endpoints
93
+ - `utils`: Various utility functions and constants
94
+
95
+ ## Testing
96
+
97
+ Tests use pytest and pytest-asyncio. To run tests:
98
+
99
+ ```bash
100
+ # Run all tests
101
+ pytest
102
+
103
+ # Run with coverage
104
+ pytest --cov=async_hyperliquid
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
110
+
111
+ ## Acknowledgements
112
+
113
+ This library is a community-developed project and is not officially affiliated with Hyperliquid.
114
+
115
+ ## TODO
116
+
117
+ See the TODO.md file for upcoming features and improvements.
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_hyperliquid.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,253 @@
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_hyperliquid.async_api import AsyncAPI
7
+ from async_hyperliquid.utils.types import (
8
+ Cloid,
9
+ LimitOrder,
10
+ EncodedOrder,
11
+ OrderBuilder,
12
+ PlaceOrderRequest,
13
+ CancelOrderRequest,
14
+ )
15
+ from async_hyperliquid.info_endpoint import InfoAPI
16
+ from async_hyperliquid.utils.signing import encode_order, orders_to_action
17
+ from async_hyperliquid.utils.constants import MAINNET_API_URL, TESTNET_API_URL
18
+ from async_hyperliquid.exchange_endpoint import ExchangeAPI
19
+
20
+
21
+ class AsyncHyper(AsyncAPI):
22
+ def __init__(self, address: str, api_key: str, is_mainnet: bool = True):
23
+ self.address = address
24
+ self.is_mainnet = is_mainnet
25
+ self.account = Account.from_key(api_key)
26
+ self.session = ClientSession(timeout=ClientTimeout(connect=3))
27
+ self.base_url = MAINNET_API_URL if is_mainnet else TESTNET_API_URL
28
+ self._info = InfoAPI(self.base_url, self.session)
29
+ self._exchange = ExchangeAPI(
30
+ self.account, self.session, self.base_url, address=self.address
31
+ )
32
+ self.metas: Optional[Dict[str, Any]] = None
33
+ # TODO: figure out the vault address
34
+ self.vault: Optional[str] = None
35
+
36
+ def _init_coin_assets(self):
37
+ self.coin_assets = {}
38
+ for asset, asset_info in enumerate(self.metas["perps"]["universe"]):
39
+ self.coin_assets[asset_info["name"]] = asset
40
+
41
+ for asset_info in self.metas["spots"]["universe"]:
42
+ asset = asset_info["index"] + 10_000
43
+ self.coin_assets[asset_info["name"]] = asset
44
+
45
+ def _init_coin_names(self):
46
+ self.coin_names = {}
47
+ for asset_info in self.metas["perps"]["universe"]:
48
+ self.coin_names[asset_info["name"]] = asset_info["name"]
49
+
50
+ for asset_info in self.metas["spots"]["universe"]:
51
+ self.coin_names[asset_info["name"]] = asset_info["name"]
52
+ # For token pairs
53
+ base, quote = asset_info["tokens"]
54
+ base_info = self.metas["spots"]["tokens"][base]
55
+ quote_info = self.metas["spots"]["tokens"][quote]
56
+ base_name = (
57
+ base_info["name"] if base_info["name"] != "UBTC" else "BTC"
58
+ )
59
+ quote_name = quote_info["name"]
60
+ name = f"{base_name}/{quote_name}"
61
+ if name not in self.coin_names:
62
+ self.coin_names[name] = asset_info["name"]
63
+
64
+ def _init_asset_sz_decimals(self):
65
+ self.asset_sz_decimals = {}
66
+ for asset, asset_info in enumerate(self.metas["perps"]["universe"]):
67
+ self.asset_sz_decimals[asset] = asset_info["szDecimals"]
68
+
69
+ for asset_info in self.metas["spots"]["universe"]:
70
+ asset = asset_info["index"] + 10_000
71
+ base, _quote = asset_info["tokens"]
72
+ base_info = self.metas["spots"]["tokens"][base]
73
+ self.asset_sz_decimals[asset] = base_info["szDecimals"]
74
+
75
+ async def get_metas(self, perp_only: bool = False) -> dict:
76
+ perp_meta = await self._info.get_perp_meta()
77
+ if perp_only:
78
+ return {"perps": perp_meta, "spots": []}
79
+
80
+ spot_meta = await self._info.get_spot_meta()
81
+
82
+ return {"perps": perp_meta, "spots": spot_meta}
83
+
84
+ async def init_metas(self):
85
+ if not hasattr(self, "metas") or not self.metas:
86
+ self.metas = await self.get_metas()
87
+
88
+ if not self.metas.get("perps") or not self.metas["perps"]:
89
+ self.metas["perps"] = await self._info.get_perp_meta()
90
+
91
+ if not self.metas.get("spots") or not self.metas["spots"]:
92
+ self.metas["spots"] = await self._info.get_spot_meta()
93
+
94
+ if not hasattr(self, "coin_assets") or not self.coin_assets:
95
+ self._init_coin_assets()
96
+
97
+ if not hasattr(self, "coin_names") or not self.coin_names:
98
+ self._init_coin_names()
99
+
100
+ if not hasattr(self, "asset_sz_decimals") or not self.asset_sz_decimals:
101
+ self._init_asset_sz_decimals()
102
+
103
+ async def get_coin_name(self, coin: str) -> str:
104
+ await self.init_metas()
105
+
106
+ if coin not in self.coin_names:
107
+ raise ValueError(f"Coin {coin} not found")
108
+
109
+ return self.coin_names[coin]
110
+
111
+ async def get_coin_asset(self, coin: str) -> int:
112
+ await self.init_metas()
113
+
114
+ if coin not in self.coin_assets:
115
+ raise ValueError(f"Coin {coin} not found")
116
+
117
+ return self.coin_assets[coin]
118
+
119
+ async def get_coin_sz_decimals(self, coin: str) -> int:
120
+ await self.init_metas()
121
+
122
+ asset = await self.get_coin_asset(coin)
123
+
124
+ return self.asset_sz_decimals[asset]
125
+
126
+ async def get_token_id(self, coin: str) -> str:
127
+ coin_name = await self.get_coin_name(coin)
128
+ spot_metas: Dict[str, Any] = self.metas["spots"]
129
+ for coin_info in spot_metas["universe"]:
130
+ if coin_name == coin_info["name"]:
131
+ base, _quote = coin_info["tokens"]
132
+ return spot_metas["tokens"][base]["tokenId"]
133
+
134
+ return None
135
+
136
+ async def update_leverage(
137
+ self, leverage: int, coin: str, is_cross: bool = True
138
+ ):
139
+ action = {
140
+ "type": "updateLeverage",
141
+ "asset": await self.get_coin_asset(coin),
142
+ "isCross": is_cross,
143
+ "leverage": leverage,
144
+ }
145
+
146
+ return await self._exchange.post_action(action)
147
+
148
+ async def place_orders(
149
+ self,
150
+ orders: List[PlaceOrderRequest],
151
+ builder: Optional[OrderBuilder] = None,
152
+ ):
153
+ encoded_orders: List[EncodedOrder] = []
154
+ for order in orders:
155
+ asset = await self.get_coin_asset(order["coin"])
156
+ encoded_orders.append(encode_order(order, asset))
157
+
158
+ if builder:
159
+ builder["b"] = builder["b"].lower()
160
+ action = orders_to_action(encoded_orders, builder)
161
+
162
+ return await self._exchange.post_action(action)
163
+
164
+ async def _slippage_price(
165
+ self, coin: str, is_buy: bool, slippage: float, px: float
166
+ ) -> float:
167
+ coin_name = await self.get_coin_name(coin)
168
+ if not px:
169
+ all_mids = await self._info.get_all_mids()
170
+ px = float(all_mids[coin_name])
171
+
172
+ asset = await self.get_coin_asset(coin)
173
+ is_spot = asset >= 10_000
174
+ sz_decimals = await self.get_coin_sz_decimals(coin)
175
+ px *= (1 + slippage) if is_buy else (1 - slippage)
176
+ return round(
177
+ float(f"{px:.5g}"), (6 if not is_spot else 8) - sz_decimals
178
+ )
179
+
180
+ async def place_order(
181
+ self,
182
+ coin: str,
183
+ is_buy: bool,
184
+ sz: float,
185
+ px: float,
186
+ is_market: bool = True,
187
+ *,
188
+ order_type: dict = LimitOrder.IOC.value,
189
+ reduce_only: bool = False,
190
+ cloid: Optional[Cloid] = None,
191
+ slippage: float = 0.01, # Default slippage is 1%
192
+ builder: Optional[OrderBuilder] = None,
193
+ ):
194
+ if is_market:
195
+ px = self._slippage_price(coin, is_buy, slippage, px)
196
+ # Market order is an aggressive Limit Order IoC
197
+ order_type = LimitOrder.IOC
198
+ reduce_only = False
199
+
200
+ name = await self.get_coin_name(coin)
201
+
202
+ order_req = {
203
+ "coin": name,
204
+ "is_buy": is_buy,
205
+ "sz": sz,
206
+ "limit_px": px,
207
+ "order_type": order_type,
208
+ "reduce_only": reduce_only,
209
+ "cloid": cloid,
210
+ }
211
+
212
+ if cloid:
213
+ order_req["cloid"] = cloid
214
+
215
+ return await self.place_orders([order_req], builder=builder)
216
+
217
+ async def cancel_order(self, coin: str, oid: int | str):
218
+ name = await self.get_coin_name(coin)
219
+ if not isinstance(oid, int):
220
+ oid = int(oid)
221
+ cancel_req = {"coin": name, "oid": oid}
222
+ return await self.cancel_orders([cancel_req])
223
+
224
+ async def cancel_orders(self, orders: List[CancelOrderRequest]):
225
+ # TODO: support cloid
226
+ action = {
227
+ "type": "cancel",
228
+ "cancels": [
229
+ {
230
+ "a": await self.get_coin_asset(order["coin"]),
231
+ "o": order["oid"],
232
+ }
233
+ for order in orders
234
+ ],
235
+ }
236
+
237
+ return await self._exchange.post_action(action, self.vault)
238
+
239
+ async def modify_order(self):
240
+ # TODO: implement modify order
241
+ pass
242
+
243
+ async def set_referrer_code(self, code: str):
244
+ action = {"type": "setReferrer", "code": code}
245
+ return await self._exchange.post_action(action)
246
+
247
+ async def close_all_positions(self):
248
+ # TODO: implement close all positions
249
+ pass
250
+
251
+ async def close_position(self, coin: str):
252
+ # TODO: implement close position
253
+ pass
@@ -0,0 +1,43 @@
1
+ from typing import Optional
2
+
3
+ from aiohttp import ClientSession
4
+ from eth_account import Account
5
+
6
+ from async_hyperliquid.async_api import AsyncAPI
7
+ from async_hyperliquid.utils.miscs import get_timestamp_ms
8
+ from async_hyperliquid.utils.types import Endpoint
9
+ from async_hyperliquid.utils.signing import sign_action
10
+ from async_hyperliquid.utils.constants import MAINNET_API_URL
11
+
12
+
13
+ class ExchangeAPI(AsyncAPI):
14
+ def __init__(
15
+ self,
16
+ account: Account,
17
+ session: ClientSession,
18
+ base_url: Optional[str] = None,
19
+ address: str = None,
20
+ ):
21
+ self.account = account
22
+ self.address = address or account.address
23
+ super().__init__(Endpoint.EXCHANGE, base_url, session)
24
+
25
+ async def post_action(
26
+ self, action: dict, vault: Optional[str] = None
27
+ ) -> dict:
28
+ assert self.endpoint == Endpoint.EXCHANGE, (
29
+ "only exchange endpoint supports action"
30
+ )
31
+
32
+ nonce = get_timestamp_ms()
33
+ signature = sign_action(
34
+ self.account, action, None, nonce, self.base_url == MAINNET_API_URL
35
+ )
36
+ vault_address = vault if action["type"] != "usdClassTransfer" else None
37
+ payloads = {
38
+ "action": action,
39
+ "nonce": nonce,
40
+ "signature": signature,
41
+ "vaultAddress": vault_address,
42
+ }
43
+ 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_hyperliquid.async_api import AsyncAPI
6
+ from async_hyperliquid.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_hyperliquid.utils.types import (
10
+ OrderType,
11
+ OrderAction,
12
+ EncodedOrder,
13
+ OrderBuilder,
14
+ PlaceOrderRequest,
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: PlaceOrderRequest, 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,336 @@
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 PlaceOrderRequest(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 CancelOrderRequest(TypedDict):
80
+ coin: str
81
+ oid: str
82
+ cloid: NotRequired[Cloid]
83
+
84
+
85
+ class EncodedOrder(TypedDict):
86
+ a: int # asset universe index
87
+ b: bool # is_buy
88
+ p: str # limit_px
89
+ s: str # size
90
+ r: bool # reduce_only
91
+ t: OrderType # order type
92
+ c: NotRequired[Cloid] # cloid
93
+
94
+
95
+ class OrderBuilder(TypedDict):
96
+ b: str # builder address
97
+ f: float
98
+
99
+
100
+ class OrderAction(TypedDict):
101
+ type: Literal["order"]
102
+ orders: List[EncodedOrder]
103
+ grouping: Literal["na", "normalTpsl", "positionTpsl"]
104
+ builder: NotRequired[OrderBuilder]
105
+
106
+
107
+ class Endpoint(str, Enum):
108
+ INFO = "info"
109
+ EXCHANGE = "exchange"
110
+
111
+
112
+ class Order(TypedDict):
113
+ # TODO: add order type
114
+ pass
115
+
116
+
117
+ class FilledOrder(Order):
118
+ # TODO: add filled order type
119
+ pass
120
+
121
+
122
+ class RateLimit(TypedDict):
123
+ cumVlm: str
124
+ nRequestsUsed: int
125
+ nRequestsCap: int
126
+
127
+
128
+ class OrderStatus(TypedDict):
129
+ # TODO: add order status
130
+ pass
131
+
132
+
133
+ class L2Book(TypedDict):
134
+ px: str
135
+ sz: str
136
+ n: int
137
+
138
+
139
+ class Depth(TypedDict):
140
+ bids: List[L2Book]
141
+ asks: List[L2Book]
142
+
143
+
144
+ class PerpMeta(TypedDict):
145
+ name: str
146
+ szDecimals: int
147
+ maxLeverage: int
148
+ onlyIsolated: NotRequired[bool]
149
+ isDelisted: NotRequired[bool]
150
+
151
+
152
+ class PerpMetaResponse(TypedDict):
153
+ universe: List[PerpMeta]
154
+
155
+
156
+ class PerpMetaCtx(TypedDict):
157
+ dayNtlVlm: str
158
+ funding: str
159
+ impactPxs: List[str]
160
+ markPx: str
161
+ midPx: str
162
+ openInterest: str
163
+ oraclePx: str
164
+ premium: str
165
+ prevDayPx: str
166
+
167
+
168
+ PerpMetaCtxResponse = List[Union[PerpMetaResponse, List[PerpMetaCtx]]]
169
+
170
+
171
+ class AssetFunding(TypedDict):
172
+ allTime: str
173
+ sinceChange: str
174
+ sinceOpen: str
175
+
176
+
177
+ class AssetLeverage(TypedDict):
178
+ rawUsd: str
179
+ type: str
180
+ value: int
181
+
182
+
183
+ class Position(TypedDict):
184
+ coin: str
185
+ cumFunding: AssetFunding
186
+ entryPx: str
187
+ leverage: AssetLeverage
188
+ liquidationPx: str
189
+ marginUsed: str
190
+ maxLeverage: int
191
+ positionValue: str
192
+ returnOnEquity: str
193
+ szi: str
194
+ unrealizedPnl: str
195
+
196
+
197
+ class AssetPosition(TypedDict):
198
+ type: str
199
+ position: Position
200
+
201
+
202
+ class MarginSummary(TypedDict):
203
+ accountValue: str
204
+ totalMarginUsed: str
205
+ totalNtlPos: str
206
+ totalRawUsd: str
207
+
208
+
209
+ class ClearinghouseState(TypedDict):
210
+ assetPositions: List[AssetPosition]
211
+ crosssMaintenanceMarginUsed: str
212
+ crossMarginSummary: MarginSummary
213
+ marginSummary: MarginSummary
214
+ time: int
215
+ withdrawable: str
216
+
217
+
218
+ class UserFundingDelta(TypedDict):
219
+ coin: str
220
+ fundingRate: str
221
+ szi: str
222
+ type: str
223
+ usdc: str
224
+
225
+
226
+ class UserFunding(TypedDict):
227
+ delta: UserFundingDelta
228
+ hash: str
229
+ time: int
230
+
231
+
232
+ class FundingRate(TypedDict):
233
+ coin: str
234
+ fundingRate: str
235
+ premium: str
236
+ time: int
237
+
238
+
239
+ class Token(TypedDict):
240
+ name: str
241
+ index: int
242
+ isCanonical: bool
243
+
244
+
245
+ class TokenPairs(Token):
246
+ tokens: Tuple[int, int]
247
+
248
+
249
+ class SpotMeta(Token):
250
+ szDecimals: int
251
+ weiDecimals: int
252
+ tokenId: str
253
+ evmContract: Optional[str]
254
+ fullName: Optional[str]
255
+
256
+
257
+ class SpotMetaResponse(TypedDict):
258
+ tokens: List[SpotMeta]
259
+ universe: List[TokenPairs]
260
+
261
+
262
+ class SpotMetaCtx(TypedDict):
263
+ dayNtlVlm: str
264
+ markPx: str
265
+ midPx: str
266
+ prevDayPx: str
267
+
268
+
269
+ SpotMetaCtxResponse = List[Union[SpotMetaResponse, List[SpotMetaCtx]]]
270
+
271
+
272
+ class TokenBalance(TypedDict):
273
+ coin: str
274
+ token: int
275
+ hold: str
276
+ total: str
277
+ entryNtl: str
278
+
279
+
280
+ class SpotClearinghouseState(TypedDict):
281
+ balances: List[TokenBalance]
282
+
283
+
284
+ class GasAuction(TypedDict):
285
+ startTimeSeconds: int
286
+ durationSeconds: int
287
+ startGas: str
288
+ currentGas: Optional[str]
289
+ endGas: str
290
+
291
+
292
+ class DeployStateSpec(TypedDict):
293
+ name: str
294
+ szDecimals: int
295
+ weiDecimals: int
296
+
297
+
298
+ class DeployState(TypedDict):
299
+ token: int
300
+ spec: DeployStateSpec
301
+ fullName: str
302
+ spots: List[int]
303
+ maxSupply: int
304
+ hyperliquidityGenesisBalance: str
305
+ totalGenesisBalanceWei: str
306
+ userGenesisBalances: List[Tuple[str, str]]
307
+ existingTokenGenesisBalances: List[Tuple[int, str]]
308
+
309
+
310
+ class SpotDeployState(TypedDict):
311
+ states: List[DeployState]
312
+ gasAuction: GasAuction
313
+
314
+
315
+ class TokenGenesis(TypedDict):
316
+ userBalances: List[Tuple[str, str]]
317
+ existingTokenBalances: List[Any]
318
+
319
+
320
+ class TokenDetails(TypedDict):
321
+ name: str
322
+ maxSupply: str
323
+ totalSupply: str
324
+ circulatingSupply: str
325
+ szDecimals: int
326
+ weiDecimals: int
327
+ midPx: str
328
+ markPx: str
329
+ prevDayPx: str
330
+ genesis: List[TokenGenesis]
331
+ deployer: str
332
+ deployGas: str
333
+ deployTime: str
334
+ seededUsdc: str
335
+ nonCirculatingUserBalances: List[Any]
336
+ futureEmissions: str
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "async-hyperliquid"
3
+ version = "0.1.0"
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.12,<4.0.0)",
10
+ "msgpack (>=1.1.0,<2.0.0)",
11
+ "eth-account (>=0.13.5,<0.14.0)",
12
+ "eth-utils (>=5.2.0,<6.0.0)",
13
+ ]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/oneforalone/async-hyperliquid"
17
+ Documentation = "https://github.com/oneforalone/async-hyperliquid"
18
+ Repository = "https://github.com/oneforalone/async-hyperliquid"
19
+ Issues = "https://github.com/oneforalone/async-hyperliquid/issues"
20
+ # Changelog = "https://github.com/oneforalone/async-hyperliquid/blob/master/CHANGELOG.md"
21
+
22
+ [tool.poetry]
23
+ packages = [{ include = "async_hyperliquid" }]
24
+
25
+
26
+ [tool.poetry.group.dev.dependencies]
27
+ pytest = "^8.3.5"
28
+ pytest-asyncio = "^0.25.3"
29
+ pytest-ruff = "^0.4.1"
30
+ pytest-cov = "^6.0.0"
31
+ pre-commit = "^4.1.0"
32
+ python-dotenv = "^1.0.1"
33
+
34
+ [build-system]
35
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
36
+ build-backend = "poetry.core.masonry.api"
37
+
38
+ [tool.pytest.ini_options]
39
+ asyncio_mode = "auto"
40
+ addopts = "--verbose --capture=no"
41
+ asyncio_default_fixture_loop_scope = "session"