wayfinder-paths 0.1.25__py3-none-any.whl → 0.1.28__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 +367 -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.28.dist-info}/METADATA +3 -2
- {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.28.dist-info}/RECORD +42 -37
- {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.28.dist-info}/WHEEL +1 -1
- {wayfinder_paths-0.1.25.dist-info → wayfinder_paths-0.1.28.dist-info}/LICENSE +0 -0
|
@@ -3,93 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from typing import Any, Literal
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
from wayfinder_paths.core.config import get_api_base_url, get_api_key
|
|
9
|
-
from wayfinder_paths.core.constants.base import DEFAULT_HTTP_TIMEOUT
|
|
6
|
+
from wayfinder_paths.core.clients import BalanceClient
|
|
10
7
|
from wayfinder_paths.mcp.utils import err, find_wallet_by_label, normalize_address, ok
|
|
11
8
|
|
|
12
9
|
|
|
13
|
-
class _BalanceClient:
|
|
14
|
-
def __init__(self):
|
|
15
|
-
self.api_base_url = get_api_base_url()
|
|
16
|
-
timeout = httpx.Timeout(DEFAULT_HTTP_TIMEOUT)
|
|
17
|
-
self.client = httpx.AsyncClient(timeout=timeout)
|
|
18
|
-
self._headers = {"Content-Type": "application/json"}
|
|
19
|
-
api_key = get_api_key()
|
|
20
|
-
if api_key:
|
|
21
|
-
self._headers["X-API-KEY"] = api_key
|
|
22
|
-
|
|
23
|
-
async def close(self):
|
|
24
|
-
await self.client.aclose()
|
|
25
|
-
|
|
26
|
-
async def get_enriched_wallet_balances(
|
|
27
|
-
self,
|
|
28
|
-
*,
|
|
29
|
-
wallet_address: str,
|
|
30
|
-
exclude_spam_tokens: bool = True,
|
|
31
|
-
) -> dict:
|
|
32
|
-
url = f"{self.api_base_url}/blockchain/balances/enriched/"
|
|
33
|
-
params = {
|
|
34
|
-
"address": wallet_address,
|
|
35
|
-
"exclude_spam_tokens": str(exclude_spam_tokens).lower(),
|
|
36
|
-
}
|
|
37
|
-
response = await self.client.get(url, params=params, headers=self._headers)
|
|
38
|
-
response.raise_for_status()
|
|
39
|
-
return response.json()
|
|
40
|
-
|
|
41
|
-
async def get_wallet_activity(
|
|
42
|
-
self,
|
|
43
|
-
*,
|
|
44
|
-
wallet_address: str,
|
|
45
|
-
limit: int = 20,
|
|
46
|
-
offset: str | None = None,
|
|
47
|
-
) -> dict:
|
|
48
|
-
url = f"{self.api_base_url}/blockchain/balances/activity/"
|
|
49
|
-
params: dict[str, str | int] = {"address": wallet_address, "limit": limit}
|
|
50
|
-
if offset:
|
|
51
|
-
params["offset"] = offset
|
|
52
|
-
response = await self.client.get(url, params=params, headers=self._headers)
|
|
53
|
-
response.raise_for_status()
|
|
54
|
-
return response.json()
|
|
55
|
-
|
|
56
|
-
async def get_token_balance(
|
|
57
|
-
self,
|
|
58
|
-
*,
|
|
59
|
-
wallet_address: str,
|
|
60
|
-
token_id: str,
|
|
61
|
-
human_readable: bool = True,
|
|
62
|
-
) -> dict:
|
|
63
|
-
url = f"{self.api_base_url}/public/balances/token/"
|
|
64
|
-
params = {
|
|
65
|
-
"wallet_address": wallet_address,
|
|
66
|
-
"token_id": token_id,
|
|
67
|
-
"human_readable": str(human_readable).lower(),
|
|
68
|
-
}
|
|
69
|
-
response = await self.client.get(url, params=params, headers=self._headers)
|
|
70
|
-
response.raise_for_status()
|
|
71
|
-
return response.json()
|
|
72
|
-
|
|
73
|
-
async def get_pool_balance(
|
|
74
|
-
self,
|
|
75
|
-
*,
|
|
76
|
-
pool_address: str,
|
|
77
|
-
chain_id: int,
|
|
78
|
-
user_address: str,
|
|
79
|
-
human_readable: bool = True,
|
|
80
|
-
) -> dict:
|
|
81
|
-
url = f"{self.api_base_url}/public/balances/pool/"
|
|
82
|
-
params = {
|
|
83
|
-
"pool_address": pool_address,
|
|
84
|
-
"chain_id": chain_id,
|
|
85
|
-
"user_address": user_address,
|
|
86
|
-
"human_readable": str(human_readable).lower(),
|
|
87
|
-
}
|
|
88
|
-
response = await self.client.get(url, params=params, headers=self._headers)
|
|
89
|
-
response.raise_for_status()
|
|
90
|
-
return response.json()
|
|
91
|
-
|
|
92
|
-
|
|
93
10
|
def _dedupe_ordered(items: list[str]) -> list[str]:
|
|
94
11
|
seen: set[str] = set()
|
|
95
12
|
out: list[str] = []
|
|
@@ -145,146 +62,137 @@ async def balances(
|
|
|
145
62
|
"invalid_request", "wallet_label or wallet_address is required for balances"
|
|
146
63
|
)
|
|
147
64
|
|
|
148
|
-
client =
|
|
65
|
+
client = BalanceClient()
|
|
149
66
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"filtered_count": len(filtered),
|
|
186
|
-
}
|
|
187
|
-
data = out
|
|
67
|
+
if action == "enriched":
|
|
68
|
+
try:
|
|
69
|
+
data = await client.get_enriched_wallet_balances(
|
|
70
|
+
wallet_address=waddr,
|
|
71
|
+
exclude_spam_tokens=bool(exclude_spam_tokens),
|
|
72
|
+
)
|
|
73
|
+
if (
|
|
74
|
+
not include_solana
|
|
75
|
+
and _is_evm_address(waddr)
|
|
76
|
+
and isinstance(data, dict)
|
|
77
|
+
and isinstance(data.get("balances"), list)
|
|
78
|
+
):
|
|
79
|
+
balances_list = [b for b in data["balances"] if isinstance(b, dict)]
|
|
80
|
+
filtered = [
|
|
81
|
+
b
|
|
82
|
+
for b in balances_list
|
|
83
|
+
if str(b.get("network", "")).lower() != "solana"
|
|
84
|
+
]
|
|
85
|
+
if len(filtered) != len(balances_list):
|
|
86
|
+
out = dict(data)
|
|
87
|
+
out["balances"] = filtered
|
|
88
|
+
out["total_balance_usd"] = sum(_balance_usd(b) for b in filtered)
|
|
89
|
+
breakdown: dict[str, float] = {}
|
|
90
|
+
for b in filtered:
|
|
91
|
+
net = str(b.get("network") or "").strip()
|
|
92
|
+
if not net:
|
|
93
|
+
continue
|
|
94
|
+
breakdown[net] = breakdown.get(net, 0.0) + _balance_usd(b)
|
|
95
|
+
out["chain_breakdown"] = breakdown
|
|
96
|
+
out["filtered"] = {
|
|
97
|
+
"excluded_networks": ["solana"],
|
|
98
|
+
"original_count": len(balances_list),
|
|
99
|
+
"filtered_count": len(filtered),
|
|
100
|
+
}
|
|
101
|
+
data = out
|
|
188
102
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
103
|
+
return ok(data)
|
|
104
|
+
except Exception as exc: # noqa: BLE001
|
|
105
|
+
return err("balance_error", str(exc))
|
|
106
|
+
|
|
107
|
+
if action == "activity":
|
|
108
|
+
try:
|
|
109
|
+
data = await client.get_wallet_activity(
|
|
110
|
+
wallet_address=waddr,
|
|
111
|
+
limit=int(limit),
|
|
112
|
+
offset=offset,
|
|
113
|
+
)
|
|
114
|
+
return ok(
|
|
115
|
+
{
|
|
116
|
+
"wallet_address": waddr,
|
|
117
|
+
"activity": data.get("activity", []),
|
|
118
|
+
"next_offset": data.get("next_offset"),
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
except Exception as exc: # noqa: BLE001
|
|
122
|
+
return err("activity_error", str(exc))
|
|
123
|
+
|
|
124
|
+
if action == "token":
|
|
125
|
+
raw_ids: list[str] = []
|
|
126
|
+
if token_id:
|
|
127
|
+
raw_ids.append(str(token_id).strip())
|
|
128
|
+
if token_ids:
|
|
129
|
+
raw_ids.extend(str(x).strip() for x in token_ids)
|
|
130
|
+
|
|
131
|
+
ids = _dedupe_ordered([x for x in raw_ids if x])
|
|
132
|
+
if not ids:
|
|
133
|
+
return err(
|
|
134
|
+
"invalid_request",
|
|
135
|
+
"token_id or token_ids is required for balances(action=token)",
|
|
136
|
+
)
|
|
192
137
|
|
|
193
|
-
|
|
138
|
+
async def _one(tid: str) -> dict[str, Any]:
|
|
194
139
|
try:
|
|
195
|
-
data = await client.
|
|
140
|
+
data = await client.get_token_balance(
|
|
141
|
+
token_id=tid,
|
|
196
142
|
wallet_address=waddr,
|
|
197
|
-
|
|
198
|
-
offset=offset,
|
|
199
|
-
)
|
|
200
|
-
return ok(
|
|
201
|
-
{
|
|
202
|
-
"wallet_address": waddr,
|
|
203
|
-
"activity": data.get("activity", []),
|
|
204
|
-
"next_offset": data.get("next_offset"),
|
|
205
|
-
}
|
|
143
|
+
human_readable=bool(human_readable),
|
|
206
144
|
)
|
|
145
|
+
return {"ok": True, "token_id": tid, "data": data}
|
|
207
146
|
except Exception as exc: # noqa: BLE001
|
|
208
|
-
return
|
|
209
|
-
|
|
210
|
-
if action == "token":
|
|
211
|
-
raw_ids: list[str] = []
|
|
212
|
-
if token_id:
|
|
213
|
-
raw_ids.append(str(token_id).strip())
|
|
214
|
-
if token_ids:
|
|
215
|
-
raw_ids.extend(str(x).strip() for x in token_ids)
|
|
216
|
-
|
|
217
|
-
ids = _dedupe_ordered([x for x in raw_ids if x])
|
|
218
|
-
if not ids:
|
|
219
|
-
return err(
|
|
220
|
-
"invalid_request",
|
|
221
|
-
"token_id or token_ids is required for balances(action=token)",
|
|
222
|
-
)
|
|
147
|
+
return {"ok": False, "token_id": tid, "error": str(exc)}
|
|
223
148
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
data = await client.get_token_balance(
|
|
227
|
-
token_id=tid,
|
|
228
|
-
wallet_address=waddr,
|
|
229
|
-
human_readable=bool(human_readable),
|
|
230
|
-
)
|
|
231
|
-
return {"ok": True, "token_id": tid, "data": data}
|
|
232
|
-
except Exception as exc: # noqa: BLE001
|
|
233
|
-
return {"ok": False, "token_id": tid, "error": str(exc)}
|
|
149
|
+
results = await asyncio.gather(*[_one(tid) for tid in ids])
|
|
150
|
+
return ok({"wallet_address": waddr, "balances": results})
|
|
234
151
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return err(
|
|
241
|
-
"invalid_request", "chain_id is required for balances(action=pool)"
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
raw_pools: list[str] = []
|
|
245
|
-
if pool_address:
|
|
246
|
-
raw_pools.append(str(pool_address).strip())
|
|
247
|
-
if pool_addresses:
|
|
248
|
-
raw_pools.extend(str(x).strip() for x in pool_addresses)
|
|
249
|
-
|
|
250
|
-
pools = _dedupe_ordered([x for x in raw_pools if x])
|
|
251
|
-
if not pools:
|
|
252
|
-
return err(
|
|
253
|
-
"invalid_request",
|
|
254
|
-
"pool_address or pool_addresses is required for balances(action=pool)",
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
async def _one_pool(paddr: str) -> dict[str, Any]:
|
|
258
|
-
try:
|
|
259
|
-
data = await client.get_pool_balance(
|
|
260
|
-
pool_address=paddr,
|
|
261
|
-
chain_id=int(chain_id), # type: ignore[arg-type]
|
|
262
|
-
user_address=waddr,
|
|
263
|
-
human_readable=bool(human_readable),
|
|
264
|
-
)
|
|
265
|
-
return {
|
|
266
|
-
"ok": True,
|
|
267
|
-
"pool_address": paddr,
|
|
268
|
-
"chain_id": int(chain_id), # type: ignore[arg-type]
|
|
269
|
-
"data": data,
|
|
270
|
-
}
|
|
271
|
-
except Exception as exc: # noqa: BLE001
|
|
272
|
-
return {
|
|
273
|
-
"ok": False,
|
|
274
|
-
"pool_address": paddr,
|
|
275
|
-
"chain_id": int(chain_id), # type: ignore[arg-type]
|
|
276
|
-
"error": str(exc),
|
|
277
|
-
}
|
|
152
|
+
if action == "pool":
|
|
153
|
+
if chain_id is None:
|
|
154
|
+
return err(
|
|
155
|
+
"invalid_request", "chain_id is required for balances(action=pool)"
|
|
156
|
+
)
|
|
278
157
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
158
|
+
raw_pools: list[str] = []
|
|
159
|
+
if pool_address:
|
|
160
|
+
raw_pools.append(str(pool_address).strip())
|
|
161
|
+
if pool_addresses:
|
|
162
|
+
raw_pools.extend(str(x).strip() for x in pool_addresses)
|
|
163
|
+
|
|
164
|
+
pools = _dedupe_ordered([x for x in raw_pools if x])
|
|
165
|
+
if not pools:
|
|
166
|
+
return err(
|
|
167
|
+
"invalid_request",
|
|
168
|
+
"pool_address or pool_addresses is required for balances(action=pool)",
|
|
282
169
|
)
|
|
283
170
|
|
|
284
|
-
|
|
171
|
+
async def _one_pool(paddr: str) -> dict[str, Any]:
|
|
172
|
+
try:
|
|
173
|
+
data = await client.get_pool_balance(
|
|
174
|
+
pool_address=paddr,
|
|
175
|
+
chain_id=int(chain_id), # type: ignore[arg-type]
|
|
176
|
+
user_address=waddr,
|
|
177
|
+
human_readable=bool(human_readable),
|
|
178
|
+
)
|
|
179
|
+
return {
|
|
180
|
+
"ok": True,
|
|
181
|
+
"pool_address": paddr,
|
|
182
|
+
"chain_id": int(chain_id), # type: ignore[arg-type]
|
|
183
|
+
"data": data,
|
|
184
|
+
}
|
|
185
|
+
except Exception as exc: # noqa: BLE001
|
|
186
|
+
return {
|
|
187
|
+
"ok": False,
|
|
188
|
+
"pool_address": paddr,
|
|
189
|
+
"chain_id": int(chain_id), # type: ignore[arg-type]
|
|
190
|
+
"error": str(exc),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
results = await asyncio.gather(*[_one_pool(p) for p in pools])
|
|
194
|
+
return ok(
|
|
195
|
+
{"wallet_address": waddr, "chain_id": int(chain_id), "pools": results}
|
|
196
|
+
)
|
|
285
197
|
|
|
286
|
-
|
|
287
|
-
try:
|
|
288
|
-
await client.close()
|
|
289
|
-
except Exception: # noqa: BLE001
|
|
290
|
-
pass
|
|
198
|
+
return err("invalid_request", f"Unknown balances action: {action}")
|
|
@@ -13,7 +13,6 @@ from wayfinder_paths.core.clients.BRAPClient import BRAPClient
|
|
|
13
13
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
14
14
|
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
15
15
|
from wayfinder_paths.core.constants.chains import CHAIN_CODE_TO_ID
|
|
16
|
-
from wayfinder_paths.core.constants.contracts import TOKENS_REQUIRING_APPROVAL_RESET
|
|
17
16
|
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
18
17
|
from wayfinder_paths.core.constants.hyperliquid import (
|
|
19
18
|
ARBITRUM_USDC_ADDRESS,
|
|
@@ -21,8 +20,7 @@ from wayfinder_paths.core.constants.hyperliquid import (
|
|
|
21
20
|
HYPERLIQUID_BRIDGE_ADDRESS,
|
|
22
21
|
)
|
|
23
22
|
from wayfinder_paths.core.utils.tokens import (
|
|
24
|
-
|
|
25
|
-
get_token_allowance,
|
|
23
|
+
ensure_allowance,
|
|
26
24
|
)
|
|
27
25
|
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
28
26
|
from wayfinder_paths.mcp.preview import build_execution_preview
|
|
@@ -108,10 +106,6 @@ class ExecutionRequest(BaseModel):
|
|
|
108
106
|
return self
|
|
109
107
|
|
|
110
108
|
|
|
111
|
-
def _slippage_float(bps: int) -> float:
|
|
112
|
-
return max(0.0, float(int(bps)) / 10_000.0)
|
|
113
|
-
|
|
114
|
-
|
|
115
109
|
def _addr_lower(addr: str | None) -> str | None:
|
|
116
110
|
if not addr:
|
|
117
111
|
return None
|
|
@@ -257,6 +251,59 @@ def _sanitize_for_json(obj: Any) -> Any:
|
|
|
257
251
|
return obj
|
|
258
252
|
|
|
259
253
|
|
|
254
|
+
def _compact_quote(
|
|
255
|
+
quote_data: dict[str, Any], best_quote: dict[str, Any] | None
|
|
256
|
+
) -> dict[str, Any]:
|
|
257
|
+
"""Create a compact summary of quote data, stripping verbose nested structures."""
|
|
258
|
+
result: dict[str, Any] = {}
|
|
259
|
+
|
|
260
|
+
# Extract provider list from all_quotes
|
|
261
|
+
all_quotes = quote_data.get("quotes", {}).get("all_quotes", [])
|
|
262
|
+
if isinstance(all_quotes, list):
|
|
263
|
+
result["providers"] = list(
|
|
264
|
+
{
|
|
265
|
+
q.get("provider")
|
|
266
|
+
for q in all_quotes
|
|
267
|
+
if isinstance(q, dict) and q.get("provider")
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
result["quote_count"] = len(all_quotes)
|
|
271
|
+
|
|
272
|
+
# Compact best_quote - only essential fields
|
|
273
|
+
if isinstance(best_quote, dict):
|
|
274
|
+
result["best"] = {
|
|
275
|
+
"provider": best_quote.get("provider"),
|
|
276
|
+
"input_amount": best_quote.get("input_amount"),
|
|
277
|
+
"output_amount": best_quote.get("output_amount"),
|
|
278
|
+
"input_usd": best_quote.get("input_amount_usd"),
|
|
279
|
+
"output_usd": best_quote.get("output_amount_usd"),
|
|
280
|
+
}
|
|
281
|
+
# Compact fee info
|
|
282
|
+
fee = best_quote.get("fee_estimate")
|
|
283
|
+
if isinstance(fee, dict):
|
|
284
|
+
result["best"]["fee_usd"] = fee.get("fee_total_usd")
|
|
285
|
+
# Extract route summary (just protocol names)
|
|
286
|
+
quote_inner = best_quote.get("quote", {})
|
|
287
|
+
if isinstance(quote_inner, dict):
|
|
288
|
+
route = quote_inner.get("route", [])
|
|
289
|
+
if isinstance(route, list):
|
|
290
|
+
result["best"]["route"] = [
|
|
291
|
+
r.get("protocol")
|
|
292
|
+
for r in route
|
|
293
|
+
if isinstance(r, dict) and r.get("protocol")
|
|
294
|
+
]
|
|
295
|
+
# Or from includedSteps
|
|
296
|
+
steps = quote_inner.get("includedSteps", [])
|
|
297
|
+
if isinstance(steps, list) and not result["best"].get("route"):
|
|
298
|
+
result["best"]["route"] = [
|
|
299
|
+
s.get("tool")
|
|
300
|
+
for s in steps
|
|
301
|
+
if isinstance(s, dict) and s.get("tool")
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
260
307
|
def _is_dry_run() -> bool:
|
|
261
308
|
return os.getenv("DRY_RUN", "").lower() in ("1", "true")
|
|
262
309
|
|
|
@@ -298,41 +345,16 @@ async def _ensure_allowance(
|
|
|
298
345
|
# May emit 0-allowance clear tx for tokens requiring approval reset (e.g. USDT)
|
|
299
346
|
effects: list[dict[str, Any]] = []
|
|
300
347
|
|
|
301
|
-
|
|
348
|
+
sent_ok, sent = await ensure_allowance(
|
|
302
349
|
token_address=token_address,
|
|
350
|
+
owner=owner,
|
|
351
|
+
spender=spender,
|
|
352
|
+
amount=amount,
|
|
303
353
|
chain_id=chain_id,
|
|
304
|
-
|
|
305
|
-
spender_address=spender,
|
|
354
|
+
signing_callback=sign_callback,
|
|
306
355
|
)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (chain_id, token_checksum) in TOKENS_REQUIRING_APPROVAL_RESET and allowance > 0:
|
|
310
|
-
clear_tx = await build_approve_transaction(
|
|
311
|
-
from_address=owner,
|
|
312
|
-
chain_id=chain_id,
|
|
313
|
-
token_address=token_checksum,
|
|
314
|
-
spender_address=spender,
|
|
315
|
-
amount=0,
|
|
316
|
-
)
|
|
317
|
-
sent_ok, sent = await _broadcast(sign_callback, clear_tx, chain_id=chain_id)
|
|
318
|
-
effects.append({"type": "tx", "label": "approve_clear", **sent})
|
|
319
|
-
if not sent_ok:
|
|
320
|
-
return False, effects
|
|
321
|
-
# After clear, we will still submit a fresh approve below.
|
|
322
|
-
|
|
323
|
-
if allowance >= int(amount):
|
|
324
|
-
return True, effects
|
|
325
|
-
|
|
326
|
-
approve_tx = await build_approve_transaction(
|
|
327
|
-
from_address=owner,
|
|
328
|
-
chain_id=chain_id,
|
|
329
|
-
token_address=token_checksum,
|
|
330
|
-
spender_address=spender,
|
|
331
|
-
amount=int(amount),
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
sent_ok, sent = await _broadcast(sign_callback, approve_tx, chain_id=chain_id)
|
|
335
|
-
effects.append({"type": "tx", "label": "approve", **sent})
|
|
356
|
+
if isinstance(sent, dict) and sent:
|
|
357
|
+
effects.append({"type": "tx", "label": "approve", **sent})
|
|
336
358
|
return sent_ok, effects
|
|
337
359
|
|
|
338
360
|
|
|
@@ -550,7 +572,7 @@ async def execute(
|
|
|
550
572
|
"recipient": rcpt,
|
|
551
573
|
"preview": preview_text,
|
|
552
574
|
"effects": effects,
|
|
553
|
-
"raw":
|
|
575
|
+
"raw": _compact_quote(quote_data, None),
|
|
554
576
|
}
|
|
555
577
|
)
|
|
556
578
|
store.put(key, tool_input, response)
|
|
@@ -576,7 +598,7 @@ async def execute(
|
|
|
576
598
|
"recipient": rcpt,
|
|
577
599
|
"preview": preview_text,
|
|
578
600
|
"effects": effects,
|
|
579
|
-
"raw":
|
|
601
|
+
"raw": _compact_quote(quote_data, best_quote),
|
|
580
602
|
}
|
|
581
603
|
)
|
|
582
604
|
store.put(key, tool_input, response)
|
|
@@ -196,10 +196,10 @@ async def quote_swap(
|
|
|
196
196
|
data = await brap_client.get_quote(
|
|
197
197
|
from_token=from_token_addr,
|
|
198
198
|
to_token=to_token_addr,
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
from_chain=from_chain_id,
|
|
200
|
+
to_chain=to_chain_id,
|
|
201
201
|
from_wallet=sender,
|
|
202
|
-
|
|
202
|
+
from_amount=str(amount_raw),
|
|
203
203
|
slippage=slip,
|
|
204
204
|
)
|
|
205
205
|
except Exception as exc: # noqa: BLE001
|
|
@@ -244,9 +244,20 @@ async def quote_swap(
|
|
|
244
244
|
"native_input": best_quote.get("native_input"),
|
|
245
245
|
"native_output": best_quote.get("native_output"),
|
|
246
246
|
"error": best_quote.get("error"),
|
|
247
|
-
"wrap_transaction": best_quote.get("wrap_transaction"),
|
|
248
|
-
"unwrap_transaction": best_quote.get("unwrap_transaction"),
|
|
249
247
|
}
|
|
248
|
+
|
|
249
|
+
# Strip data fields from wrap/unwrap transactions to reduce response size
|
|
250
|
+
wrap_tx = best_quote.get("wrap_transaction")
|
|
251
|
+
if isinstance(wrap_tx, dict):
|
|
252
|
+
best_out["wrap_transaction"] = {
|
|
253
|
+
k: v for k, v in wrap_tx.items() if k != "data"
|
|
254
|
+
}
|
|
255
|
+
unwrap_tx = best_quote.get("unwrap_transaction")
|
|
256
|
+
if isinstance(unwrap_tx, dict):
|
|
257
|
+
best_out["unwrap_transaction"] = {
|
|
258
|
+
k: v for k, v in unwrap_tx.items() if k != "data"
|
|
259
|
+
}
|
|
260
|
+
|
|
250
261
|
if include_calldata:
|
|
251
262
|
best_out["calldata"] = calldata
|
|
252
263
|
else:
|
|
@@ -14,9 +14,6 @@ from typing import Any
|
|
|
14
14
|
|
|
15
15
|
from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
|
|
16
16
|
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdapter
|
|
17
|
-
from wayfinder_paths.adapters.hyperliquid_adapter.executor import (
|
|
18
|
-
LocalHyperliquidExecutor,
|
|
19
|
-
)
|
|
20
17
|
from wayfinder_paths.adapters.hyperliquid_adapter.paired_filler import (
|
|
21
18
|
MIN_NOTIONAL_USD,
|
|
22
19
|
FillConfig,
|
|
@@ -51,9 +48,6 @@ from wayfinder_paths.core.analytics import (
|
|
|
51
48
|
from wayfinder_paths.core.analytics import (
|
|
52
49
|
z_from_conf as analytics_z_from_conf,
|
|
53
50
|
)
|
|
54
|
-
from wayfinder_paths.core.clients.protocols import (
|
|
55
|
-
HyperliquidExecutorProtocol as HyperliquidExecutor,
|
|
56
|
-
)
|
|
57
51
|
from wayfinder_paths.core.constants.contracts import HYPERLIQUID_BRIDGE
|
|
58
52
|
from wayfinder_paths.core.strategies.descriptors import (
|
|
59
53
|
Complexity,
|
|
@@ -64,6 +58,7 @@ from wayfinder_paths.core.strategies.descriptors import (
|
|
|
64
58
|
Volatility,
|
|
65
59
|
)
|
|
66
60
|
from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
|
|
61
|
+
from wayfinder_paths.core.types import HyperliquidSignCallback
|
|
67
62
|
from wayfinder_paths.policies.erc20 import any_erc20_function
|
|
68
63
|
from wayfinder_paths.policies.hyperliquid import (
|
|
69
64
|
any_hyperliquid_l1_payload,
|
|
@@ -188,16 +183,15 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
188
183
|
*,
|
|
189
184
|
main_wallet: dict[str, Any] | None = None,
|
|
190
185
|
strategy_wallet: dict[str, Any] | None = None,
|
|
191
|
-
|
|
192
|
-
api_key: str | None = None,
|
|
186
|
+
strategy_sign_typed_data: HyperliquidSignCallback | None = None,
|
|
193
187
|
main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
|
|
194
188
|
strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
|
|
195
189
|
| None = None,
|
|
196
190
|
) -> None:
|
|
197
191
|
super().__init__(
|
|
198
|
-
api_key=api_key,
|
|
199
192
|
main_wallet_signing_callback=main_wallet_signing_callback,
|
|
200
193
|
strategy_wallet_signing_callback=strategy_wallet_signing_callback,
|
|
194
|
+
strategy_sign_typed_data=strategy_sign_typed_data,
|
|
201
195
|
)
|
|
202
196
|
|
|
203
197
|
merged_config = dict(config or {})
|
|
@@ -228,24 +222,14 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
228
222
|
"strategy": self.config,
|
|
229
223
|
}
|
|
230
224
|
|
|
231
|
-
# This is only required for placing/canceling orders (not market reads).
|
|
232
|
-
hl_executor = hyperliquid_executor
|
|
233
|
-
if hl_executor is None:
|
|
234
|
-
try:
|
|
235
|
-
hl_executor = LocalHyperliquidExecutor(config=adapter_config)
|
|
236
|
-
self.logger.info("Created LocalHyperliquidExecutor for real execution")
|
|
237
|
-
except Exception as e:
|
|
238
|
-
self.logger.warning(
|
|
239
|
-
f"Could not create LocalHyperliquidExecutor: {e}. "
|
|
240
|
-
"Real Hyperliquid execution will not be available."
|
|
241
|
-
)
|
|
242
|
-
|
|
243
225
|
# Hyperliquid market data adapter should be usable even when wallet/web3
|
|
244
226
|
# configuration is missing (e.g. local --action analyze).
|
|
227
|
+
# The adapter will create Exchange internally with local signer from config
|
|
228
|
+
# if no sign_callback is provided.
|
|
245
229
|
try:
|
|
246
230
|
self.hyperliquid_adapter = HyperliquidAdapter(
|
|
247
231
|
config=adapter_config,
|
|
248
|
-
|
|
232
|
+
sign_callback=strategy_sign_typed_data,
|
|
249
233
|
)
|
|
250
234
|
except Exception as e:
|
|
251
235
|
self.logger.warning(f"Could not initialize HyperliquidAdapter: {e}")
|