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,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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
501
|
+
"wayfinder_paths.adapters.pendle_adapter.adapter.ensure_allowance",
|
|
503
502
|
new_callable=AsyncMock,
|
|
504
|
-
return_value=
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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"
|
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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 {
|