wayfinder-paths 0.1.22__py3-none-any.whl → 0.1.24__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 (156) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
  4. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  5. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
  6. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  7. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  8. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  9. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  10. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  11. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  12. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  13. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  14. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  15. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  16. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  17. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  18. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  19. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  20. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
  21. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  22. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
  23. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  24. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
  25. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  26. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  28. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  29. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  30. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  31. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  32. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  33. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  34. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  35. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  36. wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
  37. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  38. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
  39. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  40. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  41. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  42. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  43. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  44. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  45. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  46. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  47. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  48. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  49. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  50. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  51. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  52. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  53. wayfinder_paths/conftest.py +24 -17
  54. wayfinder_paths/core/__init__.py +0 -3
  55. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  56. wayfinder_paths/core/adapters/models.py +17 -7
  57. wayfinder_paths/core/clients/BRAPClient.py +4 -1
  58. wayfinder_paths/core/clients/ClientManager.py +0 -7
  59. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  60. wayfinder_paths/core/clients/TokenClient.py +47 -1
  61. wayfinder_paths/core/clients/WayfinderClient.py +1 -3
  62. wayfinder_paths/core/clients/__init__.py +0 -5
  63. wayfinder_paths/core/clients/protocols.py +21 -35
  64. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  65. wayfinder_paths/core/config.py +10 -162
  66. wayfinder_paths/core/constants/__init__.py +73 -2
  67. wayfinder_paths/core/constants/base.py +8 -17
  68. wayfinder_paths/core/constants/chains.py +36 -0
  69. wayfinder_paths/core/constants/contracts.py +52 -0
  70. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  71. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  72. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  73. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  74. wayfinder_paths/core/constants/tokens.py +9 -0
  75. wayfinder_paths/core/engine/manifest.py +66 -0
  76. wayfinder_paths/core/strategies/Strategy.py +0 -71
  77. wayfinder_paths/core/strategies/__init__.py +10 -1
  78. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  79. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  80. wayfinder_paths/core/utils/test_transaction.py +289 -0
  81. wayfinder_paths/core/utils/tokens.py +28 -0
  82. wayfinder_paths/core/utils/transaction.py +57 -8
  83. wayfinder_paths/core/utils/web3.py +8 -3
  84. wayfinder_paths/mcp/__init__.py +5 -0
  85. wayfinder_paths/mcp/preview.py +185 -0
  86. wayfinder_paths/mcp/scripting.py +84 -0
  87. wayfinder_paths/mcp/server.py +52 -0
  88. wayfinder_paths/mcp/state/profile_store.py +195 -0
  89. wayfinder_paths/mcp/state/store.py +89 -0
  90. wayfinder_paths/mcp/test_scripting.py +267 -0
  91. wayfinder_paths/mcp/tools/__init__.py +0 -0
  92. wayfinder_paths/mcp/tools/balances.py +290 -0
  93. wayfinder_paths/mcp/tools/discovery.py +158 -0
  94. wayfinder_paths/mcp/tools/execute.py +770 -0
  95. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  96. wayfinder_paths/mcp/tools/quotes.py +288 -0
  97. wayfinder_paths/mcp/tools/run_script.py +286 -0
  98. wayfinder_paths/mcp/tools/strategies.py +188 -0
  99. wayfinder_paths/mcp/tools/tokens.py +46 -0
  100. wayfinder_paths/mcp/tools/wallets.py +354 -0
  101. wayfinder_paths/mcp/utils.py +129 -0
  102. wayfinder_paths/policies/enso.py +1 -2
  103. wayfinder_paths/policies/hyper_evm.py +6 -3
  104. wayfinder_paths/policies/hyperlend.py +1 -2
  105. wayfinder_paths/policies/hyperliquid.py +1 -1
  106. wayfinder_paths/policies/lifi.py +18 -0
  107. wayfinder_paths/policies/moonwell.py +12 -7
  108. wayfinder_paths/policies/prjx.py +1 -3
  109. wayfinder_paths/policies/util.py +8 -2
  110. wayfinder_paths/run_strategy.py +97 -300
  111. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  112. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
  113. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  114. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  115. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  116. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  117. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  118. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  119. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  120. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  121. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  122. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  123. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  124. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  125. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  126. wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
  127. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  128. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  129. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
  130. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
  131. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
  132. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  133. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
  134. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
  135. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  136. wayfinder_paths/tests/test_test_coverage.py +1 -4
  137. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  138. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  139. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  140. wayfinder_paths/core/clients/WalletClient.py +0 -41
  141. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  142. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  143. wayfinder_paths/scripts/create_strategy.py +0 -139
  144. wayfinder_paths/scripts/make_wallets.py +0 -142
  145. wayfinder_paths/templates/adapter/README.md +0 -150
  146. wayfinder_paths/templates/adapter/adapter.py +0 -16
  147. wayfinder_paths/templates/adapter/examples.json +0 -8
  148. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  149. wayfinder_paths/templates/strategy/README.md +0 -186
  150. wayfinder_paths/templates/strategy/examples.json +0 -11
  151. wayfinder_paths/templates/strategy/strategy.py +0 -35
  152. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  153. wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
  154. wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
  155. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  156. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from wayfinder_paths.core.constants.hyperliquid import (
8
+ ARBITRUM_USDC_TOKEN_ID,
9
+ HYPE_FEE_WALLET,
10
+ HYPERLIQUID_BRIDGE_ADDRESS,
11
+ )
12
+ from wayfinder_paths.mcp.utils import (
13
+ find_wallet_by_label,
14
+ normalize_address,
15
+ read_text_excerpt,
16
+ repo_root,
17
+ )
18
+
19
+
20
+ def build_execution_preview(tool_input: dict[str, Any]) -> dict[str, Any]:
21
+ req = tool_input.get("request") if isinstance(tool_input, dict) else None
22
+ if not isinstance(req, dict):
23
+ return {
24
+ "summary": "Execute request missing 'request' object.",
25
+ "recipient_mismatch": False,
26
+ }
27
+
28
+ kind = str(req.get("kind") or "").strip()
29
+ wallet_label = str(req.get("wallet_label") or "").strip()
30
+ w = find_wallet_by_label(wallet_label) if wallet_label else None
31
+ sender = normalize_address((w or {}).get("address")) if w else None
32
+
33
+ recipient = normalize_address(req.get("recipient"))
34
+ if kind == "swap":
35
+ recipient = recipient or sender
36
+ summary = (
37
+ "EXECUTE swap\n"
38
+ f"wallet_label: {wallet_label}\n"
39
+ f"from_token: {req.get('from_token')}\n"
40
+ f"to_token: {req.get('to_token')}\n"
41
+ f"amount: {req.get('amount')}\n"
42
+ f"slippage_bps: {req.get('slippage_bps')}\n"
43
+ f"sender: {sender or '(unknown)'}\n"
44
+ f"recipient: {recipient or '(unknown)'}"
45
+ )
46
+ elif kind == "hyperliquid_deposit":
47
+ recipient = normalize_address(HYPERLIQUID_BRIDGE_ADDRESS)
48
+ summary = (
49
+ "EXECUTE hyperliquid_deposit (Bridge2)\n"
50
+ f"wallet_label: {wallet_label}\n"
51
+ f"token: {ARBITRUM_USDC_TOKEN_ID}\n"
52
+ f"amount: {req.get('amount')}\n"
53
+ "chain_id: 42161\n"
54
+ f"sender: {sender or '(unknown)'}\n"
55
+ f"recipient: {recipient or '(missing)'}"
56
+ )
57
+ elif kind == "send":
58
+ summary = (
59
+ "EXECUTE send\n"
60
+ f"wallet_label: {wallet_label}\n"
61
+ f"token: {req.get('token')}\n"
62
+ f"amount: {req.get('amount')}\n"
63
+ f"chain_id: {req.get('chain_id')}\n"
64
+ f"sender: {sender or '(unknown)'}\n"
65
+ f"recipient: {recipient or '(missing)'}"
66
+ )
67
+ else:
68
+ summary = f"EXECUTE {kind or '(unknown kind)'}\nwallet_label: {wallet_label}"
69
+
70
+ mismatch = bool(sender and recipient and sender.lower() != recipient.lower())
71
+ if kind == "hyperliquid_deposit":
72
+ mismatch = False # deposit recipient is fixed; mismatch is expected
73
+ return {"summary": summary, "recipient_mismatch": mismatch}
74
+
75
+
76
+ def build_run_script_preview(tool_input: dict[str, Any]) -> dict[str, Any]:
77
+ ti = tool_input if isinstance(tool_input, dict) else {}
78
+ path_raw = ti.get("script_path") or ti.get("path")
79
+ args = ti.get("args") if isinstance(ti.get("args"), list) else []
80
+ dry_run = bool(ti.get("dry_run", True))
81
+
82
+ if not isinstance(path_raw, str) or not path_raw.strip():
83
+ return {"summary": "RUN_SCRIPT missing script_path."}
84
+
85
+ root = repo_root()
86
+ p = Path(path_raw)
87
+ if not p.is_absolute():
88
+ p = root / p
89
+ resolved = p.resolve(strict=False)
90
+
91
+ rel = str(resolved)
92
+ try:
93
+ rel = str(resolved.relative_to(root))
94
+ except Exception:
95
+ pass
96
+
97
+ sha = None
98
+ try:
99
+ if resolved.exists():
100
+ sha = hashlib.sha256(resolved.read_bytes()).hexdigest()
101
+ except Exception:
102
+ sha = None
103
+
104
+ excerpt = read_text_excerpt(resolved, max_chars=1200) if resolved.exists() else None
105
+
106
+ summary = (
107
+ "RUN_SCRIPT (executes local python)\n"
108
+ f"script_path: {rel}\n"
109
+ f"dry_run: {dry_run}\n"
110
+ f"args: {args or []}\n"
111
+ f"script_sha256: {(sha[:12] + '…') if sha else '(unavailable)'}"
112
+ )
113
+ if excerpt:
114
+ summary += "\n\n" + excerpt
115
+ else:
116
+ summary += "\n\n(no script contents available)"
117
+
118
+ return {"summary": summary}
119
+
120
+
121
+ def build_hyperliquid_execute_preview(tool_input: dict[str, Any]) -> dict[str, Any]:
122
+ # hyperliquid_execute uses direct parameters, not a 'request' wrapper
123
+ req = tool_input if isinstance(tool_input, dict) else {}
124
+ if not req:
125
+ return {"summary": "HYPERLIQUID_EXECUTE missing parameters."}
126
+
127
+ action = str(req.get("action") or "").strip()
128
+ wallet_label = str(req.get("wallet_label") or "").strip()
129
+ w = find_wallet_by_label(wallet_label) if wallet_label else None
130
+ sender = normalize_address((w or {}).get("address")) if w else None
131
+
132
+ dry_run = req.get("dry_run")
133
+ coin = req.get("coin")
134
+ asset_id = req.get("asset_id")
135
+
136
+ header = "HYPERLIQUID_EXECUTE\n"
137
+ base = (
138
+ f"action: {action or '(missing)'}\n"
139
+ f"wallet_label: {wallet_label}\n"
140
+ f"address: {sender or '(unknown)'}\n"
141
+ f"dry_run: {dry_run}\n"
142
+ f"coin: {coin}\n"
143
+ f"asset_id: {asset_id}"
144
+ )
145
+
146
+ if action == "place_order":
147
+ details = (
148
+ "\n\nORDER\n"
149
+ f"order_type: {req.get('order_type')}\n"
150
+ f"is_buy: {req.get('is_buy')}\n"
151
+ f"size: {req.get('size')}\n"
152
+ f"usd_amount: {req.get('usd_amount')}\n"
153
+ f"usd_amount_kind: {req.get('usd_amount_kind')}\n"
154
+ f"price: {req.get('price')}\n"
155
+ f"slippage: {req.get('slippage')}\n"
156
+ f"reduce_only: {req.get('reduce_only')}\n"
157
+ f"cloid: {req.get('cloid')}\n"
158
+ f"leverage: {req.get('leverage')}\n"
159
+ f"is_cross: {req.get('is_cross')}\n"
160
+ f"builder_wallet: {HYPE_FEE_WALLET}\n"
161
+ f"builder_fee_tenths_bp: {req.get('builder_fee_tenths_bp') or '(from config/default)'}"
162
+ )
163
+ return {"summary": header + base + details}
164
+
165
+ if action == "cancel_order":
166
+ details = (
167
+ "\n\nCANCEL\n"
168
+ f"order_id: {req.get('order_id')}\n"
169
+ f"cancel_cloid: {req.get('cancel_cloid')}"
170
+ )
171
+ return {"summary": header + base + details}
172
+
173
+ if action == "update_leverage":
174
+ details = (
175
+ "\n\nLEVERAGE\n"
176
+ f"leverage: {req.get('leverage')}\n"
177
+ f"is_cross: {req.get('is_cross')}"
178
+ )
179
+ return {"summary": header + base + details}
180
+
181
+ if action == "withdraw":
182
+ details = f"\n\nWITHDRAW\namount_usdc: {req.get('amount_usdc')}"
183
+ return {"summary": header + base + details}
184
+
185
+ return {"summary": header + base}
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Any
5
+
6
+ from eth_account import Account
7
+
8
+ from wayfinder_paths.mcp.utils import find_wallet_by_label, load_config_json
9
+
10
+ # Known signing callback parameter names used by adapters
11
+ _SIGNING_CALLBACK_PARAMS = frozenset(
12
+ {
13
+ "strategy_wallet_signing_callback",
14
+ "sign_callback",
15
+ "signing_callback",
16
+ }
17
+ )
18
+
19
+
20
+ def _make_sign_callback(private_key: str):
21
+ account = Account.from_key(private_key)
22
+
23
+ async def sign_callback(transaction: dict) -> bytes:
24
+ signed = account.sign_transaction(transaction)
25
+ return signed.raw_transaction
26
+
27
+ return sign_callback
28
+
29
+
30
+ def _detect_callback_params(adapter_class: type) -> set[str]:
31
+ try:
32
+ sig = inspect.signature(adapter_class.__init__)
33
+ except (ValueError, TypeError):
34
+ return set()
35
+
36
+ return {
37
+ name
38
+ for name in sig.parameters
39
+ if name in _SIGNING_CALLBACK_PARAMS or name.endswith("_signing_callback")
40
+ }
41
+
42
+
43
+ def get_adapter[T](
44
+ adapter_class: type[T],
45
+ wallet_label: str | None = None,
46
+ *,
47
+ config_overrides: dict[str, Any] | None = None,
48
+ **kwargs: Any,
49
+ ) -> T:
50
+ config = load_config_json()
51
+
52
+ if config_overrides:
53
+ config = {**config, **config_overrides}
54
+
55
+ sign_callback = None
56
+ if wallet_label:
57
+ wallet = find_wallet_by_label(wallet_label)
58
+ if not wallet:
59
+ raise ValueError(
60
+ f"Wallet '{wallet_label}' not found in wallets.json. "
61
+ "Run 'just create-wallets' or check WALLETS_PATH."
62
+ )
63
+
64
+ private_key = wallet.get("private_key") or wallet.get("private_key_hex")
65
+ if not private_key:
66
+ raise ValueError(
67
+ f"Wallet '{wallet_label}' is missing private_key_hex. "
68
+ "Local signing requires a private key."
69
+ )
70
+
71
+ config["strategy_wallet"] = wallet
72
+ sign_callback = _make_sign_callback(private_key)
73
+
74
+ callback_params = _detect_callback_params(adapter_class)
75
+ adapter_kwargs: dict[str, Any] = {"config": config}
76
+
77
+ if sign_callback and callback_params:
78
+ for param_name in callback_params:
79
+ if param_name not in kwargs:
80
+ adapter_kwargs[param_name] = sign_callback
81
+
82
+ adapter_kwargs.update(kwargs)
83
+
84
+ return adapter_class(**adapter_kwargs)
@@ -0,0 +1,52 @@
1
+ """Wayfinder Paths MCP server (FastMCP).
2
+
3
+ Run locally (via Claude Code .mcp.json):
4
+ poetry run python -m wayfinder_paths.mcp.server
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+
11
+ from mcp.server.fastmcp import FastMCP
12
+
13
+ from wayfinder_paths.mcp.tools.balances import balances
14
+ from wayfinder_paths.mcp.tools.discovery import describe, discover
15
+ from wayfinder_paths.mcp.tools.execute import execute
16
+ from wayfinder_paths.mcp.tools.hyperliquid import hyperliquid, hyperliquid_execute
17
+ from wayfinder_paths.mcp.tools.quotes import quote_swap
18
+ from wayfinder_paths.mcp.tools.run_script import run_script
19
+ from wayfinder_paths.mcp.tools.strategies import run_strategy
20
+ from wayfinder_paths.mcp.tools.tokens import tokens
21
+ from wayfinder_paths.mcp.tools.wallets import wallets
22
+
23
+ mcp = FastMCP("wayfinder")
24
+
25
+ mcp.tool()(discover)
26
+ mcp.tool()(describe)
27
+ mcp.tool()(wallets)
28
+ mcp.tool()(tokens)
29
+ mcp.tool()(balances)
30
+ mcp.tool()(quote_swap)
31
+ mcp.tool()(hyperliquid)
32
+ mcp.tool()(hyperliquid_execute)
33
+ mcp.tool()(run_strategy)
34
+ mcp.tool()(run_script)
35
+ mcp.tool()(execute)
36
+
37
+
38
+ def main() -> None:
39
+ # FastMCP is sync, but our tools are async; the library handles that.
40
+ mcp.run()
41
+
42
+
43
+ if __name__ == "__main__":
44
+ # Some environments complain if an event loop is already running.
45
+ # FastMCP handles stdio and tool execution; we just start it.
46
+ try:
47
+ main()
48
+ except RuntimeError as exc:
49
+ if "asyncio.run()" in str(exc) and asyncio.get_event_loop().is_running():
50
+ main()
51
+ else:
52
+ raise
@@ -0,0 +1,195 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from wayfinder_paths.mcp.utils import repo_root
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _now_iso() -> str:
16
+ return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z")
17
+
18
+
19
+ def _runs_root() -> Path:
20
+ candidate = (os.getenv("WAYFINDER_RUNS_DIR") or ".wayfinder_runs").strip()
21
+ p = Path(candidate)
22
+ if not p.is_absolute():
23
+ p = repo_root() / p
24
+ return p.resolve(strict=False)
25
+
26
+
27
+ class WalletProfileStore:
28
+ SCHEMA_VERSION = "1.0"
29
+ MAX_TRANSACTIONS = 100 # Bound history size per wallet
30
+
31
+ def __init__(self, path: Path | None = None):
32
+ if path is None:
33
+ path = _runs_root() / "wallet_profiles.json"
34
+ self.path = path
35
+
36
+ @staticmethod
37
+ def default() -> WalletProfileStore:
38
+ return WalletProfileStore()
39
+
40
+ def _ensure_dir(self) -> None:
41
+ self.path.parent.mkdir(parents=True, exist_ok=True)
42
+
43
+ def _load(self) -> dict[str, Any]:
44
+ if not self.path.exists():
45
+ return {"schema_version": self.SCHEMA_VERSION, "profiles": {}}
46
+ try:
47
+ data = json.loads(self.path.read_text())
48
+ if not isinstance(data, dict):
49
+ return {"schema_version": self.SCHEMA_VERSION, "profiles": {}}
50
+ if not isinstance(data.get("profiles"), dict):
51
+ data["profiles"] = {}
52
+ return data
53
+ except Exception as exc:
54
+ logger.warning(f"Failed to load wallet profiles: {exc}")
55
+ return {"schema_version": self.SCHEMA_VERSION, "profiles": {}}
56
+
57
+ def _save(self, data: dict[str, Any]) -> None:
58
+ self._ensure_dir()
59
+ data["schema_version"] = self.SCHEMA_VERSION
60
+ self.path.write_text(json.dumps(data, indent=2, sort_keys=False))
61
+
62
+ def _normalize_address(self, address: str) -> str:
63
+ addr = str(address).strip().lower()
64
+ if addr.startswith("0x"):
65
+ return addr
66
+ return addr
67
+
68
+ def get_profile(self, address: str) -> dict[str, Any] | None:
69
+ data = self._load()
70
+ norm = self._normalize_address(address)
71
+ profile = data["profiles"].get(norm)
72
+ if profile:
73
+ return {"address": norm, **profile}
74
+ return None
75
+
76
+ def list_profiles(self) -> list[dict[str, Any]]:
77
+ data = self._load()
78
+ results: list[dict[str, Any]] = []
79
+ for addr, profile in data.get("profiles", {}).items():
80
+ protocols = list((profile.get("protocols") or {}).keys())
81
+ tx_count = len(profile.get("transactions") or [])
82
+ results.append(
83
+ {
84
+ "address": addr,
85
+ "label": profile.get("label"),
86
+ "protocols": protocols,
87
+ "protocol_count": len(protocols),
88
+ "transaction_count": tx_count,
89
+ "last_activity": profile.get("last_activity"),
90
+ }
91
+ )
92
+ return results
93
+
94
+ def get_protocols_for_wallet(self, address: str) -> list[str]:
95
+ profile = self.get_profile(address)
96
+ if not profile:
97
+ return []
98
+ return list((profile.get("protocols") or {}).keys())
99
+
100
+ def annotate(
101
+ self,
102
+ *,
103
+ address: str,
104
+ label: str | None = None,
105
+ protocol: str,
106
+ action: str,
107
+ tool: str,
108
+ status: str,
109
+ chain_id: int | None = None,
110
+ details: dict[str, Any] | None = None,
111
+ idempotency_key: str | None = None,
112
+ ) -> None:
113
+ data = self._load()
114
+ norm = self._normalize_address(address)
115
+ now = _now_iso()
116
+
117
+ if norm not in data["profiles"]:
118
+ data["profiles"][norm] = {
119
+ "label": label,
120
+ "protocols": {},
121
+ "transactions": [],
122
+ }
123
+
124
+ profile = data["profiles"][norm]
125
+
126
+ if label:
127
+ profile["label"] = label
128
+
129
+ if protocol not in profile["protocols"]:
130
+ profile["protocols"][protocol] = {
131
+ "first_seen": now,
132
+ "last_seen": now,
133
+ "interaction_count": 0,
134
+ "chains": [],
135
+ }
136
+
137
+ proto_info = profile["protocols"][protocol]
138
+ proto_info["last_seen"] = now
139
+ proto_info["interaction_count"] = proto_info.get("interaction_count", 0) + 1
140
+ if chain_id is not None:
141
+ chains = proto_info.get("chains") or []
142
+ if chain_id not in chains:
143
+ chains.append(chain_id)
144
+ proto_info["chains"] = chains
145
+
146
+ tx = {
147
+ "timestamp": now,
148
+ "protocol": protocol,
149
+ "action": action,
150
+ "tool": tool,
151
+ "status": status,
152
+ }
153
+ if chain_id is not None:
154
+ tx["chain_id"] = chain_id
155
+ if details:
156
+ tx["details"] = details
157
+ if idempotency_key:
158
+ tx["idempotency_key"] = idempotency_key
159
+
160
+ transactions = profile.get("transactions") or []
161
+ transactions.insert(0, tx)
162
+ if len(transactions) > self.MAX_TRANSACTIONS:
163
+ transactions = transactions[: self.MAX_TRANSACTIONS]
164
+ profile["transactions"] = transactions
165
+ profile["last_activity"] = now
166
+ self._save(data)
167
+
168
+ def annotate_safe(
169
+ self,
170
+ *,
171
+ address: str,
172
+ label: str | None = None,
173
+ protocol: str,
174
+ action: str,
175
+ tool: str,
176
+ status: str,
177
+ chain_id: int | None = None,
178
+ details: dict[str, Any] | None = None,
179
+ idempotency_key: str | None = None,
180
+ ) -> None:
181
+ # Best-effort: logs but doesn't raise on failure so annotation doesn't block main operation
182
+ try:
183
+ self.annotate(
184
+ address=address,
185
+ label=label,
186
+ protocol=protocol,
187
+ action=action,
188
+ tool=tool,
189
+ status=status,
190
+ chain_id=chain_id,
191
+ details=details,
192
+ idempotency_key=idempotency_key,
193
+ )
194
+ except Exception as exc:
195
+ logger.warning(f"Failed to annotate wallet profile: {exc}")
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sqlite3
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from wayfinder_paths.mcp.utils import repo_root
11
+
12
+
13
+ class _HexBytesEncoder(json.JSONEncoder):
14
+ """JSON encoder that handles HexBytes and other byte-like objects."""
15
+
16
+ def default(self, obj: Any) -> Any:
17
+ if hasattr(obj, "hex") and callable(obj.hex):
18
+ return obj.hex()
19
+ if isinstance(obj, bytes):
20
+ return obj.hex()
21
+ return super().default(obj)
22
+
23
+
24
+ class IdempotencyStore:
25
+ def __init__(self, db_path: Path):
26
+ self.db_path = db_path
27
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
28
+ self._init_db()
29
+
30
+ @staticmethod
31
+ def default() -> IdempotencyStore:
32
+ candidate = (
33
+ os.getenv("WAYFINDER_MCP_STATE_PATH") or ".cache/wayfinder_mcp.sqlite3"
34
+ )
35
+ path = Path(candidate)
36
+ if not path.is_absolute():
37
+ path = repo_root() / path
38
+ return IdempotencyStore(path)
39
+
40
+ def _connect(self) -> sqlite3.Connection:
41
+ conn = sqlite3.connect(str(self.db_path))
42
+ conn.row_factory = sqlite3.Row
43
+ return conn
44
+
45
+ def _init_db(self) -> None:
46
+ with self._connect() as conn:
47
+ conn.execute(
48
+ """
49
+ CREATE TABLE IF NOT EXISTS idempotency (
50
+ idempotency_key TEXT PRIMARY KEY,
51
+ created_at INTEGER NOT NULL,
52
+ request_json TEXT NOT NULL,
53
+ response_json TEXT NOT NULL
54
+ )
55
+ """
56
+ )
57
+ conn.commit()
58
+
59
+ def get(self, key: str) -> dict[str, Any] | None:
60
+ with self._connect() as conn:
61
+ row = conn.execute(
62
+ "SELECT response_json FROM idempotency WHERE idempotency_key = ?",
63
+ (key,),
64
+ ).fetchone()
65
+ if row is None:
66
+ return None
67
+ try:
68
+ parsed = json.loads(row["response_json"])
69
+ except Exception:
70
+ return None
71
+ return parsed if isinstance(parsed, dict) else None
72
+
73
+ def put(self, key: str, request: Any, response: Any) -> None:
74
+ with self._connect() as conn:
75
+ conn.execute(
76
+ """
77
+ INSERT OR REPLACE INTO idempotency
78
+ (idempotency_key, created_at, request_json, response_json)
79
+ VALUES
80
+ (?, ?, ?, ?)
81
+ """,
82
+ (
83
+ key,
84
+ int(time.time()),
85
+ json.dumps(request, sort_keys=True, cls=_HexBytesEncoder),
86
+ json.dumps(response, sort_keys=True, cls=_HexBytesEncoder),
87
+ ),
88
+ )
89
+ conn.commit()