wayfinder-paths 0.1.24__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 (44) hide show
  1. wayfinder_paths/__init__.py +2 -0
  2. wayfinder_paths/adapters/brap_adapter/adapter.py +7 -47
  3. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +10 -31
  4. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +128 -60
  5. wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +399 -0
  6. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +74 -0
  7. wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +82 -0
  8. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +1 -1
  9. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
  10. wayfinder_paths/adapters/hyperliquid_adapter/util.py +237 -0
  11. wayfinder_paths/adapters/pendle_adapter/adapter.py +19 -55
  12. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +14 -46
  13. wayfinder_paths/core/__init__.py +2 -0
  14. wayfinder_paths/core/clients/BalanceClient.py +72 -0
  15. wayfinder_paths/core/clients/TokenClient.py +1 -1
  16. wayfinder_paths/core/clients/__init__.py +2 -0
  17. wayfinder_paths/core/strategies/Strategy.py +3 -3
  18. wayfinder_paths/core/types.py +19 -0
  19. wayfinder_paths/core/utils/tokens.py +19 -1
  20. wayfinder_paths/core/utils/transaction.py +9 -7
  21. wayfinder_paths/mcp/tools/balances.py +122 -214
  22. wayfinder_paths/mcp/tools/execute.py +63 -41
  23. wayfinder_paths/mcp/tools/quotes.py +16 -5
  24. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +6 -22
  25. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +227 -33
  26. wayfinder_paths/strategies/boros_hype_strategy/constants.py +17 -1
  27. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +44 -1
  28. wayfinder_paths/strategies/boros_hype_strategy/planner.py +87 -32
  29. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +50 -28
  30. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +71 -50
  31. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +3 -1
  32. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +0 -2
  33. wayfinder_paths/strategies/boros_hype_strategy/types.py +4 -1
  34. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +0 -2
  35. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -2
  36. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +0 -2
  37. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +0 -2
  38. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -2
  39. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -2
  40. wayfinder_paths/tests/test_mcp_quote_swap.py +3 -3
  41. {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/METADATA +2 -3
  42. {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/RECORD +44 -39
  43. {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/WHEEL +1 -1
  44. {wayfinder_paths-0.1.24.dist-info → wayfinder_paths-0.1.27.dist-info}/LICENSE +0 -0
@@ -0,0 +1,237 @@
1
+ import asyncio
2
+ import time
3
+ from decimal import ROUND_DOWN, Decimal
4
+ from typing import Any
5
+
6
+ from hyperliquid.info import Info
7
+ from loguru import logger
8
+
9
+
10
+ def _get_sigfigs(price):
11
+ num_str = str(price).strip().lower()
12
+ if "e" in num_str:
13
+ mantissa = num_str.split("e")[0]
14
+ mantissa = mantissa.replace(".", "")
15
+ mantissa = mantissa.strip("0")
16
+ return len(mantissa)
17
+
18
+ if "." in num_str:
19
+ int_part, dec_part = num_str.split(".")
20
+ int_part = int_part.lstrip("0")
21
+ num_str = int_part + dec_part
22
+ if dec_part:
23
+ num_str = num_str.rstrip("0")
24
+ else:
25
+ num_str = num_str.rstrip("0")
26
+ return len(num_str)
27
+
28
+
29
+ class Util:
30
+ def __init__(self, info: Info):
31
+ self.info: Info = info
32
+
33
+ def get_hypecore_spot_assets(self):
34
+ response = {}
35
+ spot_meta_attr = self.info.spot_meta
36
+ spot_meta = spot_meta_attr() if callable(spot_meta_attr) else spot_meta_attr
37
+ for i in spot_meta["universe"]:
38
+ base, quote = i["tokens"]
39
+ base_info = spot_meta["tokens"][base]
40
+ quote_info = spot_meta["tokens"][quote]
41
+ name = f"{base_info['name']}/{quote_info['name']}"
42
+ response[name] = i["index"] + 10000
43
+ return response
44
+
45
+ def get_hypecore_perpetual_assets(self):
46
+ response = {}
47
+ for k, v in self.info.coin_to_asset.items():
48
+ # First 10_000 are default perp ids
49
+ # Anything over 100_000 are HIP3 perp ids
50
+ if 0 <= v < 10000 or 100000 <= v:
51
+ response[k] = v
52
+
53
+ return response
54
+
55
+ def get_hypecore_asset_id(self, asset_name, is_perp):
56
+ assets = (
57
+ self.get_hypecore_spot_assets()
58
+ if not is_perp
59
+ else self.get_hypecore_perpetual_assets()
60
+ )
61
+ return assets.get(asset_name)
62
+
63
+ async def get_hypecore_all_dex_mid_prices(self):
64
+ return await self.info.all_dex_mid_prices()
65
+
66
+ async def get_hypecore_all_dex_meta_universe(self):
67
+ return await self.info.all_dex_meta_universe()
68
+
69
+ def get_size_decimals_for_hypecore_asset(self, asset_id: int):
70
+ return self.info.asset_to_sz_decimals[asset_id]
71
+
72
+ def get_price_decimals_for_hypecore_asset(self, asset_id: int):
73
+ is_spot = asset_id >= 10_000
74
+ decimals = (
75
+ 6 if not is_spot else 8
76
+ ) - self.get_size_decimals_for_hypecore_asset(asset_id)
77
+ return decimals
78
+
79
+ def get_valid_hypecore_order_size(self, asset_id: int, size: float):
80
+ decimals = self.get_size_decimals_for_hypecore_asset(asset_id)
81
+ step = Decimal(10) ** -decimals
82
+ value = Decimal(str(size)).quantize(step, rounding=ROUND_DOWN)
83
+ return float(value)
84
+
85
+ def get_valid_hypecore_price_size(self, asset_id: int, price: float):
86
+ decimals = self.get_price_decimals_for_hypecore_asset(asset_id)
87
+ actual_decimals = max(str(price)[::-1].find("."), 0)
88
+
89
+ sigfigs = _get_sigfigs(price)
90
+ if sigfigs > 5 and actual_decimals:
91
+ price = float(f"{price:.5g}")
92
+
93
+ if actual_decimals > decimals:
94
+ price = max(10**-decimals, round(price, decimals))
95
+ return price
96
+
97
+ def _reformat_perp_user_state(self, perp_user_state: dict) -> dict:
98
+ asset_positions = perp_user_state.get("assetPositions", [])
99
+ for pos in asset_positions:
100
+ position = pos.get("position", {})
101
+ # Fix funding direction: negative = earned, positive = paid
102
+ if "cumFunding" in position:
103
+ old_funding = position.pop("cumFunding")
104
+ position["cumFundingEarned"] = {
105
+ k: str(float(v)) for k, v in old_funding.items()
106
+ }
107
+ return perp_user_state
108
+
109
+ async def get_hypecore_user(self, address):
110
+ perp_user_state, spot_user_state, open_orders = await asyncio.gather(
111
+ self.info.all_dex_user_state(address),
112
+ self.info.spot_user_state(address),
113
+ self.info.all_dex_open_orders(address),
114
+ )
115
+ formatted_perp_state = self._reformat_perp_user_state(perp_user_state)
116
+ state = {
117
+ "perp_user_state": formatted_perp_state,
118
+ "spot_user_state": spot_user_state,
119
+ "open_orders": open_orders,
120
+ }
121
+ logger.info(state)
122
+ return state
123
+
124
+ def get_perp_margin_amount(self, state):
125
+ return float(state["perp_user_state"]["marginSummary"]["accountValue"])
126
+
127
+ def get_spot_usdc_amount(self, state):
128
+ for i in state["spot_user_state"]["balances"]:
129
+ if i["coin"] == "USDC":
130
+ return float(i["total"])
131
+ return 0.0
132
+
133
+ def get_margin_utilization(self, state):
134
+ account_value = float(state["perp_user_state"]["marginSummary"]["accountValue"])
135
+ total_margin_used = float(
136
+ state["perp_user_state"]["marginSummary"]["totalMarginUsed"]
137
+ )
138
+ return total_margin_used / account_value if account_value > 0 else 0.0
139
+
140
+ async def get_spot_account_value(
141
+ self,
142
+ state,
143
+ ignore_dust=False,
144
+ dust_threshold: float = 1.0,
145
+ mid_prices: dict[str, Any] | None = None,
146
+ ):
147
+ if mid_prices is None:
148
+ mid_prices = await self.get_hypecore_all_dex_mid_prices()
149
+
150
+ total_spot = 0.0
151
+ for i in state["spot_user_state"]["balances"]:
152
+ asset_name = i["coin"]
153
+ mid_price = 0.0
154
+ if asset_name == "USDC":
155
+ mid_price = 1.0
156
+ else:
157
+ asset_id = self.get_hypecore_asset_id(
158
+ f"{asset_name}/USDC", is_perp=False
159
+ )
160
+ mid_price_id = self.info.asset_to_coin[asset_id]
161
+ raw_price = mid_prices.get(mid_price_id)
162
+ mid_price = float(raw_price) if raw_price is not None else 0.0
163
+ value = mid_price * float(i["total"])
164
+ if ignore_dust and value < dust_threshold:
165
+ continue
166
+ total_spot += value
167
+ return total_spot
168
+
169
+ async def fetch_hypecore_user_fills(self, wallet: str):
170
+ """Fetch all available trade fills (up to the most-recent 10,000) from HypeCore."""
171
+ start, end = 0, int(time.time() * 1000)
172
+ out = []
173
+ while True:
174
+ try:
175
+ batch = await self.info.user_fills_by_time(wallet, start, end, False)
176
+ except Exception as e:
177
+ logger.error(f"Failed to fetch fills via node/public/SDK: {e}")
178
+ break
179
+ if not batch or len(batch) == 0:
180
+ break
181
+ out.extend(batch)
182
+ start = batch[-1]["time"] + 1
183
+ if len(batch) < 2000: # each page ≤ 2000 rows
184
+ break
185
+ return out
186
+
187
+ async def get_hypecore_position(self, address, asset_name):
188
+ perp_user_state = await self.info.all_dex_user_state(address)
189
+ formatted_perp_user_state = self._reformat_perp_user_state(perp_user_state)
190
+
191
+ for pos in formatted_perp_user_state.get("assetPositions", []):
192
+ if pos["position"]["coin"] == asset_name:
193
+ return pos
194
+
195
+ return None
196
+
197
+ @classmethod
198
+ def parse_dollar_value(cls, response: dict) -> Decimal | None:
199
+ if response.get("status", "") != "ok" or not len(
200
+ resp := response.get("response", {})
201
+ ):
202
+ return None
203
+
204
+ if resp.get("type", "") != "order" or not len(data := resp.get("data", {})):
205
+ return None
206
+
207
+ statuses = data.get("statuses", [])
208
+
209
+ if not len(statuses):
210
+ return None
211
+
212
+ return sum(
213
+ [
214
+ Decimal(status["filled"]["totalSz"])
215
+ * Decimal(status["filled"]["avgPx"])
216
+ for status in statuses
217
+ if "filled" in status
218
+ ]
219
+ )
220
+
221
+ @staticmethod
222
+ def _sig_hex_to_hl_signature(sig_hex: str) -> dict[str, Any]:
223
+ """Convert a 65-byte hex signature into Hyperliquid {r,s,v}."""
224
+ if not isinstance(sig_hex, str) or not sig_hex.startswith("0x"):
225
+ raise ValueError("Expected hex signature string starting with 0x")
226
+ raw = bytes.fromhex(sig_hex[2:])
227
+ if len(raw) != 65:
228
+ raise ValueError(f"Expected 65-byte signature, got {len(raw)} bytes")
229
+
230
+ r = raw[0:32]
231
+ s = raw[32:64]
232
+ v = raw[64]
233
+ # Normalize v to 27/28 when needed.
234
+ if v < 27:
235
+ v += 27
236
+
237
+ return {"r": f"0x{r.hex()}", "s": f"0x{s.hex()}", "v": int(v)}
@@ -10,11 +10,9 @@ from eth_utils import to_checksum_address
10
10
 
11
11
  from wayfinder_paths.adapters.multicall_adapter.adapter import MulticallAdapter
12
12
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
13
- from wayfinder_paths.core.constants.contracts import TOKENS_REQUIRING_APPROVAL_RESET
14
13
  from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
15
14
  from wayfinder_paths.core.utils.tokens import (
16
- build_approve_transaction,
17
- get_token_allowance,
15
+ ensure_allowance,
18
16
  get_token_balance,
19
17
  )
20
18
  from wayfinder_paths.core.utils.transaction import send_transaction
@@ -240,56 +238,6 @@ class PendleAdapter(BaseAdapter):
240
238
  txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
241
239
  return True, txn_hash
242
240
 
243
- async def _ensure_allowance(
244
- self,
245
- *,
246
- chain_id: int,
247
- token_address: str,
248
- owner: str,
249
- spender: str,
250
- amount: int,
251
- ) -> tuple[bool, Any]:
252
- token_checksum = to_checksum_address(token_address)
253
- owner_checksum = to_checksum_address(owner)
254
- spender_checksum = to_checksum_address(spender)
255
-
256
- allowance = await get_token_allowance(
257
- token_checksum,
258
- chain_id,
259
- owner_checksum,
260
- spender_checksum,
261
- )
262
- if allowance >= amount:
263
- return True, {"status": "already_approved"}
264
-
265
- if (int(chain_id), token_checksum) in TOKENS_REQUIRING_APPROVAL_RESET:
266
- # Some tokens (e.g., USDT) require allowance to be set to 0 before
267
- # being increased.
268
- if int(allowance) > 0:
269
- clear_tx = await build_approve_transaction(
270
- from_address=owner_checksum,
271
- chain_id=chain_id,
272
- token_address=token_checksum,
273
- spender_address=spender_checksum,
274
- amount=0,
275
- )
276
- try:
277
- await self._send_tx(clear_tx)
278
- except Exception as exc: # noqa: BLE001
279
- return False, {"error": str(exc), "token": token_address}
280
-
281
- approve_tx = await build_approve_transaction(
282
- from_address=owner_checksum,
283
- chain_id=chain_id,
284
- token_address=token_checksum,
285
- spender_address=spender_checksum,
286
- amount=self.MAX_UINT256,
287
- )
288
- try:
289
- return await self._send_tx(approve_tx)
290
- except Exception as exc:
291
- return False, {"error": str(exc), "token": token_address}
292
-
293
241
  # ---------------------------
294
242
  # Multicall helpers
295
243
  # ---------------------------
@@ -1783,12 +1731,21 @@ class PendleAdapter(BaseAdapter):
1783
1731
  amount = approval.get("amount")
1784
1732
  if not token or not amount:
1785
1733
  continue
1786
- approved, result = await self._ensure_allowance(
1734
+ if not self.strategy_wallet_signing_callback:
1735
+ return False, {
1736
+ "error": "strategy_wallet_signing_callback is required",
1737
+ "stage": "approval",
1738
+ "details": {
1739
+ "error": "strategy_wallet_signing_callback is required"
1740
+ },
1741
+ }
1742
+ approved, result = await ensure_allowance(
1787
1743
  chain_id=chain_id,
1788
1744
  token_address=token,
1789
1745
  owner=sender,
1790
1746
  spender=spender,
1791
1747
  amount=int(amount),
1748
+ signing_callback=self.strategy_wallet_signing_callback,
1792
1749
  )
1793
1750
  if not approved:
1794
1751
  return False, {
@@ -1910,13 +1867,20 @@ class PendleAdapter(BaseAdapter):
1910
1867
  amount = approval.get("amount")
1911
1868
  if not (isinstance(token, str) and token and amount is not None):
1912
1869
  continue
1870
+ if not self.strategy_wallet_signing_callback:
1871
+ return False, {
1872
+ "stage": "approval",
1873
+ "error": "strategy_wallet_signing_callback is required",
1874
+ "token": token,
1875
+ }
1913
1876
  try:
1914
- approved, result = await self._ensure_allowance(
1877
+ approved, result = await ensure_allowance(
1915
1878
  chain_id=chain_id,
1916
1879
  token_address=token,
1917
1880
  owner=sender,
1918
1881
  spender=spender,
1919
1882
  amount=int(str(amount)),
1883
+ signing_callback=self.strategy_wallet_signing_callback,
1920
1884
  )
1921
1885
  except Exception as exc: # noqa: BLE001
1922
1886
  return False, {
@@ -310,11 +310,10 @@ class TestPendleAdapter:
310
310
  new_callable=AsyncMock,
311
311
  return_value=10_000,
312
312
  ),
313
- patch.object(
314
- adapter,
315
- "_ensure_allowance",
313
+ patch(
314
+ "wayfinder_paths.adapters.pendle_adapter.adapter.ensure_allowance",
316
315
  new_callable=AsyncMock,
317
- return_value=(True, {"status": "already_approved"}),
316
+ return_value=(True, "0xapprovehash"),
318
317
  ),
319
318
  patch.object(
320
319
  adapter,
@@ -499,9 +498,9 @@ class TestPendleAdapter:
499
498
  # Mock allowance and approval
500
499
  with (
501
500
  patch(
502
- "wayfinder_paths.adapters.pendle_adapter.adapter.get_token_allowance",
501
+ "wayfinder_paths.adapters.pendle_adapter.adapter.ensure_allowance",
503
502
  new_callable=AsyncMock,
504
- return_value=2**256 - 1, # Already approved
503
+ return_value=(True, "0xapprovehash"),
505
504
  ),
506
505
  patch(
507
506
  "wayfinder_paths.adapters.pendle_adapter.adapter.send_transaction",
@@ -569,24 +568,9 @@ class TestPendleAdapter:
569
568
 
570
569
  with (
571
570
  patch(
572
- "wayfinder_paths.adapters.pendle_adapter.adapter.get_token_allowance",
573
- new_callable=AsyncMock,
574
- return_value=0, # No allowance
575
- ),
576
- patch(
577
- "wayfinder_paths.adapters.pendle_adapter.adapter.build_approve_transaction",
578
- new_callable=AsyncMock,
579
- return_value={
580
- "to": token_in_addr,
581
- "data": "0x",
582
- "chainId": 42161,
583
- "from": "0x" + "a" * 40,
584
- },
585
- ),
586
- patch(
587
- "wayfinder_paths.adapters.pendle_adapter.adapter.send_transaction",
571
+ "wayfinder_paths.adapters.pendle_adapter.adapter.ensure_allowance",
588
572
  new_callable=AsyncMock,
589
- side_effect=Exception("Approval tx failed"),
573
+ return_value=(False, {"error": "Approval tx failed"}),
590
574
  ),
591
575
  ):
592
576
  success, result = await adapter.execute_swap(
@@ -620,29 +604,13 @@ class TestPendleAdapter:
620
604
  }
621
605
  )
622
606
 
623
- with (
624
- patch(
625
- "wayfinder_paths.adapters.pendle_adapter.adapter.get_token_allowance",
626
- new_callable=AsyncMock,
627
- return_value=0, # No allowance, will try to approve
628
- ),
629
- patch(
630
- "wayfinder_paths.adapters.pendle_adapter.adapter.build_approve_transaction",
631
- new_callable=AsyncMock,
632
- return_value={
633
- "to": token_in_addr,
634
- "data": "0xapprove",
635
- "chainId": 42161,
636
- },
637
- ),
638
- ):
639
- success, result = await adapter.execute_swap(
640
- chain="arbitrum",
641
- market_address="0x" + "d" * 40,
642
- token_in=token_in_addr,
643
- token_out="0x" + "e" * 40,
644
- amount_in="1000000",
645
- )
607
+ success, result = await adapter.execute_swap(
608
+ chain="arbitrum",
609
+ market_address="0x" + "d" * 40,
610
+ token_in=token_in_addr,
611
+ token_out="0x" + "e" * 40,
612
+ amount_in="1000000",
613
+ )
646
614
 
647
615
  assert success is False
648
616
  assert result["stage"] == "approval"
@@ -1,5 +1,6 @@
1
1
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
2
2
  from wayfinder_paths.core.strategies.Strategy import (
3
+ LiquidationResult,
3
4
  StatusDict,
4
5
  StatusTuple,
5
6
  Strategy,
@@ -7,6 +8,7 @@ from wayfinder_paths.core.strategies.Strategy import (
7
8
 
8
9
  __all__ = [
9
10
  "Strategy",
11
+ "LiquidationResult",
10
12
  "StatusDict",
11
13
  "StatusTuple",
12
14
  "BaseAdapter",
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
4
+ from wayfinder_paths.core.config import get_api_base_url
5
+
6
+
7
+ class BalanceClient(WayfinderClient):
8
+ def __init__(self):
9
+ super().__init__()
10
+ self.api_base_url = get_api_base_url()
11
+
12
+ async def get_enriched_wallet_balances(
13
+ self,
14
+ *,
15
+ wallet_address: str,
16
+ exclude_spam_tokens: bool = True,
17
+ ) -> dict:
18
+ url = f"{self.api_base_url}/blockchain/balances/enriched/"
19
+ params = {
20
+ "address": wallet_address,
21
+ "exclude_spam_tokens": str(exclude_spam_tokens).lower(),
22
+ }
23
+ response = await self._request("GET", url, params=params)
24
+ return response.json()
25
+
26
+ async def get_wallet_activity(
27
+ self,
28
+ *,
29
+ wallet_address: str,
30
+ limit: int = 20,
31
+ offset: str | None = None,
32
+ ) -> dict:
33
+ url = f"{self.api_base_url}/blockchain/balances/activity/"
34
+ params: dict[str, str | int] = {"address": wallet_address, "limit": limit}
35
+ if offset:
36
+ params["offset"] = offset
37
+ response = await self._request("GET", url, params=params)
38
+ return response.json()
39
+
40
+ async def get_token_balance(
41
+ self,
42
+ *,
43
+ wallet_address: str,
44
+ token_id: str,
45
+ human_readable: bool = True,
46
+ ) -> dict:
47
+ url = f"{self.api_base_url}/public/balances/token/"
48
+ params = {
49
+ "wallet_address": wallet_address,
50
+ "token_id": token_id,
51
+ "human_readable": str(human_readable).lower(),
52
+ }
53
+ response = await self._request("GET", url, params=params)
54
+ return response.json()
55
+
56
+ async def get_pool_balance(
57
+ self,
58
+ *,
59
+ pool_address: str,
60
+ chain_id: int,
61
+ user_address: str,
62
+ human_readable: bool = True,
63
+ ) -> dict:
64
+ url = f"{self.api_base_url}/public/balances/pool/"
65
+ params = {
66
+ "pool_address": pool_address,
67
+ "chain_id": chain_id,
68
+ "user_address": user_address,
69
+ "human_readable": str(human_readable).lower(),
70
+ }
71
+ response = await self._request("GET", url, params=params)
72
+ return response.json()
@@ -90,7 +90,7 @@ class FuzzyTokenResult(TypedDict):
90
90
  class TokenClient(WayfinderClient):
91
91
  def __init__(self):
92
92
  super().__init__()
93
- self.api_base_url = f"{get_api_base_url()}/blockchain/tokens"
93
+ self.api_base_url = f"{get_api_base_url()}/v1/blockchain/tokens"
94
94
 
95
95
  async def get_token_details(
96
96
  self, query: str, market_data: bool = False, chain_id: int | None = None
@@ -1,3 +1,4 @@
1
+ from wayfinder_paths.core.clients.BalanceClient import BalanceClient
1
2
  from wayfinder_paths.core.clients.BRAPClient import BRAPClient
2
3
  from wayfinder_paths.core.clients.ClientManager import ClientManager
3
4
  from wayfinder_paths.core.clients.HyperlendClient import HyperlendClient
@@ -16,6 +17,7 @@ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
16
17
  __all__ = [
17
18
  "WayfinderClient",
18
19
  "ClientManager",
20
+ "BalanceClient",
19
21
  "TokenClient",
20
22
  "LedgerClient",
21
23
  "PoolClient",
@@ -8,6 +8,7 @@ from loguru import logger
8
8
 
9
9
  from wayfinder_paths.core.clients.TokenClient import TokenDetails
10
10
  from wayfinder_paths.core.strategies.descriptors import StratDescriptor
11
+ from wayfinder_paths.core.types import HyperliquidSignCallback
11
12
 
12
13
 
13
14
  class StatusDict(TypedDict):
@@ -48,18 +49,17 @@ class Strategy(ABC):
48
49
  self,
49
50
  config: StrategyConfig | dict[str, Any] | None = None,
50
51
  *,
51
- main_wallet: WalletConfig | dict[str, Any] | None = None,
52
- strategy_wallet: WalletConfig | dict[str, Any] | None = None,
53
- api_key: str | None = None,
54
52
  main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
55
53
  strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
56
54
  | None = None,
55
+ strategy_sign_typed_data: HyperliquidSignCallback | None = None,
57
56
  ):
58
57
  self.ledger_adapter = None
59
58
  self.logger = logger.bind(strategy=self.__class__.__name__)
60
59
  self.config = config
61
60
  self.main_wallet_signing_callback = main_wallet_signing_callback
62
61
  self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
62
+ self.strategy_sign_typed_data = strategy_sign_typed_data
63
63
 
64
64
  async def setup(self) -> None:
65
65
  pass
@@ -0,0 +1,19 @@
1
+ from collections.abc import Awaitable, Callable
2
+ from typing import Any
3
+
4
+ # EVM transaction signing callback
5
+ # Used for signing EVM-compatible transactions (mainnet, Base, Arbitrum, etc.)
6
+ # Parameters: transaction dict with to/from/data/value/etc.
7
+ # Returns: signed transaction hex string
8
+ TransactionSigningCallback = Callable[[dict], Awaitable[str]]
9
+
10
+ # Hyperliquid signing callback
11
+ # Used for signing Hyperliquid actions (orders, transfers, withdrawals, etc.)
12
+ # Parameters:
13
+ # - action: dict - The action being signed (order, transfer, etc.)
14
+ # - payload: str - Either JSON string (EIP-712) or keccak hash (local) of typed data
15
+ # - address: str - The address signing the transaction
16
+ # Returns: signature dict {"r": "0x...", "s": "0x...", "v": 28} or None if declined
17
+ HyperliquidSignCallback = Callable[
18
+ [dict[str, Any], str, str], Awaitable[dict[str, str] | None]
19
+ ]
@@ -1,8 +1,10 @@
1
1
  from collections.abc import Callable
2
2
  from typing import Any
3
3
 
4
+ from eth_utils import to_checksum_address
4
5
  from web3 import AsyncWeb3
5
6
 
7
+ from wayfinder_paths.core.constants.contracts import TOKENS_REQUIRING_APPROVAL_RESET
6
8
  from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
7
9
  from wayfinder_paths.core.utils.transaction import send_transaction
8
10
  from wayfinder_paths.core.utils.web3 import web3_from_chain_id
@@ -27,7 +29,9 @@ async def get_token_balance(
27
29
  checksum_wallet = AsyncWeb3.to_checksum_address(wallet_address)
28
30
 
29
31
  if is_native_token(token_address):
30
- balance = await web3.eth.get_balance(checksum_wallet)
32
+ balance = await web3.eth.get_balance(
33
+ checksum_wallet, block_identifier="pending"
34
+ )
31
35
  return int(balance)
32
36
  else:
33
37
  checksum_token = AsyncWeb3.to_checksum_address(token_address)
@@ -121,6 +125,20 @@ async def ensure_allowance(
121
125
  allowance = await get_token_allowance(token_address, chain_id, owner, spender)
122
126
  if allowance >= amount:
123
127
  return True, {}
128
+
129
+ if (
130
+ int(chain_id),
131
+ to_checksum_address(token_address),
132
+ ) in TOKENS_REQUIRING_APPROVAL_RESET:
133
+ clear_transaction = await build_approve_transaction(
134
+ from_address=owner,
135
+ chain_id=chain_id,
136
+ token_address=token_address,
137
+ spender_address=spender,
138
+ amount=0,
139
+ )
140
+ await send_transaction(clear_transaction, signing_callback)
141
+
124
142
  approve_tx = await build_approve_transaction(
125
143
  from_address=owner,
126
144
  chain_id=chain_id,
@@ -33,7 +33,9 @@ async def nonce_transaction(transaction: dict):
33
33
  from_address = _get_transaction_from_address(transaction)
34
34
 
35
35
  async def _get_nonce(web3: AsyncWeb3, from_address: str) -> int:
36
- return await web3.eth.get_transaction_count(from_address)
36
+ return await web3.eth.get_transaction_count(
37
+ from_address, block_identifier="pending"
38
+ )
37
39
 
38
40
  async with web3s_from_chain_id(get_transaction_chain_id(transaction)) as web3s:
39
41
  nonces = await asyncio.gather(
@@ -218,13 +220,13 @@ async def encode_call(
218
220
  value: int = 0,
219
221
  ) -> dict[str, Any]:
220
222
  async with web3_from_chain_id(chain_id) as web3:
221
- contract = web3.eth.contract(address=target, abi=abi)
222
223
  try:
223
- tx_data = await getattr(contract.functions, fn_name)(
224
- *args
225
- ).build_transaction({"from": from_address})
226
- data = tx_data["data"]
227
- except ValueError as exc:
224
+ contract = web3.eth.contract(
225
+ address=web3.to_checksum_address(target),
226
+ abi=abi,
227
+ )
228
+ data = contract.encode_abi(fn_name, args)
229
+ except (ValueError, TypeError) as exc:
228
230
  raise ValueError(f"Failed to encode {fn_name}: {exc}") from exc
229
231
 
230
232
  return {