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.

Files changed (42) hide show
  1. wayfinder_paths/adapters/brap_adapter/adapter.py +7 -47
  2. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +10 -31
  3. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +128 -60
  4. wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +399 -0
  5. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +74 -0
  6. wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +82 -0
  7. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +1 -1
  8. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
  9. wayfinder_paths/adapters/hyperliquid_adapter/util.py +237 -0
  10. wayfinder_paths/adapters/pendle_adapter/adapter.py +19 -55
  11. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +14 -46
  12. wayfinder_paths/core/clients/BalanceClient.py +72 -0
  13. wayfinder_paths/core/clients/TokenClient.py +1 -1
  14. wayfinder_paths/core/clients/__init__.py +2 -0
  15. wayfinder_paths/core/strategies/Strategy.py +3 -3
  16. wayfinder_paths/core/types.py +19 -0
  17. wayfinder_paths/core/utils/tokens.py +19 -1
  18. wayfinder_paths/core/utils/transaction.py +9 -7
  19. wayfinder_paths/mcp/tools/balances.py +122 -214
  20. wayfinder_paths/mcp/tools/execute.py +63 -41
  21. wayfinder_paths/mcp/tools/quotes.py +16 -5
  22. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +6 -22
  23. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +227 -33
  24. wayfinder_paths/strategies/boros_hype_strategy/constants.py +17 -1
  25. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +44 -1
  26. wayfinder_paths/strategies/boros_hype_strategy/planner.py +87 -32
  27. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +50 -28
  28. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +71 -50
  29. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +3 -1
  30. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +0 -2
  31. wayfinder_paths/strategies/boros_hype_strategy/types.py +4 -1
  32. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +0 -2
  33. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -2
  34. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +0 -2
  35. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +0 -2
  36. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -2
  37. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -2
  38. wayfinder_paths/tests/test_mcp_quote_swap.py +3 -3
  39. {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.27.dist-info}/METADATA +1 -1
  40. {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.27.dist-info}/RECORD +42 -37
  41. {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.27.dist-info}/LICENSE +0 -0
  42. {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
 
@@ -5,7 +5,7 @@ from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdap
5
5
 
6
6
  @pytest.fixture
7
7
  def live_adapter():
8
- return HyperliquidAdapter(config={})
8
+ return HyperliquidAdapter(config={}, simulation=True)
9
9
 
10
10
 
11
11
  class TestSpotAssetIDs: