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.

Files changed (124) hide show
  1. wayfinder_paths/__init__.py +2 -0
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
  3. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  4. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
  5. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  6. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  7. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  8. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  9. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  10. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  11. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  12. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  13. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  14. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
  15. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
  17. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
  19. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  20. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
  21. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  22. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  24. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  28. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  29. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  30. wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
  31. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  32. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
  33. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  34. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  35. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  36. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  37. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  38. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  39. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  40. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  41. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  42. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  43. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  46. wayfinder_paths/conftest.py +24 -17
  47. wayfinder_paths/core/__init__.py +2 -0
  48. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  49. wayfinder_paths/core/adapters/models.py +17 -7
  50. wayfinder_paths/core/clients/BRAPClient.py +1 -1
  51. wayfinder_paths/core/clients/TokenClient.py +47 -1
  52. wayfinder_paths/core/clients/WayfinderClient.py +1 -2
  53. wayfinder_paths/core/clients/protocols.py +21 -22
  54. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  55. wayfinder_paths/core/config.py +12 -0
  56. wayfinder_paths/core/constants/__init__.py +15 -0
  57. wayfinder_paths/core/constants/base.py +6 -1
  58. wayfinder_paths/core/constants/contracts.py +39 -26
  59. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  60. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  61. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  62. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  63. wayfinder_paths/core/engine/manifest.py +66 -0
  64. wayfinder_paths/core/strategies/Strategy.py +0 -61
  65. wayfinder_paths/core/strategies/__init__.py +10 -1
  66. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  67. wayfinder_paths/core/utils/test_transaction.py +289 -0
  68. wayfinder_paths/core/utils/transaction.py +44 -1
  69. wayfinder_paths/core/utils/web3.py +3 -0
  70. wayfinder_paths/mcp/__init__.py +5 -0
  71. wayfinder_paths/mcp/preview.py +185 -0
  72. wayfinder_paths/mcp/scripting.py +84 -0
  73. wayfinder_paths/mcp/server.py +52 -0
  74. wayfinder_paths/mcp/state/profile_store.py +195 -0
  75. wayfinder_paths/mcp/state/store.py +89 -0
  76. wayfinder_paths/mcp/test_scripting.py +267 -0
  77. wayfinder_paths/mcp/tools/__init__.py +0 -0
  78. wayfinder_paths/mcp/tools/balances.py +290 -0
  79. wayfinder_paths/mcp/tools/discovery.py +158 -0
  80. wayfinder_paths/mcp/tools/execute.py +770 -0
  81. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  82. wayfinder_paths/mcp/tools/quotes.py +288 -0
  83. wayfinder_paths/mcp/tools/run_script.py +286 -0
  84. wayfinder_paths/mcp/tools/strategies.py +188 -0
  85. wayfinder_paths/mcp/tools/tokens.py +46 -0
  86. wayfinder_paths/mcp/tools/wallets.py +354 -0
  87. wayfinder_paths/mcp/utils.py +129 -0
  88. wayfinder_paths/policies/hyperliquid.py +1 -1
  89. wayfinder_paths/policies/lifi.py +18 -0
  90. wayfinder_paths/policies/util.py +8 -2
  91. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
  92. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  93. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  94. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  95. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  96. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  97. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  98. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  99. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  100. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  101. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  102. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  103. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  104. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  105. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
  106. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  107. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  108. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
  109. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
  110. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
  111. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  112. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
  113. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
  114. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  115. wayfinder_paths/tests/test_test_coverage.py +1 -4
  116. wayfinder_paths-0.1.25.dist-info/METADATA +377 -0
  117. wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
  118. wayfinder_paths/scripts/create_strategy.py +0 -139
  119. wayfinder_paths/scripts/make_wallets.py +0 -142
  120. wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
  121. wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
  122. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  123. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/LICENSE +0 -0
  124. {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)