wayfinder-paths 0.1.25__py3-none-any.whl → 0.1.27__py3-none-any.whl
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.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/adapters/brap_adapter/adapter.py +7 -47
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +10 -31
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +128 -60
- wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +399 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +74 -0
- wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +82 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/util.py +237 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +19 -55
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +14 -46
- wayfinder_paths/core/clients/BalanceClient.py +72 -0
- wayfinder_paths/core/clients/TokenClient.py +1 -1
- wayfinder_paths/core/clients/__init__.py +2 -0
- wayfinder_paths/core/strategies/Strategy.py +3 -3
- wayfinder_paths/core/types.py +19 -0
- wayfinder_paths/core/utils/tokens.py +19 -1
- wayfinder_paths/core/utils/transaction.py +9 -7
- wayfinder_paths/mcp/tools/balances.py +122 -214
- wayfinder_paths/mcp/tools/execute.py +63 -41
- wayfinder_paths/mcp/tools/quotes.py +16 -5
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +6 -22
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +227 -33
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +17 -1
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +44 -1
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +87 -32
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +50 -28
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +71 -50
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +3 -1
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +0 -2
- wayfinder_paths/strategies/boros_hype_strategy/types.py +4 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +0 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -2
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +0 -2
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +0 -2
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -2
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -2
- wayfinder_paths/tests/test_mcp_quote_swap.py +3 -3
- {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.27.dist-info}/METADATA +1 -1
- {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.27.dist-info}/RECORD +42 -37
- {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.27.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.27.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from eth_account.messages import encode_typed_data
|
|
6
|
+
from eth_utils import keccak
|
|
7
|
+
from hyperliquid.api import API
|
|
8
|
+
from hyperliquid.exchange import get_timestamp_ms
|
|
9
|
+
from hyperliquid.info import Info
|
|
10
|
+
from hyperliquid.utils.signing import (
|
|
11
|
+
BUILDER_FEE_SIGN_TYPES,
|
|
12
|
+
SPOT_TRANSFER_SIGN_TYPES,
|
|
13
|
+
USD_CLASS_TRANSFER_SIGN_TYPES,
|
|
14
|
+
USER_DEX_ABSTRACTION_SIGN_TYPES,
|
|
15
|
+
WITHDRAW_SIGN_TYPES,
|
|
16
|
+
OrderType,
|
|
17
|
+
OrderWire,
|
|
18
|
+
float_to_wire,
|
|
19
|
+
get_l1_action_payload,
|
|
20
|
+
order_type_to_wire,
|
|
21
|
+
order_wires_to_order_action,
|
|
22
|
+
user_signed_payload,
|
|
23
|
+
)
|
|
24
|
+
from hyperliquid.utils.types import BuilderInfo
|
|
25
|
+
from loguru import logger
|
|
26
|
+
from web3 import Web3
|
|
27
|
+
|
|
28
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.util import Util
|
|
29
|
+
from wayfinder_paths.core.types import HyperliquidSignCallback
|
|
30
|
+
|
|
31
|
+
ARBITRUM_CHAIN_ID = "0xa4b1"
|
|
32
|
+
MAINNET = "Mainnet"
|
|
33
|
+
USER_DECLINED_ERROR = {
|
|
34
|
+
"status": "err",
|
|
35
|
+
"error": "User declined transaction. Please try again..",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Exchange:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
info: Info,
|
|
43
|
+
util: Util,
|
|
44
|
+
sign_callback: HyperliquidSignCallback,
|
|
45
|
+
signing_type: Literal["eip712", "local"],
|
|
46
|
+
):
|
|
47
|
+
self.info = info
|
|
48
|
+
self.util = util
|
|
49
|
+
self.api = API()
|
|
50
|
+
self.sign_callback = sign_callback
|
|
51
|
+
self.signing_type = signing_type
|
|
52
|
+
|
|
53
|
+
def _create_hypecore_order_actions(
|
|
54
|
+
self,
|
|
55
|
+
asset_id: int,
|
|
56
|
+
is_buy: bool,
|
|
57
|
+
price: float,
|
|
58
|
+
size: float,
|
|
59
|
+
reduce_only: bool,
|
|
60
|
+
order_type: OrderType,
|
|
61
|
+
builder: BuilderInfo | None = None,
|
|
62
|
+
cloid: str | None = None,
|
|
63
|
+
):
|
|
64
|
+
order: OrderWire = {
|
|
65
|
+
"a": asset_id,
|
|
66
|
+
"b": is_buy,
|
|
67
|
+
"p": float_to_wire(price),
|
|
68
|
+
"s": float_to_wire(size),
|
|
69
|
+
"r": reduce_only,
|
|
70
|
+
"t": order_type_to_wire(order_type),
|
|
71
|
+
}
|
|
72
|
+
if cloid is not None:
|
|
73
|
+
order["c"] = cloid
|
|
74
|
+
return order_wires_to_order_action([order], builder)
|
|
75
|
+
|
|
76
|
+
async def place_market_order(
|
|
77
|
+
self,
|
|
78
|
+
asset_id: int,
|
|
79
|
+
is_buy: bool,
|
|
80
|
+
slippage: float,
|
|
81
|
+
size: float,
|
|
82
|
+
address: str,
|
|
83
|
+
reduce_only: bool = False,
|
|
84
|
+
builder: BuilderInfo | None = None,
|
|
85
|
+
cloid: str | None = None,
|
|
86
|
+
):
|
|
87
|
+
"""Place a market order (IOC limit with slippage)."""
|
|
88
|
+
asset_name = self.info.asset_to_coin[asset_id]
|
|
89
|
+
mids = await self.info.all_dex_mid_prices()
|
|
90
|
+
midprice = float(mids[asset_name])
|
|
91
|
+
|
|
92
|
+
if slippage >= 1 or slippage < 0:
|
|
93
|
+
return {"error": f"slippage must be in [0, 1), got {slippage}"}
|
|
94
|
+
|
|
95
|
+
price = midprice * ((1 + slippage) if is_buy else (1 - slippage))
|
|
96
|
+
price = round(
|
|
97
|
+
float(f"{price:.5g}"),
|
|
98
|
+
self.util.get_price_decimals_for_hypecore_asset(asset_id),
|
|
99
|
+
)
|
|
100
|
+
order_actions = self._create_hypecore_order_actions(
|
|
101
|
+
asset_id,
|
|
102
|
+
is_buy,
|
|
103
|
+
price,
|
|
104
|
+
size,
|
|
105
|
+
reduce_only,
|
|
106
|
+
{"limit": {"tif": "Ioc"}},
|
|
107
|
+
builder,
|
|
108
|
+
cloid,
|
|
109
|
+
)
|
|
110
|
+
return await self.sign_and_broadcast_hypecore(order_actions, address)
|
|
111
|
+
|
|
112
|
+
async def place_limit_order(
|
|
113
|
+
self,
|
|
114
|
+
asset_id: int,
|
|
115
|
+
is_buy: bool,
|
|
116
|
+
price: float,
|
|
117
|
+
size: float,
|
|
118
|
+
address: str,
|
|
119
|
+
builder: BuilderInfo | None = None,
|
|
120
|
+
cloid: str | None = None,
|
|
121
|
+
):
|
|
122
|
+
"""Place a limit order (GTC)."""
|
|
123
|
+
order_actions = self._create_hypecore_order_actions(
|
|
124
|
+
asset_id,
|
|
125
|
+
is_buy,
|
|
126
|
+
price,
|
|
127
|
+
size,
|
|
128
|
+
False,
|
|
129
|
+
{"limit": {"tif": "Gtc"}},
|
|
130
|
+
builder,
|
|
131
|
+
cloid,
|
|
132
|
+
)
|
|
133
|
+
return await self.sign_and_broadcast_hypecore(order_actions, address)
|
|
134
|
+
|
|
135
|
+
async def place_trigger_order(
|
|
136
|
+
self,
|
|
137
|
+
asset_id: int,
|
|
138
|
+
is_buy: bool,
|
|
139
|
+
trigger_price: float,
|
|
140
|
+
size: float,
|
|
141
|
+
address: str,
|
|
142
|
+
tpsl: Literal["tp", "sl"],
|
|
143
|
+
is_market: bool = True,
|
|
144
|
+
limit_price: float | None = None,
|
|
145
|
+
builder: BuilderInfo | None = None,
|
|
146
|
+
):
|
|
147
|
+
"""Place a trigger order (TP or SL, market or limit, reduce only)."""
|
|
148
|
+
order_type = {
|
|
149
|
+
"trigger": {"triggerPx": trigger_price, "isMarket": is_market, "tpsl": tpsl}
|
|
150
|
+
}
|
|
151
|
+
price = trigger_price if is_market else (limit_price or trigger_price)
|
|
152
|
+
order_actions = self._create_hypecore_order_actions(
|
|
153
|
+
asset_id, is_buy, price, size, True, order_type, builder
|
|
154
|
+
)
|
|
155
|
+
return await self.sign_and_broadcast_hypecore(order_actions, address)
|
|
156
|
+
|
|
157
|
+
async def cancel_order(self, asset_id: int, order_id: str, address: str):
|
|
158
|
+
"""Cancel an open order."""
|
|
159
|
+
order_actions = {
|
|
160
|
+
"type": "cancel",
|
|
161
|
+
"cancels": [
|
|
162
|
+
{
|
|
163
|
+
"a": asset_id,
|
|
164
|
+
"o": order_id,
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
return await self.sign_and_broadcast_hypecore(order_actions, address)
|
|
169
|
+
|
|
170
|
+
async def update_leverage(
|
|
171
|
+
self, asset: int, leverage: int, is_cross: bool, address: str
|
|
172
|
+
):
|
|
173
|
+
"""Update leverage for an asset."""
|
|
174
|
+
order_actions = {
|
|
175
|
+
"type": "updateLeverage",
|
|
176
|
+
"asset": asset,
|
|
177
|
+
"isCross": is_cross,
|
|
178
|
+
"leverage": leverage,
|
|
179
|
+
}
|
|
180
|
+
return await self.sign_and_broadcast_hypecore(order_actions, address)
|
|
181
|
+
|
|
182
|
+
async def update_isolated_margin(self, asset: int, delta_usdc: float, address: str):
|
|
183
|
+
"""
|
|
184
|
+
Add/remove USDC margin on an existing ISOLATED position.
|
|
185
|
+
Works for both longs & shorts. Positive = add, negative = remove.
|
|
186
|
+
"""
|
|
187
|
+
ntli = int(round(delta_usdc * 1_000_000))
|
|
188
|
+
order_actions = {
|
|
189
|
+
"type": "updateIsolatedMargin",
|
|
190
|
+
"asset": asset,
|
|
191
|
+
"isBuy": delta_usdc >= 0,
|
|
192
|
+
"ntli": ntli,
|
|
193
|
+
}
|
|
194
|
+
return await self.sign_and_broadcast_hypecore(order_actions, address)
|
|
195
|
+
|
|
196
|
+
async def withdraw(self, amount: float, address: str):
|
|
197
|
+
"""Initiate a withdrawal request to Arbitrum."""
|
|
198
|
+
nonce = get_timestamp_ms()
|
|
199
|
+
action = {
|
|
200
|
+
"hyperliquidChain": MAINNET,
|
|
201
|
+
"signatureChainId": ARBITRUM_CHAIN_ID,
|
|
202
|
+
"destination": address,
|
|
203
|
+
"amount": str(amount),
|
|
204
|
+
"time": nonce,
|
|
205
|
+
"type": "withdraw3",
|
|
206
|
+
}
|
|
207
|
+
payload = self._get_hypecore_user_signature_payload(
|
|
208
|
+
"HyperliquidTransaction:Withdraw", WITHDRAW_SIGN_TYPES, action
|
|
209
|
+
)
|
|
210
|
+
if not (sig := await self.sign(payload, action, address)):
|
|
211
|
+
return USER_DECLINED_ERROR
|
|
212
|
+
return self._broadcast_hypecore(action, nonce, sig)
|
|
213
|
+
|
|
214
|
+
async def spot_transfer(
|
|
215
|
+
self,
|
|
216
|
+
signature_chain_id: int,
|
|
217
|
+
destination: str,
|
|
218
|
+
token: str,
|
|
219
|
+
amount: str,
|
|
220
|
+
address: str,
|
|
221
|
+
):
|
|
222
|
+
"""Transfer spot assets to HyperEVM or another address."""
|
|
223
|
+
nonce = get_timestamp_ms()
|
|
224
|
+
action = {
|
|
225
|
+
"type": "spotSend",
|
|
226
|
+
"hyperliquidChain": MAINNET,
|
|
227
|
+
"signatureChainId": hex(signature_chain_id),
|
|
228
|
+
"destination": destination,
|
|
229
|
+
"token": token,
|
|
230
|
+
"amount": amount,
|
|
231
|
+
"time": nonce,
|
|
232
|
+
}
|
|
233
|
+
payload = self._get_hypecore_user_signature_payload(
|
|
234
|
+
"HyperliquidTransaction:SpotSend", SPOT_TRANSFER_SIGN_TYPES, action
|
|
235
|
+
)
|
|
236
|
+
if not (sig := await self.sign(payload, action, address)):
|
|
237
|
+
return USER_DECLINED_ERROR
|
|
238
|
+
return self._broadcast_hypecore(action, nonce, sig)
|
|
239
|
+
|
|
240
|
+
async def usd_class_transfer(self, amount: float, address: str, to_perp: bool):
|
|
241
|
+
"""Transfer USDC between spot and perp accounts."""
|
|
242
|
+
nonce = get_timestamp_ms()
|
|
243
|
+
action = {
|
|
244
|
+
"hyperliquidChain": MAINNET,
|
|
245
|
+
"signatureChainId": ARBITRUM_CHAIN_ID,
|
|
246
|
+
"amount": str(amount),
|
|
247
|
+
"toPerp": to_perp,
|
|
248
|
+
"nonce": nonce,
|
|
249
|
+
"type": "usdClassTransfer",
|
|
250
|
+
}
|
|
251
|
+
payload = self._get_hypecore_user_signature_payload(
|
|
252
|
+
"HyperliquidTransaction:UsdClassTransfer",
|
|
253
|
+
USD_CLASS_TRANSFER_SIGN_TYPES,
|
|
254
|
+
action,
|
|
255
|
+
)
|
|
256
|
+
if not (sig := await self.sign(payload, action, address)):
|
|
257
|
+
return USER_DECLINED_ERROR
|
|
258
|
+
return self._broadcast_hypecore(action, nonce, sig)
|
|
259
|
+
|
|
260
|
+
async def set_dex_abstraction(self, address: str, enabled: bool):
|
|
261
|
+
"""Enable or disable DEX abstraction for an address."""
|
|
262
|
+
nonce = get_timestamp_ms()
|
|
263
|
+
action = {
|
|
264
|
+
"hyperliquidChain": MAINNET,
|
|
265
|
+
"signatureChainId": ARBITRUM_CHAIN_ID,
|
|
266
|
+
"user": address.lower(),
|
|
267
|
+
"enabled": enabled,
|
|
268
|
+
"nonce": nonce,
|
|
269
|
+
"type": "userDexAbstraction",
|
|
270
|
+
}
|
|
271
|
+
payload = self._get_hypecore_user_signature_payload(
|
|
272
|
+
"HyperliquidTransaction:UserDexAbstraction",
|
|
273
|
+
USER_DEX_ABSTRACTION_SIGN_TYPES,
|
|
274
|
+
action,
|
|
275
|
+
)
|
|
276
|
+
if not (sig := await self.sign(payload, action, address)):
|
|
277
|
+
return USER_DECLINED_ERROR
|
|
278
|
+
return self._broadcast_hypecore(action, nonce, sig)
|
|
279
|
+
|
|
280
|
+
async def approve_builder_fee(self, builder: str, max_fee_rate: str, address: str):
|
|
281
|
+
"""Approve a builder fee for trading."""
|
|
282
|
+
nonce = get_timestamp_ms()
|
|
283
|
+
action = {
|
|
284
|
+
"hyperliquidChain": MAINNET,
|
|
285
|
+
"signatureChainId": ARBITRUM_CHAIN_ID,
|
|
286
|
+
"maxFeeRate": max_fee_rate,
|
|
287
|
+
"builder": builder,
|
|
288
|
+
"nonce": nonce,
|
|
289
|
+
"type": "approveBuilderFee",
|
|
290
|
+
}
|
|
291
|
+
payload = self._get_hypecore_user_signature_payload(
|
|
292
|
+
"HyperliquidTransaction:ApproveBuilderFee", BUILDER_FEE_SIGN_TYPES, action
|
|
293
|
+
)
|
|
294
|
+
if not (sig := await self.sign(payload, action, address)):
|
|
295
|
+
return USER_DECLINED_ERROR
|
|
296
|
+
return self._broadcast_hypecore(action, nonce, sig)
|
|
297
|
+
|
|
298
|
+
def build_sign(self, raw_payload: dict) -> str:
|
|
299
|
+
"""Build and format the signing payload based on signing_type."""
|
|
300
|
+
if self.signing_type == "eip712":
|
|
301
|
+
# Remote signing: Return JSON string of typed data
|
|
302
|
+
return json.dumps(
|
|
303
|
+
raw_payload,
|
|
304
|
+
default=lambda o: (
|
|
305
|
+
"0x" + o.hex() if isinstance(o, (bytes, bytearray)) else o
|
|
306
|
+
),
|
|
307
|
+
separators=(",", ":"),
|
|
308
|
+
)
|
|
309
|
+
# Local signing: Return keccak hash
|
|
310
|
+
encoded = encode_typed_data(full_message=raw_payload)
|
|
311
|
+
full_msg = b"\x19" + encoded.version + encoded.header + encoded.body
|
|
312
|
+
return f"0x{keccak(full_msg).hex()}"
|
|
313
|
+
|
|
314
|
+
async def sign(
|
|
315
|
+
self, payload: str, action: dict, address: str
|
|
316
|
+
) -> dict[str, Any] | None:
|
|
317
|
+
"""Sign the payload and return Hyperliquid-format signature."""
|
|
318
|
+
if self.signing_type == "eip712":
|
|
319
|
+
# For EIP-712: parse JSON, call callback with typed data dict, convert hex to {r,s,v}
|
|
320
|
+
typed_data = json.loads(payload)
|
|
321
|
+
sig_hex = await self.sign_callback(typed_data)
|
|
322
|
+
if not sig_hex:
|
|
323
|
+
return None
|
|
324
|
+
return self.util._sig_hex_to_hl_signature(sig_hex)
|
|
325
|
+
|
|
326
|
+
# Local signing: payload is hash string, callback returns {r,s,v} directly
|
|
327
|
+
return await self.sign_callback(action, payload, address)
|
|
328
|
+
|
|
329
|
+
def _get_hypecore_l1_payload(
|
|
330
|
+
self, action, nonce, expires_after=None, is_mainnet=True
|
|
331
|
+
):
|
|
332
|
+
"""Build L1 action payload (orders, leverage, cancels)."""
|
|
333
|
+
payload = get_l1_action_payload(action, None, nonce, expires_after, is_mainnet)
|
|
334
|
+
return self.build_sign(payload)
|
|
335
|
+
|
|
336
|
+
def _get_hypecore_user_signature_payload(self, primary_type, payload_types, action):
|
|
337
|
+
"""Build user signed action payload (withdrawals, transfers, etc.)."""
|
|
338
|
+
payload = user_signed_payload(primary_type, payload_types, action)
|
|
339
|
+
return self.build_sign(payload)
|
|
340
|
+
|
|
341
|
+
def _broadcast_hypecore(self, action, nonce, signature):
|
|
342
|
+
"""Broadcast a signed action to the Hyperliquid exchange."""
|
|
343
|
+
payload = {
|
|
344
|
+
"action": action,
|
|
345
|
+
"nonce": nonce,
|
|
346
|
+
"signature": signature,
|
|
347
|
+
}
|
|
348
|
+
logger.info(f"Broadcasting Hypecore payload: {payload}")
|
|
349
|
+
return self.api.post("/exchange", payload)
|
|
350
|
+
|
|
351
|
+
async def sign_and_broadcast_hypecore(self, action, address):
|
|
352
|
+
"""Sign and broadcast an L1 action (orders, leverage, cancels)."""
|
|
353
|
+
nonce = get_timestamp_ms()
|
|
354
|
+
payload = self._get_hypecore_l1_payload(action, nonce)
|
|
355
|
+
if not (sig := await self.sign(payload, action, address)):
|
|
356
|
+
return USER_DECLINED_ERROR
|
|
357
|
+
return self._broadcast_hypecore(action, nonce, sig)
|
|
358
|
+
|
|
359
|
+
def _hypecore_get_user_transfers(
|
|
360
|
+
self,
|
|
361
|
+
user_address: str,
|
|
362
|
+
from_timestamp_ms: int,
|
|
363
|
+
type: Literal["deposit", "withdraw"],
|
|
364
|
+
) -> dict[str, Decimal]:
|
|
365
|
+
"""Get user deposits or withdrawals from a given timestamp."""
|
|
366
|
+
data = self.api.post(
|
|
367
|
+
"/info",
|
|
368
|
+
{
|
|
369
|
+
"type": "userNonFundingLedgerUpdates",
|
|
370
|
+
"user": Web3.to_checksum_address(user_address),
|
|
371
|
+
"startTime": int(from_timestamp_ms),
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
res = {}
|
|
375
|
+
for u in sorted(data, key=lambda x: x.get("time", 0)):
|
|
376
|
+
delta = u.get("delta")
|
|
377
|
+
if delta and delta.get("type") == type:
|
|
378
|
+
res[u["hash"]] = Decimal(str(delta["usdc"]))
|
|
379
|
+
return res
|
|
380
|
+
|
|
381
|
+
def hypecore_get_user_deposits(
|
|
382
|
+
self, user_address: str, from_timestamp_ms: int
|
|
383
|
+
) -> dict[str, Decimal]:
|
|
384
|
+
"""Get user deposits from a given timestamp."""
|
|
385
|
+
return self._hypecore_get_user_transfers(
|
|
386
|
+
user_address=user_address,
|
|
387
|
+
from_timestamp_ms=from_timestamp_ms,
|
|
388
|
+
type="deposit",
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def hypecore_get_user_withdrawals(
|
|
392
|
+
self, user_address: str, from_timestamp_ms: int
|
|
393
|
+
) -> dict[str, Decimal]:
|
|
394
|
+
"""Get user withdrawals from a given timestamp."""
|
|
395
|
+
return self._hypecore_get_user_transfers(
|
|
396
|
+
user_address=user_address,
|
|
397
|
+
from_timestamp_ms=from_timestamp_ms,
|
|
398
|
+
type="withdraw",
|
|
399
|
+
)
|
|
@@ -209,6 +209,49 @@ class LocalHyperliquidExecutor:
|
|
|
209
209
|
"response": {"type": "error", "data": str(exc)},
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
async def cancel_order_by_cloid(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
asset_id: int,
|
|
216
|
+
cloid: str,
|
|
217
|
+
address: str,
|
|
218
|
+
) -> dict[str, Any]:
|
|
219
|
+
if address.lower() != self._wallet.address.lower():
|
|
220
|
+
return {
|
|
221
|
+
"status": "err",
|
|
222
|
+
"response": {"type": "error", "data": "Address mismatch"},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
is_spot = asset_id >= 10000
|
|
227
|
+
if is_spot:
|
|
228
|
+
spot_index = asset_id - 10000
|
|
229
|
+
coin = f"@{spot_index}"
|
|
230
|
+
else:
|
|
231
|
+
coin = self._get_perp_coin(asset_id)
|
|
232
|
+
if not coin:
|
|
233
|
+
return {
|
|
234
|
+
"status": "err",
|
|
235
|
+
"response": {
|
|
236
|
+
"type": "error",
|
|
237
|
+
"data": f"Unknown asset_id: {asset_id}",
|
|
238
|
+
},
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
cloid_obj = Cloid(cloid) if isinstance(cloid, str) else cloid
|
|
242
|
+
result = self.exchange.cancel_by_cloid(name=coin, cloid=cloid_obj)
|
|
243
|
+
if asyncio.iscoroutine(result):
|
|
244
|
+
result = await result
|
|
245
|
+
logger.debug(f"Cancel by cloid result: {result}")
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
except Exception as exc:
|
|
249
|
+
logger.error(f"Cancel by cloid failed: {exc}")
|
|
250
|
+
return {
|
|
251
|
+
"status": "err",
|
|
252
|
+
"response": {"type": "error", "data": str(exc)},
|
|
253
|
+
}
|
|
254
|
+
|
|
212
255
|
async def update_leverage(
|
|
213
256
|
self,
|
|
214
257
|
*,
|
|
@@ -249,6 +292,37 @@ class LocalHyperliquidExecutor:
|
|
|
249
292
|
"response": {"type": "error", "data": str(exc)},
|
|
250
293
|
}
|
|
251
294
|
|
|
295
|
+
async def spot_transfer(
|
|
296
|
+
self,
|
|
297
|
+
*,
|
|
298
|
+
amount: float,
|
|
299
|
+
destination: str,
|
|
300
|
+
token: str,
|
|
301
|
+
address: str,
|
|
302
|
+
) -> dict[str, Any]:
|
|
303
|
+
if address.lower() != self._wallet.address.lower():
|
|
304
|
+
return {
|
|
305
|
+
"status": "err",
|
|
306
|
+
"response": {"type": "error", "data": "Address mismatch"},
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
result = self.exchange.spot_transfer(
|
|
311
|
+
amount=float(amount),
|
|
312
|
+
destination=str(destination),
|
|
313
|
+
token=str(token),
|
|
314
|
+
)
|
|
315
|
+
if asyncio.iscoroutine(result):
|
|
316
|
+
result = await result
|
|
317
|
+
logger.debug(f"Spot transfer result: {result}")
|
|
318
|
+
return result
|
|
319
|
+
except Exception as exc:
|
|
320
|
+
logger.error(f"Spot transfer failed: {exc}")
|
|
321
|
+
return {
|
|
322
|
+
"status": "err",
|
|
323
|
+
"response": {"type": "error", "data": str(exc)},
|
|
324
|
+
}
|
|
325
|
+
|
|
252
326
|
async def transfer_spot_to_perp(
|
|
253
327
|
self,
|
|
254
328
|
*,
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from eth_account import Account
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.core.types import HyperliquidSignCallback
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _resolve_private_key(config: dict[str, Any]) -> str | None:
|
|
9
|
+
"""Extract private key from config."""
|
|
10
|
+
# Try strategy_wallet first
|
|
11
|
+
strategy_wallet = config.get("strategy_wallet", {})
|
|
12
|
+
if isinstance(strategy_wallet, dict):
|
|
13
|
+
pk = strategy_wallet.get("private_key_hex") or strategy_wallet.get(
|
|
14
|
+
"private_key"
|
|
15
|
+
)
|
|
16
|
+
if pk:
|
|
17
|
+
return pk
|
|
18
|
+
|
|
19
|
+
# Try main_wallet as fallback (for single-wallet setups)
|
|
20
|
+
main_wallet = config.get("main_wallet", {})
|
|
21
|
+
if isinstance(main_wallet, dict):
|
|
22
|
+
pk = main_wallet.get("private_key_hex") or main_wallet.get("private_key")
|
|
23
|
+
if pk:
|
|
24
|
+
return pk
|
|
25
|
+
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_local_signer(config: dict[str, Any]) -> HyperliquidSignCallback:
|
|
30
|
+
"""
|
|
31
|
+
Create a Hyperliquid signing callback using private key from config.
|
|
32
|
+
|
|
33
|
+
For local signing, the payload is a keccak hash (0x...) of the encoded EIP-712 typed data.
|
|
34
|
+
The callback signs with the private key and returns a signature dict.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: Configuration dict containing private key in strategy_wallet or main_wallet
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
HyperliquidSignCallback that signs payloads with the local private key
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: If no private key found in config
|
|
44
|
+
"""
|
|
45
|
+
# Extract private key from config
|
|
46
|
+
private_key = _resolve_private_key(config)
|
|
47
|
+
if not private_key:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"No private key found in config. "
|
|
50
|
+
"Provide strategy_wallet.private_key_hex or strategy_wallet.private_key"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Create account
|
|
54
|
+
pk = private_key if private_key.startswith("0x") else "0x" + private_key
|
|
55
|
+
account = Account.from_key(pk)
|
|
56
|
+
|
|
57
|
+
async def sign(
|
|
58
|
+
action: dict[str, Any], payload: str, address: str
|
|
59
|
+
) -> dict[str, str] | None:
|
|
60
|
+
"""
|
|
61
|
+
Sign a Hyperliquid action with local private key.
|
|
62
|
+
|
|
63
|
+
For local signing, payload is keccak hash (0x...) of encoded typed data.
|
|
64
|
+
Sign with account and return signature dict.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
action: The action being signed (not used for local signing)
|
|
68
|
+
payload: Keccak hash (0x...) of encoded EIP-712 typed data
|
|
69
|
+
address: The address signing (validation check)
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Signature dict {"r": "0x...", "s": "0x...", "v": 28} or None if validation fails
|
|
73
|
+
"""
|
|
74
|
+
# Verify address matches account
|
|
75
|
+
if address.lower() != account.address.lower():
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Sign the hash
|
|
79
|
+
signed = account.signHash(payload)
|
|
80
|
+
return {"r": hex(signed.r), "s": hex(signed.s), "v": signed.v}
|
|
81
|
+
|
|
82
|
+
return sign
|
|
@@ -45,7 +45,7 @@ class TestHyperliquidAdapter:
|
|
|
45
45
|
"wayfinder_paths.adapters.hyperliquid_adapter.adapter.constants",
|
|
46
46
|
mock_constants,
|
|
47
47
|
):
|
|
48
|
-
adapter = HyperliquidAdapter(config={})
|
|
48
|
+
adapter = HyperliquidAdapter(config={}, simulation=True)
|
|
49
49
|
adapter.info = mock_info
|
|
50
50
|
return adapter
|
|
51
51
|
|