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,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