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.
- async_hyperliquid-0.1.0a1/PKG-INFO +27 -0
- async_hyperliquid-0.1.0a1/README.md +3 -0
- async_hyperliquid-0.1.0a1/async_hyper/__init__.py +0 -0
- async_hyperliquid-0.1.0a1/async_hyper/async_api.py +46 -0
- async_hyperliquid-0.1.0a1/async_hyper/async_hyper.py +239 -0
- async_hyperliquid-0.1.0a1/async_hyper/exchange_endpoint.py +36 -0
- async_hyperliquid-0.1.0a1/async_hyper/info_endpoint.py +207 -0
- async_hyperliquid-0.1.0a1/async_hyper/utils/__init__.py +0 -0
- async_hyperliquid-0.1.0a1/async_hyper/utils/constants.py +2 -0
- async_hyperliquid-0.1.0a1/async_hyper/utils/miscs.py +28 -0
- async_hyperliquid-0.1.0a1/async_hyper/utils/signing.py +127 -0
- async_hyperliquid-0.1.0a1/async_hyper/utils/types.py +330 -0
- async_hyperliquid-0.1.0a1/pyproject.toml +42 -0
|
@@ -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
|
+
|
|
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)
|
|
File without changes
|
|
@@ -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"
|