wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.25__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/__init__.py +2 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +2 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.25.dist-info/METADATA +377 -0
- wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
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
|
|
10
|
+
from wayfinder_paths.mcp.utils import err, find_wallet_by_label, normalize_address, ok
|
|
11
|
+
|
|
12
|
+
|
|
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
|
+
def _dedupe_ordered(items: list[str]) -> list[str]:
|
|
94
|
+
seen: set[str] = set()
|
|
95
|
+
out: list[str] = []
|
|
96
|
+
for item in items:
|
|
97
|
+
key = item.lower()
|
|
98
|
+
if key in seen:
|
|
99
|
+
continue
|
|
100
|
+
seen.add(key)
|
|
101
|
+
out.append(item)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_evm_address(addr: str) -> bool:
|
|
106
|
+
a = (addr or "").strip()
|
|
107
|
+
return a.startswith("0x") and len(a) == 42
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _balance_usd(entry: dict[str, Any]) -> float:
|
|
111
|
+
val = entry.get("balanceUSD", 0)
|
|
112
|
+
try:
|
|
113
|
+
return float(val or 0)
|
|
114
|
+
except (TypeError, ValueError):
|
|
115
|
+
return 0.0
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def balances(
|
|
119
|
+
action: Literal["token", "pool", "enriched", "activity"],
|
|
120
|
+
*,
|
|
121
|
+
wallet_label: str | None = None,
|
|
122
|
+
wallet_address: str | None = None,
|
|
123
|
+
human_readable: bool = True,
|
|
124
|
+
include_solana: bool = False,
|
|
125
|
+
exclude_spam_tokens: bool = True,
|
|
126
|
+
limit: int = 20,
|
|
127
|
+
offset: str | None = None,
|
|
128
|
+
token_id: str | None = None,
|
|
129
|
+
token_ids: list[str] | None = None,
|
|
130
|
+
chain_id: int | None = None,
|
|
131
|
+
pool_address: str | None = None,
|
|
132
|
+
pool_addresses: list[str] | None = None,
|
|
133
|
+
) -> dict[str, Any]:
|
|
134
|
+
waddr = normalize_address(wallet_address)
|
|
135
|
+
if not waddr:
|
|
136
|
+
want = (wallet_label or "").strip()
|
|
137
|
+
if want:
|
|
138
|
+
w = find_wallet_by_label(want)
|
|
139
|
+
if not w:
|
|
140
|
+
return err("not_found", f"Unknown wallet_label: {want}")
|
|
141
|
+
waddr = normalize_address(w.get("address"))
|
|
142
|
+
|
|
143
|
+
if not waddr:
|
|
144
|
+
return err(
|
|
145
|
+
"invalid_request", "wallet_label or wallet_address is required for balances"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
client = _BalanceClient()
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
if action == "enriched":
|
|
152
|
+
try:
|
|
153
|
+
data = await client.get_enriched_wallet_balances(
|
|
154
|
+
wallet_address=waddr,
|
|
155
|
+
exclude_spam_tokens=bool(exclude_spam_tokens),
|
|
156
|
+
)
|
|
157
|
+
if (
|
|
158
|
+
not include_solana
|
|
159
|
+
and _is_evm_address(waddr)
|
|
160
|
+
and isinstance(data, dict)
|
|
161
|
+
and isinstance(data.get("balances"), list)
|
|
162
|
+
):
|
|
163
|
+
balances_list = [b for b in data["balances"] if isinstance(b, dict)]
|
|
164
|
+
filtered = [
|
|
165
|
+
b
|
|
166
|
+
for b in balances_list
|
|
167
|
+
if str(b.get("network", "")).lower() != "solana"
|
|
168
|
+
]
|
|
169
|
+
if len(filtered) != len(balances_list):
|
|
170
|
+
out = dict(data)
|
|
171
|
+
out["balances"] = filtered
|
|
172
|
+
out["total_balance_usd"] = sum(
|
|
173
|
+
_balance_usd(b) for b in filtered
|
|
174
|
+
)
|
|
175
|
+
breakdown: dict[str, float] = {}
|
|
176
|
+
for b in filtered:
|
|
177
|
+
net = str(b.get("network") or "").strip()
|
|
178
|
+
if not net:
|
|
179
|
+
continue
|
|
180
|
+
breakdown[net] = breakdown.get(net, 0.0) + _balance_usd(b)
|
|
181
|
+
out["chain_breakdown"] = breakdown
|
|
182
|
+
out["filtered"] = {
|
|
183
|
+
"excluded_networks": ["solana"],
|
|
184
|
+
"original_count": len(balances_list),
|
|
185
|
+
"filtered_count": len(filtered),
|
|
186
|
+
}
|
|
187
|
+
data = out
|
|
188
|
+
|
|
189
|
+
return ok(data)
|
|
190
|
+
except Exception as exc: # noqa: BLE001
|
|
191
|
+
return err("balance_error", str(exc))
|
|
192
|
+
|
|
193
|
+
if action == "activity":
|
|
194
|
+
try:
|
|
195
|
+
data = await client.get_wallet_activity(
|
|
196
|
+
wallet_address=waddr,
|
|
197
|
+
limit=int(limit),
|
|
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
|
+
}
|
|
206
|
+
)
|
|
207
|
+
except Exception as exc: # noqa: BLE001
|
|
208
|
+
return err("activity_error", str(exc))
|
|
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
|
+
)
|
|
223
|
+
|
|
224
|
+
async def _one(tid: str) -> dict[str, Any]:
|
|
225
|
+
try:
|
|
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)}
|
|
234
|
+
|
|
235
|
+
results = await asyncio.gather(*[_one(tid) for tid in ids])
|
|
236
|
+
return ok({"wallet_address": waddr, "balances": results})
|
|
237
|
+
|
|
238
|
+
if action == "pool":
|
|
239
|
+
if chain_id is None:
|
|
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
|
+
}
|
|
278
|
+
|
|
279
|
+
results = await asyncio.gather(*[_one_pool(p) for p in pools])
|
|
280
|
+
return ok(
|
|
281
|
+
{"wallet_address": waddr, "chain_id": int(chain_id), "pools": results}
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
return err("invalid_request", f"Unknown balances action: {action}")
|
|
285
|
+
|
|
286
|
+
finally:
|
|
287
|
+
try:
|
|
288
|
+
await client.close()
|
|
289
|
+
except Exception: # noqa: BLE001
|
|
290
|
+
pass
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.mcp.utils import err, ok, read_text_excerpt, read_yaml, repo_root
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def discover(
|
|
9
|
+
kind: Literal["adapters", "strategies"],
|
|
10
|
+
detail: Literal["summary", "full"] = "summary",
|
|
11
|
+
) -> dict[str, Any]:
|
|
12
|
+
root = repo_root()
|
|
13
|
+
base = (
|
|
14
|
+
root / "wayfinder_paths" / ("adapters" if kind == "adapters" else "strategies")
|
|
15
|
+
)
|
|
16
|
+
if not base.exists():
|
|
17
|
+
return err("not_found", f"Directory not found: {base}")
|
|
18
|
+
|
|
19
|
+
items: list[dict[str, Any]] = []
|
|
20
|
+
for child in sorted(base.iterdir()):
|
|
21
|
+
if not child.is_dir():
|
|
22
|
+
continue
|
|
23
|
+
manifest_path = child / "manifest.yaml"
|
|
24
|
+
if not manifest_path.exists():
|
|
25
|
+
continue
|
|
26
|
+
manifest = read_yaml(manifest_path)
|
|
27
|
+
rel_manifest = str(manifest_path.relative_to(root))
|
|
28
|
+
|
|
29
|
+
if kind == "adapters":
|
|
30
|
+
item = {
|
|
31
|
+
"name": child.name,
|
|
32
|
+
"entrypoint": manifest.get("entrypoint"),
|
|
33
|
+
"capabilities": manifest.get("capabilities", []),
|
|
34
|
+
"dependencies": manifest.get("dependencies", []),
|
|
35
|
+
"manifest_path": rel_manifest,
|
|
36
|
+
}
|
|
37
|
+
else:
|
|
38
|
+
adapters = manifest.get("adapters", [])
|
|
39
|
+
item = {
|
|
40
|
+
"name": child.name,
|
|
41
|
+
"entrypoint": manifest.get("entrypoint"),
|
|
42
|
+
"adapters": adapters if isinstance(adapters, list) else [],
|
|
43
|
+
"permissions_policy_present": bool(
|
|
44
|
+
isinstance(manifest.get("permissions"), dict)
|
|
45
|
+
and (manifest.get("permissions") or {}).get("policy")
|
|
46
|
+
),
|
|
47
|
+
"manifest_path": rel_manifest,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if detail == "full":
|
|
51
|
+
readme = read_text_excerpt(child / "README.md")
|
|
52
|
+
if readme:
|
|
53
|
+
item["readme_excerpt"] = readme
|
|
54
|
+
item["manifest"] = manifest
|
|
55
|
+
|
|
56
|
+
items.append(item)
|
|
57
|
+
|
|
58
|
+
return ok({"kind": kind, "items": items})
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _default_claude_notes_for_adapter(name: str) -> dict[str, Any] | None:
|
|
62
|
+
# Keep this deliberately small; the real docs live in .claude/skills.
|
|
63
|
+
if name == "brap_adapter":
|
|
64
|
+
return {
|
|
65
|
+
"reads": [
|
|
66
|
+
{
|
|
67
|
+
"name": "quote_swap (via mcp__wayfinder__quote_swap)",
|
|
68
|
+
"summary": "Get best BRAP route + fees; no on-chain effects.",
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"writes": [
|
|
72
|
+
{
|
|
73
|
+
"name": "swap (via mcp__wayfinder__execute kind=swap)",
|
|
74
|
+
"risk": "funds",
|
|
75
|
+
"summary": "May submit ERC20 approvals and a swap/bridge tx.",
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
"gotchas": [
|
|
79
|
+
"USDT-style tokens may require setting allowance to 0 before approve.",
|
|
80
|
+
"Cross-chain swaps still broadcast a tx on the source chain.",
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
if name == "balance_adapter":
|
|
84
|
+
return {
|
|
85
|
+
"reads": [
|
|
86
|
+
{
|
|
87
|
+
"name": "balances (via mcp__wayfinder__balances)",
|
|
88
|
+
"summary": "Read token/pool balances via Wayfinder API.",
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
"writes": [
|
|
92
|
+
{
|
|
93
|
+
"name": "send (via mcp__wayfinder__execute kind=send)",
|
|
94
|
+
"risk": "funds",
|
|
95
|
+
"summary": "Native/ERC20 sends from a local wallet.",
|
|
96
|
+
}
|
|
97
|
+
],
|
|
98
|
+
"gotchas": ["Prefer DRY_RUN=1 until ready to broadcast."],
|
|
99
|
+
}
|
|
100
|
+
if name == "hyperliquid_adapter":
|
|
101
|
+
return {
|
|
102
|
+
"reads": [
|
|
103
|
+
{
|
|
104
|
+
"name": "hyperliquid (via mcp__wayfinder__hyperliquid)",
|
|
105
|
+
"summary": "User state + market reads (no signing).",
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
"writes": [
|
|
109
|
+
{
|
|
110
|
+
"name": "hyperliquid_execute (via mcp__wayfinder__hyperliquid_execute)",
|
|
111
|
+
"risk": "funds",
|
|
112
|
+
"summary": "Perp orders/leverage/withdraw with local signing (gated by review prompt).",
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"gotchas": [
|
|
116
|
+
"Perp coins are mapped to asset_id (<10000).",
|
|
117
|
+
"Builder fees are attributed to 0xaA1D89f333857eD78F8434CC4f896A9293EFE65c and use tenths-of-bp units.",
|
|
118
|
+
"Prefer DRY_RUN=1 until ready to execute.",
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def describe(
|
|
125
|
+
kind: Literal["adapter", "strategy"],
|
|
126
|
+
name: str,
|
|
127
|
+
include_manifest: bool = True,
|
|
128
|
+
include_readme_excerpt: bool = True,
|
|
129
|
+
include_claude_notes: bool = True,
|
|
130
|
+
) -> dict[str, Any]:
|
|
131
|
+
root = repo_root()
|
|
132
|
+
base = (
|
|
133
|
+
root / "wayfinder_paths" / ("adapters" if kind == "adapter" else "strategies")
|
|
134
|
+
)
|
|
135
|
+
target = base / name
|
|
136
|
+
if not target.exists():
|
|
137
|
+
return err("not_found", f"Unknown {kind}: {name}")
|
|
138
|
+
|
|
139
|
+
manifest_path = target / "manifest.yaml"
|
|
140
|
+
if not manifest_path.exists():
|
|
141
|
+
return err("not_found", f"Missing manifest.yaml for {kind}: {name}")
|
|
142
|
+
|
|
143
|
+
out: dict[str, Any] = {"kind": kind, "name": name}
|
|
144
|
+
if include_manifest:
|
|
145
|
+
out["manifest"] = read_yaml(manifest_path)
|
|
146
|
+
out["manifest_path"] = str(manifest_path.relative_to(root))
|
|
147
|
+
|
|
148
|
+
if include_readme_excerpt:
|
|
149
|
+
readme = read_text_excerpt(target / "README.md")
|
|
150
|
+
if readme:
|
|
151
|
+
out["readme_excerpt"] = readme
|
|
152
|
+
|
|
153
|
+
if include_claude_notes and kind == "adapter":
|
|
154
|
+
notes = _default_claude_notes_for_adapter(name)
|
|
155
|
+
if notes:
|
|
156
|
+
out["claude_notes"] = notes
|
|
157
|
+
|
|
158
|
+
return ok(out)
|