wayfinder-paths 0.1.23__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/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/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.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- 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.24.dist-info}/LICENSE +0 -0
|
@@ -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()
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Tests for wayfinder_paths.mcp.scripting module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock, patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from wayfinder_paths.adapters.boros_adapter import BorosAdapter
|
|
10
|
+
from wayfinder_paths.adapters.hyperlend_adapter import HyperlendAdapter
|
|
11
|
+
from wayfinder_paths.adapters.moonwell_adapter import MoonwellAdapter
|
|
12
|
+
from wayfinder_paths.adapters.pendle_adapter import PendleAdapter
|
|
13
|
+
from wayfinder_paths.mcp.scripting import (
|
|
14
|
+
_detect_callback_params,
|
|
15
|
+
_make_sign_callback,
|
|
16
|
+
get_adapter,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestDetectCallbackParams:
|
|
21
|
+
"""Tests for _detect_callback_params function."""
|
|
22
|
+
|
|
23
|
+
def test_detects_strategy_wallet_signing_callback(self):
|
|
24
|
+
"""Should detect strategy_wallet_signing_callback parameter."""
|
|
25
|
+
|
|
26
|
+
class MockAdapter:
|
|
27
|
+
def __init__(self, config=None, strategy_wallet_signing_callback=None):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
result = _detect_callback_params(MockAdapter)
|
|
31
|
+
assert "strategy_wallet_signing_callback" in result
|
|
32
|
+
|
|
33
|
+
def test_detects_sign_callback(self):
|
|
34
|
+
"""Should detect sign_callback parameter."""
|
|
35
|
+
|
|
36
|
+
class MockAdapter:
|
|
37
|
+
def __init__(self, config=None, *, sign_callback=None):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
result = _detect_callback_params(MockAdapter)
|
|
41
|
+
assert "sign_callback" in result
|
|
42
|
+
|
|
43
|
+
def test_detects_custom_signing_callback_suffix(self):
|
|
44
|
+
"""Should detect params ending with _signing_callback."""
|
|
45
|
+
|
|
46
|
+
class MockAdapter:
|
|
47
|
+
def __init__(self, config=None, custom_signing_callback=None):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
result = _detect_callback_params(MockAdapter)
|
|
51
|
+
assert "custom_signing_callback" in result
|
|
52
|
+
|
|
53
|
+
def test_returns_empty_for_no_callback_params(self):
|
|
54
|
+
"""Should return empty set when no callback params found."""
|
|
55
|
+
|
|
56
|
+
class MockAdapter:
|
|
57
|
+
def __init__(self, config=None, timeout=30):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
result = _detect_callback_params(MockAdapter)
|
|
61
|
+
assert result == set()
|
|
62
|
+
|
|
63
|
+
def test_detects_multiple_callback_params(self):
|
|
64
|
+
"""Should detect all matching callback params."""
|
|
65
|
+
|
|
66
|
+
class MockAdapter:
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
config=None,
|
|
70
|
+
strategy_wallet_signing_callback=None,
|
|
71
|
+
sign_callback=None,
|
|
72
|
+
):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
result = _detect_callback_params(MockAdapter)
|
|
76
|
+
assert "strategy_wallet_signing_callback" in result
|
|
77
|
+
assert "sign_callback" in result
|
|
78
|
+
|
|
79
|
+
def test_real_moonwell_adapter(self):
|
|
80
|
+
"""Should detect callback param from MoonwellAdapter."""
|
|
81
|
+
result = _detect_callback_params(MoonwellAdapter)
|
|
82
|
+
assert "strategy_wallet_signing_callback" in result
|
|
83
|
+
|
|
84
|
+
def test_real_boros_adapter(self):
|
|
85
|
+
"""Should detect callback param from BorosAdapter."""
|
|
86
|
+
result = _detect_callback_params(BorosAdapter)
|
|
87
|
+
assert "sign_callback" in result
|
|
88
|
+
|
|
89
|
+
def test_real_hyperlend_adapter(self):
|
|
90
|
+
"""Should detect callback param from HyperlendAdapter."""
|
|
91
|
+
result = _detect_callback_params(HyperlendAdapter)
|
|
92
|
+
assert "strategy_wallet_signing_callback" in result
|
|
93
|
+
|
|
94
|
+
def test_real_pendle_adapter(self):
|
|
95
|
+
"""Should detect callback param from PendleAdapter."""
|
|
96
|
+
result = _detect_callback_params(PendleAdapter)
|
|
97
|
+
assert "strategy_wallet_signing_callback" in result
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestMakeSignCallback:
|
|
101
|
+
"""Tests for _make_sign_callback function."""
|
|
102
|
+
|
|
103
|
+
@pytest.mark.asyncio
|
|
104
|
+
async def test_creates_working_callback(self):
|
|
105
|
+
"""Should create a callback that signs transactions."""
|
|
106
|
+
# Test private key (DO NOT USE IN PRODUCTION)
|
|
107
|
+
test_pk = "0x" + "ab" * 32
|
|
108
|
+
|
|
109
|
+
callback = _make_sign_callback(test_pk)
|
|
110
|
+
|
|
111
|
+
# Create a minimal transaction
|
|
112
|
+
tx = {
|
|
113
|
+
"to": "0x" + "00" * 20,
|
|
114
|
+
"value": 0,
|
|
115
|
+
"gas": 21000,
|
|
116
|
+
"gasPrice": 1000000000,
|
|
117
|
+
"nonce": 0,
|
|
118
|
+
"chainId": 1,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
result = await callback(tx)
|
|
122
|
+
assert isinstance(result, bytes)
|
|
123
|
+
assert len(result) > 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class TestGetAdapter:
|
|
127
|
+
"""Tests for get_adapter function."""
|
|
128
|
+
|
|
129
|
+
def test_raises_when_wallet_not_found(self):
|
|
130
|
+
"""Should raise ValueError when wallet label not found."""
|
|
131
|
+
|
|
132
|
+
class MockAdapter:
|
|
133
|
+
def __init__(self, config=None):
|
|
134
|
+
pass
|
|
135
|
+
|
|
136
|
+
with patch(
|
|
137
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=None
|
|
138
|
+
):
|
|
139
|
+
with pytest.raises(ValueError, match="not found"):
|
|
140
|
+
get_adapter(MockAdapter, "nonexistent")
|
|
141
|
+
|
|
142
|
+
def test_raises_when_wallet_missing_private_key(self):
|
|
143
|
+
"""Should raise ValueError when wallet has no private key."""
|
|
144
|
+
|
|
145
|
+
class MockAdapter:
|
|
146
|
+
def __init__(self, config=None):
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
wallet = {"label": "test", "address": "0x" + "11" * 20}
|
|
150
|
+
|
|
151
|
+
with patch(
|
|
152
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
153
|
+
):
|
|
154
|
+
with pytest.raises(ValueError, match="missing private_key"):
|
|
155
|
+
get_adapter(MockAdapter, "test")
|
|
156
|
+
|
|
157
|
+
def test_works_without_wallet_for_readonly(self):
|
|
158
|
+
"""Should work without wallet for read-only adapters."""
|
|
159
|
+
|
|
160
|
+
class MockAdapter:
|
|
161
|
+
def __init__(self, config=None):
|
|
162
|
+
self.config = config
|
|
163
|
+
|
|
164
|
+
with patch(
|
|
165
|
+
"wayfinder_paths.mcp.scripting.load_config_json",
|
|
166
|
+
return_value={"foo": "bar"},
|
|
167
|
+
):
|
|
168
|
+
adapter = get_adapter(MockAdapter)
|
|
169
|
+
assert adapter.config == {"foo": "bar"}
|
|
170
|
+
|
|
171
|
+
def test_wires_strategy_wallet_into_config(self):
|
|
172
|
+
"""Should wire wallet into config['strategy_wallet']."""
|
|
173
|
+
|
|
174
|
+
class MockAdapter:
|
|
175
|
+
def __init__(self, config=None, strategy_wallet_signing_callback=None):
|
|
176
|
+
self.config = config
|
|
177
|
+
self.callback = strategy_wallet_signing_callback
|
|
178
|
+
|
|
179
|
+
wallet = {
|
|
180
|
+
"label": "main",
|
|
181
|
+
"address": "0x" + "11" * 20,
|
|
182
|
+
"private_key_hex": "0x" + "ab" * 32,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
with patch(
|
|
186
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
187
|
+
):
|
|
188
|
+
with patch(
|
|
189
|
+
"wayfinder_paths.mcp.scripting.load_config_json", return_value={}
|
|
190
|
+
):
|
|
191
|
+
adapter = get_adapter(MockAdapter, "main")
|
|
192
|
+
assert adapter.config["strategy_wallet"] == wallet
|
|
193
|
+
assert adapter.callback is not None
|
|
194
|
+
|
|
195
|
+
def test_applies_config_overrides(self):
|
|
196
|
+
"""Should merge config_overrides into loaded config."""
|
|
197
|
+
|
|
198
|
+
class MockAdapter:
|
|
199
|
+
def __init__(self, config=None):
|
|
200
|
+
self.config = config
|
|
201
|
+
|
|
202
|
+
with patch(
|
|
203
|
+
"wayfinder_paths.mcp.scripting.load_config_json",
|
|
204
|
+
return_value={"base": "value"},
|
|
205
|
+
):
|
|
206
|
+
adapter = get_adapter(MockAdapter, config_overrides={"override": "yes"})
|
|
207
|
+
assert adapter.config["base"] == "value"
|
|
208
|
+
assert adapter.config["override"] == "yes"
|
|
209
|
+
|
|
210
|
+
def test_passes_kwargs_to_adapter(self):
|
|
211
|
+
"""Should pass additional kwargs to adapter constructor."""
|
|
212
|
+
|
|
213
|
+
class MockAdapter:
|
|
214
|
+
def __init__(self, config=None, custom_arg=None):
|
|
215
|
+
self.config = config
|
|
216
|
+
self.custom_arg = custom_arg
|
|
217
|
+
|
|
218
|
+
with patch("wayfinder_paths.mcp.scripting.load_config_json", return_value={}):
|
|
219
|
+
adapter = get_adapter(MockAdapter, custom_arg="my_value")
|
|
220
|
+
assert adapter.custom_arg == "my_value"
|
|
221
|
+
|
|
222
|
+
def test_caller_kwargs_override_auto_wired_callback(self):
|
|
223
|
+
"""Should allow caller to override auto-wired signing callback."""
|
|
224
|
+
|
|
225
|
+
class MockAdapter:
|
|
226
|
+
def __init__(self, config=None, strategy_wallet_signing_callback=None):
|
|
227
|
+
self.callback = strategy_wallet_signing_callback
|
|
228
|
+
|
|
229
|
+
wallet = {
|
|
230
|
+
"label": "main",
|
|
231
|
+
"address": "0x" + "11" * 20,
|
|
232
|
+
"private_key_hex": "0x" + "ab" * 32,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
custom_callback = MagicMock()
|
|
236
|
+
|
|
237
|
+
with patch(
|
|
238
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
239
|
+
):
|
|
240
|
+
with patch(
|
|
241
|
+
"wayfinder_paths.mcp.scripting.load_config_json", return_value={}
|
|
242
|
+
):
|
|
243
|
+
adapter = get_adapter(
|
|
244
|
+
MockAdapter,
|
|
245
|
+
"main",
|
|
246
|
+
strategy_wallet_signing_callback=custom_callback,
|
|
247
|
+
)
|
|
248
|
+
assert adapter.callback is custom_callback
|
|
249
|
+
|
|
250
|
+
def test_integration_with_real_adapter_mocked_wallet(self):
|
|
251
|
+
"""Integration test with real adapter class and mocked wallet."""
|
|
252
|
+
wallet = {
|
|
253
|
+
"label": "main",
|
|
254
|
+
"address": "0x" + "11" * 20,
|
|
255
|
+
"private_key_hex": "0x" + "ab" * 32,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
with patch(
|
|
259
|
+
"wayfinder_paths.mcp.scripting.find_wallet_by_label", return_value=wallet
|
|
260
|
+
):
|
|
261
|
+
with patch(
|
|
262
|
+
"wayfinder_paths.mcp.scripting.load_config_json", return_value={}
|
|
263
|
+
):
|
|
264
|
+
adapter = get_adapter(MoonwellAdapter, "main")
|
|
265
|
+
assert isinstance(adapter, MoonwellAdapter)
|
|
266
|
+
assert adapter.strategy_wallet_signing_callback is not None
|
|
267
|
+
assert adapter.strategy_wallet_address == wallet["address"]
|
|
File without changes
|