async-hyperliquid 0.4.2__tar.gz → 0.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/PKG-INFO +2 -1
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/pyproject.toml +3 -2
- async_hyperliquid-0.4.4/src/async_hyperliquid/_async_hyperliquid/__init__.py +3 -0
- async_hyperliquid-0.4.4/src/async_hyperliquid/_async_hyperliquid/actions.py +261 -0
- async_hyperliquid-0.4.4/src/async_hyperliquid/_async_hyperliquid/core.py +276 -0
- async_hyperliquid-0.4.4/src/async_hyperliquid/_async_hyperliquid/info.py +222 -0
- async_hyperliquid-0.4.4/src/async_hyperliquid/_async_hyperliquid/orders.py +390 -0
- async_hyperliquid-0.4.4/src/async_hyperliquid/async_api.py +85 -0
- async_hyperliquid-0.4.4/src/async_hyperliquid/async_hyperliquid.py +214 -0
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/exchange.py +14 -7
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/info.py +5 -16
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/signing.py +111 -70
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/types.py +1 -5
- async_hyperliquid-0.4.2/src/async_hyperliquid/async_api.py +0 -51
- async_hyperliquid-0.4.2/src/async_hyperliquid/async_hyperliquid.py +0 -1138
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/LICENSE +0 -0
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/README.md +0 -0
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/__init__.py +0 -0
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/__init__.py +0 -0
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/constants.py +0 -0
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/decorators.py +0 -0
- {async_hyperliquid-0.4.2 → async_hyperliquid-0.4.4}/src/async_hyperliquid/utils/miscs.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: async-hyperliquid
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Async Hyperliquid client using aiohttp
|
|
5
5
|
Keywords: dex,hyperliquid,async,aiohttp,trading,cryptocurrency,defi
|
|
6
6
|
Author: Yuki
|
|
@@ -48,6 +48,7 @@ Requires-Dist: msgpack>=1.1.0,<2.0.0
|
|
|
48
48
|
Requires-Dist: eth-account>=0.13.5,<0.14.0
|
|
49
49
|
Requires-Dist: eth-utils>=5.2.0,<6.0.0
|
|
50
50
|
Requires-Dist: hl-web3>=0.1.0
|
|
51
|
+
Requires-Dist: coincurve>=21.0.0
|
|
51
52
|
Requires-Python: >=3.10, <4
|
|
52
53
|
Project-URL: Changelog, https://github.com/traderfiapp/async-hyperliquid/blob/master/CHANGELOG.md
|
|
53
54
|
Project-URL: Documentation, https://github.com/traderfiapp/async-hyperliquid
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "async-hyperliquid"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
4
|
description = "Async Hyperliquid client using aiohttp"
|
|
5
5
|
authors = [{ name = "Yuki", email = "yuqi.lyle@gmail.com" }]
|
|
6
6
|
readme = "README.md"
|
|
@@ -35,11 +35,12 @@ classifiers = [
|
|
|
35
35
|
]
|
|
36
36
|
requires-python = ">=3.10,<4"
|
|
37
37
|
dependencies = [
|
|
38
|
-
"aiohttp
|
|
38
|
+
"aiohttp>=3.11.12,<4.0.0",
|
|
39
39
|
"msgpack (>=1.1.0,<2.0.0)",
|
|
40
40
|
"eth-account (>=0.13.5,<0.14.0)",
|
|
41
41
|
"eth-utils (>=5.2.0,<6.0.0)",
|
|
42
42
|
"hl-web3>=0.1.0",
|
|
43
|
+
"coincurve>=21.0.0",
|
|
43
44
|
]
|
|
44
45
|
|
|
45
46
|
[dependency-groups]
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import re
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from async_hyperliquid.utils.constants import HYPE_FACTOR, MAINNET_API_URL, USD_FACTOR
|
|
6
|
+
from async_hyperliquid.utils.decorators import private_key_required
|
|
7
|
+
from async_hyperliquid.utils.miscs import round_token_amount
|
|
8
|
+
from async_hyperliquid.utils.signing import (
|
|
9
|
+
sign_approve_agent_action,
|
|
10
|
+
sign_approve_builder_fee_action,
|
|
11
|
+
sign_convert_to_multi_sig_user_action,
|
|
12
|
+
sign_send_asset_action,
|
|
13
|
+
sign_spot_transfer_action,
|
|
14
|
+
sign_staking_deposit_action,
|
|
15
|
+
sign_staking_withdraw_action,
|
|
16
|
+
sign_token_delegate_action,
|
|
17
|
+
sign_usd_class_transfer_action,
|
|
18
|
+
sign_usd_transfer_action,
|
|
19
|
+
sign_user_dex_abstraction_action,
|
|
20
|
+
sign_user_set_abstraction_action,
|
|
21
|
+
sign_withdraw_action,
|
|
22
|
+
)
|
|
23
|
+
from async_hyperliquid.utils.types import AgentAbstraction, UserSetAbstraction
|
|
24
|
+
|
|
25
|
+
from .orders import AsyncHyperliquidOrdersClient
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncHyperliquidActionsClient(AsyncHyperliquidOrdersClient):
|
|
29
|
+
async def set_referrer_code(self, code: str):
|
|
30
|
+
action = {"type": "setReferrer", "code": code}
|
|
31
|
+
return await self.exchange.post_action(action)
|
|
32
|
+
|
|
33
|
+
@private_key_required
|
|
34
|
+
async def create_sub_account(self, name: str):
|
|
35
|
+
action = {"type": "createSubAccount", "name": name}
|
|
36
|
+
return await self.exchange.post_action(action)
|
|
37
|
+
|
|
38
|
+
@private_key_required
|
|
39
|
+
async def usd_transfer(self, amount: float, dest: str):
|
|
40
|
+
nonce = self.next_nonce()
|
|
41
|
+
action = {
|
|
42
|
+
"type": "usdSend",
|
|
43
|
+
"amount": round_token_amount(amount, 2),
|
|
44
|
+
"destination": dest,
|
|
45
|
+
"time": nonce,
|
|
46
|
+
}
|
|
47
|
+
is_mainnet = self.base_url == MAINNET_API_URL
|
|
48
|
+
sig = sign_usd_transfer_action(self.account, action, is_mainnet)
|
|
49
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
50
|
+
|
|
51
|
+
@private_key_required
|
|
52
|
+
async def spot_transfer(self, coin: str, amount: float, dest: str):
|
|
53
|
+
token_info = await self.get_token_info(coin)
|
|
54
|
+
token_name = token_info["name"]
|
|
55
|
+
token_id = token_info["tokenId"]
|
|
56
|
+
wei_decimals = token_info["weiDecimals"]
|
|
57
|
+
token = f"{token_name}:{token_id}"
|
|
58
|
+
nonce = self.next_nonce()
|
|
59
|
+
action = {
|
|
60
|
+
"type": "spotSend",
|
|
61
|
+
"destination": dest,
|
|
62
|
+
"token": token,
|
|
63
|
+
"amount": round_token_amount(amount, wei_decimals),
|
|
64
|
+
"time": nonce,
|
|
65
|
+
}
|
|
66
|
+
sig = sign_spot_transfer_action(self.account, action, self.is_mainnet)
|
|
67
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
68
|
+
|
|
69
|
+
@private_key_required
|
|
70
|
+
async def initiate_withdrawal(self, amount: float):
|
|
71
|
+
nonce = self.next_nonce()
|
|
72
|
+
action = {
|
|
73
|
+
"type": "withdraw3",
|
|
74
|
+
"amount": round_token_amount(amount, 2),
|
|
75
|
+
"time": nonce,
|
|
76
|
+
"destination": self.address,
|
|
77
|
+
}
|
|
78
|
+
sig = sign_withdraw_action(self.account, action, self.is_mainnet)
|
|
79
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
80
|
+
|
|
81
|
+
@private_key_required
|
|
82
|
+
async def usd_class_transfer(self, amount: float, to_perp: bool = False):
|
|
83
|
+
nonce = self.next_nonce()
|
|
84
|
+
action = {
|
|
85
|
+
"type": "usdClassTransfer",
|
|
86
|
+
"amount": round_token_amount(amount, 2),
|
|
87
|
+
"toPerp": to_perp,
|
|
88
|
+
"nonce": nonce,
|
|
89
|
+
}
|
|
90
|
+
sig = sign_usd_class_transfer_action(
|
|
91
|
+
self.account, action, self.base_url == MAINNET_API_URL
|
|
92
|
+
)
|
|
93
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
94
|
+
|
|
95
|
+
@private_key_required
|
|
96
|
+
async def send_asset(
|
|
97
|
+
self,
|
|
98
|
+
coin: str,
|
|
99
|
+
amount: float,
|
|
100
|
+
dest: str,
|
|
101
|
+
source_dex: str,
|
|
102
|
+
dest_dex: str,
|
|
103
|
+
sub_account: str = "",
|
|
104
|
+
):
|
|
105
|
+
token_info = await self.get_token_info(coin)
|
|
106
|
+
token_name = token_info["name"]
|
|
107
|
+
token_id = token_info["tokenId"]
|
|
108
|
+
wei_decimals = token_info["weiDecimals"]
|
|
109
|
+
token = f"{token_name}:{token_id}"
|
|
110
|
+
nonce = self.next_nonce()
|
|
111
|
+
action = {
|
|
112
|
+
"type": "sendAsset",
|
|
113
|
+
"token": token,
|
|
114
|
+
"amount": round_token_amount(amount, wei_decimals),
|
|
115
|
+
"destination": dest,
|
|
116
|
+
"sourceDex": source_dex,
|
|
117
|
+
"destinationDex": dest_dex,
|
|
118
|
+
"fromSubAccount": sub_account,
|
|
119
|
+
"nonce": nonce,
|
|
120
|
+
}
|
|
121
|
+
sig = sign_send_asset_action(self.account, action, self.is_mainnet)
|
|
122
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
123
|
+
|
|
124
|
+
@private_key_required
|
|
125
|
+
async def staking_deposit(self, amount: float):
|
|
126
|
+
amount_in_wei = int(math.floor(amount * HYPE_FACTOR))
|
|
127
|
+
nonce = self.next_nonce()
|
|
128
|
+
action = {"type": "cDeposit", "wei": amount_in_wei, "nonce": nonce}
|
|
129
|
+
sig = sign_staking_deposit_action(self.account, action, self.is_mainnet)
|
|
130
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
131
|
+
|
|
132
|
+
@private_key_required
|
|
133
|
+
async def staking_withdraw(self, amount: float):
|
|
134
|
+
amount_in_wei = int(math.floor(amount * HYPE_FACTOR))
|
|
135
|
+
nonce = self.next_nonce()
|
|
136
|
+
action = {"type": "cWithdraw", "wei": amount_in_wei, "nonce": nonce}
|
|
137
|
+
sig = sign_staking_withdraw_action(self.account, action, self.is_mainnet)
|
|
138
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
139
|
+
|
|
140
|
+
@private_key_required
|
|
141
|
+
async def token_delegate(
|
|
142
|
+
self, validator: str, amount: float, is_undelegate: bool = False
|
|
143
|
+
):
|
|
144
|
+
amount_in_wei = int(math.floor(amount * HYPE_FACTOR))
|
|
145
|
+
nonce = self.next_nonce()
|
|
146
|
+
action = {
|
|
147
|
+
"type": "tokenDelegate",
|
|
148
|
+
"validator": validator,
|
|
149
|
+
"wei": amount_in_wei,
|
|
150
|
+
"isUndelegate": is_undelegate,
|
|
151
|
+
"nonce": nonce,
|
|
152
|
+
}
|
|
153
|
+
sig = sign_token_delegate_action(self.account, action, self.is_mainnet)
|
|
154
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
155
|
+
|
|
156
|
+
@private_key_required
|
|
157
|
+
async def vault_transfer(self, vault: str, amount: float, is_deposit: bool = True):
|
|
158
|
+
usd_amount = int(math.floor(amount * USD_FACTOR))
|
|
159
|
+
action = {
|
|
160
|
+
"type": "vaultTransfer",
|
|
161
|
+
"vaultAddress": vault,
|
|
162
|
+
"isDeposit": is_deposit,
|
|
163
|
+
"usd": usd_amount,
|
|
164
|
+
}
|
|
165
|
+
return await self.exchange.post_action(action)
|
|
166
|
+
|
|
167
|
+
async def approve_agent(self, agent: str, name: str | None = None):
|
|
168
|
+
nonce = self.next_nonce()
|
|
169
|
+
action = {
|
|
170
|
+
"type": "approveAgent",
|
|
171
|
+
"agentAddress": agent,
|
|
172
|
+
"agentName": name or "",
|
|
173
|
+
"nonce": nonce,
|
|
174
|
+
}
|
|
175
|
+
sig = sign_approve_agent_action(self.account, action, self.is_mainnet)
|
|
176
|
+
if name is None:
|
|
177
|
+
del action["agentName"]
|
|
178
|
+
|
|
179
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
180
|
+
|
|
181
|
+
async def approve_builder_fee(self, max_fee_rate: float, builder: str):
|
|
182
|
+
nonce = self.next_nonce()
|
|
183
|
+
action = {
|
|
184
|
+
"type": "approveBuilderFee",
|
|
185
|
+
"maxFeeRate": f"{max_fee_rate:.3%}",
|
|
186
|
+
"builder": builder,
|
|
187
|
+
"nonce": nonce,
|
|
188
|
+
}
|
|
189
|
+
sig = sign_approve_builder_fee_action(self.account, action, self.is_mainnet)
|
|
190
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
191
|
+
|
|
192
|
+
async def convert_to_multi_sig_user(self, users: list[str], threshold: int):
|
|
193
|
+
nonce = self.next_nonce()
|
|
194
|
+
signers = {"authorizedUsers": sorted(users), "threshold": threshold}
|
|
195
|
+
action = {"type": "convertToMultiSigUser", "signers": signers, "nonce": nonce}
|
|
196
|
+
sig = sign_convert_to_multi_sig_user_action(
|
|
197
|
+
self.account, action, self.is_mainnet
|
|
198
|
+
)
|
|
199
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
200
|
+
|
|
201
|
+
async def reserve_request_weight(self, weight: int):
|
|
202
|
+
action = {"type": "reserveRequestWeight", "weight": weight}
|
|
203
|
+
return await self.exchange.post_action(action, expires=self.expires)
|
|
204
|
+
|
|
205
|
+
async def use_big_block(self, enable: bool):
|
|
206
|
+
action = {"type": "evmUserModify", "usingBigBlocks": enable}
|
|
207
|
+
return await self.exchange.post_action(action)
|
|
208
|
+
|
|
209
|
+
async def user_dex_abstraction(self, user: str | None = None, enabled: bool = True):
|
|
210
|
+
warnings.warn(
|
|
211
|
+
"user_dex_abstraction is deprecated and may be removed in a "
|
|
212
|
+
"future release.",
|
|
213
|
+
DeprecationWarning,
|
|
214
|
+
stacklevel=2,
|
|
215
|
+
)
|
|
216
|
+
nonce = self.next_nonce()
|
|
217
|
+
if user is None:
|
|
218
|
+
user = self.address
|
|
219
|
+
action = {
|
|
220
|
+
"type": "userDexAbstraction",
|
|
221
|
+
"user": user.lower(),
|
|
222
|
+
"enabled": enabled,
|
|
223
|
+
"nonce": nonce,
|
|
224
|
+
}
|
|
225
|
+
sig = sign_user_dex_abstraction_action(self.account, action, self.is_mainnet)
|
|
226
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
227
|
+
|
|
228
|
+
async def user_set_abstraction(
|
|
229
|
+
self, abstraction: UserSetAbstraction, user: str | None = None
|
|
230
|
+
):
|
|
231
|
+
if user is None:
|
|
232
|
+
user = self.address
|
|
233
|
+
if re.fullmatch(r"0x[a-fA-F0-9]{40}", user) is None:
|
|
234
|
+
raise ValueError(f"user must be a 42-char hex address, got: {user!r}")
|
|
235
|
+
nonce = self.next_nonce()
|
|
236
|
+
action = {
|
|
237
|
+
"type": "userSetAbstraction",
|
|
238
|
+
"user": user.lower(),
|
|
239
|
+
"abstraction": abstraction,
|
|
240
|
+
"nonce": nonce,
|
|
241
|
+
}
|
|
242
|
+
sig = sign_user_set_abstraction_action(self.account, action, self.is_mainnet)
|
|
243
|
+
return await self.exchange.post_action_with_sig(action, sig, nonce)
|
|
244
|
+
|
|
245
|
+
async def agent_enable_dex_abstraction(self):
|
|
246
|
+
warnings.warn(
|
|
247
|
+
"agent_enable_dex_abstraction is deprecated and may be removed "
|
|
248
|
+
"in a future release.",
|
|
249
|
+
DeprecationWarning,
|
|
250
|
+
stacklevel=2,
|
|
251
|
+
)
|
|
252
|
+
action = {"type": "agentEnableDexAbstraction"}
|
|
253
|
+
return await self.exchange.post_action(
|
|
254
|
+
action, vault=self.vault, expires=self.expires
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def agent_set_abstraction(self, abstraction: AgentAbstraction):
|
|
258
|
+
action = {"type": "agentSetAbstraction", "abstraction": abstraction}
|
|
259
|
+
return await self.exchange.post_action(
|
|
260
|
+
action, vault=self.vault, expires=self.expires
|
|
261
|
+
)
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from threading import Lock
|
|
3
|
+
|
|
4
|
+
from aiohttp import TCPConnector, BaseConnector, ClientSession, ClientTimeout
|
|
5
|
+
from eth_account import Account
|
|
6
|
+
from hl_web3.info import Info as EVMInfo
|
|
7
|
+
from hl_web3.exchange import Exchange as EVMExchange
|
|
8
|
+
from hl_web3.utils.constants import HL_RPC_URL, HL_TESTNET_RPC_URL
|
|
9
|
+
from eth_account.signers.local import LocalAccount
|
|
10
|
+
|
|
11
|
+
from async_hyperliquid.info import InfoAPI
|
|
12
|
+
from async_hyperliquid.exchange import ExchangeAPI
|
|
13
|
+
from async_hyperliquid.async_api import AsyncAPI
|
|
14
|
+
from async_hyperliquid.utils.miscs import get_timestamp_ms
|
|
15
|
+
from async_hyperliquid.utils.types import Metas, PerpMeta, SpotMeta, SpotTokenMeta
|
|
16
|
+
from async_hyperliquid.utils.constants import (
|
|
17
|
+
SPOT_OFFSET,
|
|
18
|
+
MAINNET_API_URL,
|
|
19
|
+
PERP_DEX_OFFSET,
|
|
20
|
+
TESTNET_API_URL,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AsyncHyperliquidCore(AsyncAPI):
|
|
25
|
+
address: str
|
|
26
|
+
is_mainnet: bool
|
|
27
|
+
account: LocalAccount
|
|
28
|
+
session: ClientSession
|
|
29
|
+
base_url: str
|
|
30
|
+
vault: str | None
|
|
31
|
+
|
|
32
|
+
coin_assets: dict[str, int]
|
|
33
|
+
coin_names: dict[str, str]
|
|
34
|
+
coin_symbols: dict[str, str]
|
|
35
|
+
asset_sz_decimals: dict[int, int]
|
|
36
|
+
spot_tokens: dict[str, SpotTokenMeta]
|
|
37
|
+
|
|
38
|
+
perp_dexs: list[str]
|
|
39
|
+
|
|
40
|
+
enable_evm: bool
|
|
41
|
+
evm_info: EVMInfo
|
|
42
|
+
evm_exchange: EVMExchange
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
address: str,
|
|
47
|
+
api_key: str,
|
|
48
|
+
is_mainnet: bool = True,
|
|
49
|
+
enable_evm: bool = False,
|
|
50
|
+
evm_rpc_url: str | None = None,
|
|
51
|
+
private_key: str | None = None,
|
|
52
|
+
vault: str | None = None,
|
|
53
|
+
perp_dexs: list[str] = [""],
|
|
54
|
+
session: ClientSession | None = None,
|
|
55
|
+
timeout: ClientTimeout | None = None,
|
|
56
|
+
connector: BaseConnector | None = None,
|
|
57
|
+
):
|
|
58
|
+
self.address = address
|
|
59
|
+
self.is_mainnet = is_mainnet
|
|
60
|
+
self.account = Account.from_key(api_key)
|
|
61
|
+
self._nonce_lock = Lock()
|
|
62
|
+
self._last_nonce = 0
|
|
63
|
+
self._owns_session = session is None
|
|
64
|
+
self.session = session or self._build_session(
|
|
65
|
+
timeout=timeout, connector=connector
|
|
66
|
+
)
|
|
67
|
+
self.base_url = MAINNET_API_URL if is_mainnet else TESTNET_API_URL
|
|
68
|
+
self.info = InfoAPI(self.base_url, self.session)
|
|
69
|
+
self.exchange = ExchangeAPI(
|
|
70
|
+
self.account,
|
|
71
|
+
self.session,
|
|
72
|
+
self.base_url,
|
|
73
|
+
address=self.address,
|
|
74
|
+
nonce_factory=self.next_nonce,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self.coin_assets = {}
|
|
78
|
+
self.coin_names = {}
|
|
79
|
+
self.coin_symbols = {}
|
|
80
|
+
self.asset_sz_decimals = {}
|
|
81
|
+
self.spot_tokens = {}
|
|
82
|
+
|
|
83
|
+
self.vault = vault
|
|
84
|
+
self.expires: int | None = None
|
|
85
|
+
self.perp_dexs = perp_dexs
|
|
86
|
+
|
|
87
|
+
if enable_evm:
|
|
88
|
+
self._init_evm_client(private_key, evm_rpc_url)
|
|
89
|
+
|
|
90
|
+
def set_expires(self, expires: int | None) -> None:
|
|
91
|
+
self.expires = expires
|
|
92
|
+
|
|
93
|
+
def _build_session(
|
|
94
|
+
self, *, timeout: ClientTimeout | None, connector: BaseConnector | None
|
|
95
|
+
) -> ClientSession:
|
|
96
|
+
resolved_timeout = timeout or ClientTimeout(
|
|
97
|
+
connect=3, sock_connect=3, sock_read=10
|
|
98
|
+
)
|
|
99
|
+
resolved_connector = connector or TCPConnector(
|
|
100
|
+
ttl_dns_cache=300, enable_cleanup_closed=True
|
|
101
|
+
)
|
|
102
|
+
return ClientSession(timeout=resolved_timeout, connector=resolved_connector)
|
|
103
|
+
|
|
104
|
+
def next_nonce(self) -> int:
|
|
105
|
+
with self._nonce_lock:
|
|
106
|
+
nonce = get_timestamp_ms()
|
|
107
|
+
if nonce <= self._last_nonce:
|
|
108
|
+
nonce = self._last_nonce + 1
|
|
109
|
+
self._last_nonce = nonce
|
|
110
|
+
return nonce
|
|
111
|
+
|
|
112
|
+
def _init_evm_client(
|
|
113
|
+
self, private_key: str | None, rpc_url: str | None = None
|
|
114
|
+
) -> None:
|
|
115
|
+
if rpc_url is None:
|
|
116
|
+
rpc_url = HL_RPC_URL if self.is_mainnet else HL_TESTNET_RPC_URL
|
|
117
|
+
|
|
118
|
+
self.evm_info = EVMInfo(rpc_url)
|
|
119
|
+
|
|
120
|
+
if private_key is None:
|
|
121
|
+
if self.account.address != self.address:
|
|
122
|
+
raise ValueError("EVM Exchange client can not init without private key")
|
|
123
|
+
private_key = self.account.key.hex()
|
|
124
|
+
|
|
125
|
+
self.evm_exchange = EVMExchange(rpc_url, private_key)
|
|
126
|
+
|
|
127
|
+
def _init_perp_meta(self, meta: PerpMeta, offset: int) -> None:
|
|
128
|
+
for asset, info in enumerate(meta["universe"]):
|
|
129
|
+
asset += offset
|
|
130
|
+
asset_name = info["name"]
|
|
131
|
+
self.coin_assets[asset_name] = asset
|
|
132
|
+
self.coin_names[asset_name] = asset_name
|
|
133
|
+
self.asset_sz_decimals[asset] = info["szDecimals"]
|
|
134
|
+
|
|
135
|
+
def _init_spot_meta(self, meta: SpotMeta) -> None:
|
|
136
|
+
tokens = meta["tokens"]
|
|
137
|
+
total_tokens = len(tokens)
|
|
138
|
+
for info in meta["universe"]:
|
|
139
|
+
asset = info["index"] + SPOT_OFFSET
|
|
140
|
+
asset_name = info["name"]
|
|
141
|
+
|
|
142
|
+
self.coin_assets[asset_name] = asset
|
|
143
|
+
self.coin_names[asset_name] = asset_name
|
|
144
|
+
|
|
145
|
+
base, quote = info["tokens"]
|
|
146
|
+
if not 0 <= base < total_tokens or not 0 <= quote < total_tokens:
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
base_info = tokens[base]
|
|
150
|
+
base_name = base_info["name"]
|
|
151
|
+
quote_info = tokens[quote]
|
|
152
|
+
quote_name = quote_info["name"]
|
|
153
|
+
name = f"{base_name}/{quote_name}"
|
|
154
|
+
self.coin_names.setdefault(name, asset_name)
|
|
155
|
+
self.coin_names.setdefault(quote_name, quote_name)
|
|
156
|
+
|
|
157
|
+
self.asset_sz_decimals[asset] = base_info["szDecimals"]
|
|
158
|
+
self.spot_tokens[asset_name] = base_info
|
|
159
|
+
self.spot_tokens.setdefault(quote_name, quote_info)
|
|
160
|
+
|
|
161
|
+
def _update_coin_symbols(self) -> None:
|
|
162
|
+
self.coin_symbols = {
|
|
163
|
+
v: k for k, v in self.coin_names.items() if not k.startswith("@")
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async def init_metas(self) -> None:
|
|
167
|
+
meta_task = self.info.get_perp_meta()
|
|
168
|
+
spot_meta_task = self.info.get_spot_meta()
|
|
169
|
+
all_dex_names_task = self.get_all_dex_name()
|
|
170
|
+
|
|
171
|
+
meta, spot_meta, all_dex_names = await asyncio.gather(
|
|
172
|
+
meta_task, spot_meta_task, all_dex_names_task
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self._init_perp_meta(meta, 0)
|
|
176
|
+
self._init_spot_meta(spot_meta)
|
|
177
|
+
|
|
178
|
+
dex_meta_tasks = []
|
|
179
|
+
dex_indices = []
|
|
180
|
+
for dex in self.perp_dexs:
|
|
181
|
+
if dex == "":
|
|
182
|
+
continue
|
|
183
|
+
try:
|
|
184
|
+
idx = all_dex_names.index(dex)
|
|
185
|
+
except ValueError:
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
if idx > 0:
|
|
189
|
+
dex_meta_tasks.append(self.info.get_perp_meta(dex))
|
|
190
|
+
dex_indices.append(idx)
|
|
191
|
+
|
|
192
|
+
if dex_meta_tasks:
|
|
193
|
+
dex_metas = await asyncio.gather(*dex_meta_tasks)
|
|
194
|
+
for idx, dex_meta in zip(dex_indices, dex_metas):
|
|
195
|
+
dex_asset_offset = PERP_DEX_OFFSET + (idx - 1) * 10000
|
|
196
|
+
self._init_perp_meta(dex_meta, dex_asset_offset)
|
|
197
|
+
|
|
198
|
+
self._update_coin_symbols()
|
|
199
|
+
|
|
200
|
+
async def get_metas(self, perp_only: bool = False) -> Metas:
|
|
201
|
+
metas: Metas = {"perp": {}, "spot": [], "dexs": {}} # type: ignore
|
|
202
|
+
if perp_only:
|
|
203
|
+
metas["perp"] = await self.info.get_perp_meta()
|
|
204
|
+
return metas
|
|
205
|
+
|
|
206
|
+
perp_meta, spot_meta = await asyncio.gather(
|
|
207
|
+
self.info.get_perp_meta(), self.info.get_spot_meta()
|
|
208
|
+
)
|
|
209
|
+
metas["perp"] = perp_meta
|
|
210
|
+
metas["spot"] = spot_meta
|
|
211
|
+
return metas
|
|
212
|
+
|
|
213
|
+
async def get_all_metas(self) -> Metas:
|
|
214
|
+
dexs, perp_meta, spot_meta = await asyncio.gather(
|
|
215
|
+
self.get_all_dex_name(),
|
|
216
|
+
self.info.get_perp_meta(),
|
|
217
|
+
self.info.get_spot_meta(),
|
|
218
|
+
)
|
|
219
|
+
dex_metas: dict[str, PerpMeta] = {}
|
|
220
|
+
if len(dexs) > 1:
|
|
221
|
+
dex_meta_results = await asyncio.gather(
|
|
222
|
+
*(self.info.get_perp_meta(dex) for dex in dexs[1:])
|
|
223
|
+
)
|
|
224
|
+
dex_metas = {
|
|
225
|
+
dex: meta for dex, meta in zip(dexs[1:], dex_meta_results, strict=True)
|
|
226
|
+
}
|
|
227
|
+
return {"perp": perp_meta, "spot": spot_meta, "dexs": dex_metas}
|
|
228
|
+
|
|
229
|
+
async def get_all_dex_name(self) -> list[str]:
|
|
230
|
+
names = []
|
|
231
|
+
dexs = await self.info.get_perp_dexs()
|
|
232
|
+
for dex in dexs:
|
|
233
|
+
if dex is None:
|
|
234
|
+
names.append("")
|
|
235
|
+
else:
|
|
236
|
+
names.append(dex["name"])
|
|
237
|
+
return names
|
|
238
|
+
|
|
239
|
+
async def get_coin_name(self, coin: str) -> str:
|
|
240
|
+
if not hasattr(self, "coin_names") or coin not in self.coin_names:
|
|
241
|
+
await self.init_metas()
|
|
242
|
+
|
|
243
|
+
if coin not in self.coin_names:
|
|
244
|
+
raise ValueError(f"Coin {coin} not found")
|
|
245
|
+
|
|
246
|
+
return self.coin_names[coin]
|
|
247
|
+
|
|
248
|
+
async def get_coin_asset(self, coin: str) -> int:
|
|
249
|
+
coin_name = await self.get_coin_name(coin)
|
|
250
|
+
if coin_name not in self.coin_assets:
|
|
251
|
+
raise ValueError(f"Coin {coin}({coin_name}) not found")
|
|
252
|
+
|
|
253
|
+
return self.coin_assets[coin_name]
|
|
254
|
+
|
|
255
|
+
async def get_coin_symbol(self, coin: str) -> str:
|
|
256
|
+
coin_name = await self.get_coin_name(coin)
|
|
257
|
+
return self.coin_symbols[coin_name]
|
|
258
|
+
|
|
259
|
+
async def get_coin_sz_decimals(self, coin: str) -> int:
|
|
260
|
+
coin_name = await self.get_coin_name(coin)
|
|
261
|
+
if coin_name not in self.coin_assets:
|
|
262
|
+
raise ValueError(f"Coin {coin}({coin_name}) not found")
|
|
263
|
+
|
|
264
|
+
asset = self.coin_assets[coin_name]
|
|
265
|
+
return self.asset_sz_decimals[asset]
|
|
266
|
+
|
|
267
|
+
async def get_token_info(self, coin: str) -> SpotTokenMeta:
|
|
268
|
+
coin_name = await self.get_coin_name(coin)
|
|
269
|
+
return self.spot_tokens[coin_name]
|
|
270
|
+
|
|
271
|
+
async def get_token_id(self, coin: str) -> str:
|
|
272
|
+
token_info = await self.get_token_info(coin)
|
|
273
|
+
if not token_info:
|
|
274
|
+
raise ValueError(f"Token {coin} not found")
|
|
275
|
+
|
|
276
|
+
return token_info["tokenId"]
|