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.
- wayfinder_paths/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- 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/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -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/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- 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 +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- 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/adapter.py +14 -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 +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- 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/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- 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/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- 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/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- 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 +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {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()
|