wayfinder-paths 0.1.1__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 (115) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +394 -0
  2. wayfinder_paths/__init__.py +21 -0
  3. wayfinder_paths/config.example.json +20 -0
  4. wayfinder_paths/conftest.py +31 -0
  5. wayfinder_paths/core/__init__.py +13 -0
  6. wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
  7. wayfinder_paths/core/adapters/__init__.py +5 -0
  8. wayfinder_paths/core/adapters/base.py +5 -0
  9. wayfinder_paths/core/clients/AuthClient.py +83 -0
  10. wayfinder_paths/core/clients/BRAPClient.py +90 -0
  11. wayfinder_paths/core/clients/ClientManager.py +231 -0
  12. wayfinder_paths/core/clients/HyperlendClient.py +151 -0
  13. wayfinder_paths/core/clients/LedgerClient.py +222 -0
  14. wayfinder_paths/core/clients/PoolClient.py +96 -0
  15. wayfinder_paths/core/clients/SimulationClient.py +180 -0
  16. wayfinder_paths/core/clients/TokenClient.py +73 -0
  17. wayfinder_paths/core/clients/TransactionClient.py +47 -0
  18. wayfinder_paths/core/clients/WalletClient.py +90 -0
  19. wayfinder_paths/core/clients/WayfinderClient.py +258 -0
  20. wayfinder_paths/core/clients/__init__.py +48 -0
  21. wayfinder_paths/core/clients/protocols.py +295 -0
  22. wayfinder_paths/core/clients/sdk_example.py +115 -0
  23. wayfinder_paths/core/config.py +369 -0
  24. wayfinder_paths/core/constants/__init__.py +26 -0
  25. wayfinder_paths/core/constants/base.py +25 -0
  26. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  27. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  28. wayfinder_paths/core/engine/VaultJob.py +182 -0
  29. wayfinder_paths/core/engine/__init__.py +5 -0
  30. wayfinder_paths/core/engine/manifest.py +97 -0
  31. wayfinder_paths/core/services/__init__.py +0 -0
  32. wayfinder_paths/core/services/base.py +177 -0
  33. wayfinder_paths/core/services/local_evm_txn.py +429 -0
  34. wayfinder_paths/core/services/local_token_txn.py +231 -0
  35. wayfinder_paths/core/services/web3_service.py +45 -0
  36. wayfinder_paths/core/settings.py +61 -0
  37. wayfinder_paths/core/strategies/Strategy.py +183 -0
  38. wayfinder_paths/core/strategies/__init__.py +5 -0
  39. wayfinder_paths/core/strategies/base.py +7 -0
  40. wayfinder_paths/core/utils/__init__.py +1 -0
  41. wayfinder_paths/core/utils/evm_helpers.py +165 -0
  42. wayfinder_paths/core/utils/wallets.py +77 -0
  43. wayfinder_paths/core/wallets/README.md +91 -0
  44. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  45. wayfinder_paths/core/wallets/__init__.py +7 -0
  46. wayfinder_paths/run_strategy.py +409 -0
  47. wayfinder_paths/scripts/__init__.py +0 -0
  48. wayfinder_paths/scripts/create_strategy.py +181 -0
  49. wayfinder_paths/scripts/make_wallets.py +160 -0
  50. wayfinder_paths/scripts/validate_manifests.py +213 -0
  51. wayfinder_paths/tests/__init__.py +0 -0
  52. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  53. wayfinder_paths/tests/test_test_coverage.py +212 -0
  54. wayfinder_paths/tests/test_utils.py +64 -0
  55. wayfinder_paths/vaults/__init__.py +0 -0
  56. wayfinder_paths/vaults/adapters/__init__.py +0 -0
  57. wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
  58. wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
  59. wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
  60. wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
  61. wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
  62. wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
  63. wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
  64. wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
  65. wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
  66. wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
  67. wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
  68. wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
  69. wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
  70. wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
  71. wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
  72. wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
  73. wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
  74. wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
  75. wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
  76. wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
  77. wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
  78. wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
  79. wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
  80. wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
  81. wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
  82. wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
  83. wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
  84. wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
  85. wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
  86. wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
  87. wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
  88. wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
  89. wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
  90. wayfinder_paths/vaults/strategies/__init__.py +0 -0
  91. wayfinder_paths/vaults/strategies/config.py +85 -0
  92. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
  93. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
  94. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  95. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
  96. wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
  97. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
  98. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
  99. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  100. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
  101. wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
  102. wayfinder_paths/vaults/templates/adapter/README.md +105 -0
  103. wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
  104. wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
  105. wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
  106. wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
  107. wayfinder_paths/vaults/templates/strategy/README.md +152 -0
  108. wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
  109. wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
  110. wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
  111. wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
  112. wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
  113. wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
  114. wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
  115. wayfinder_paths-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,160 @@
1
+ import argparse
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from eth_account import Account
6
+
7
+ from wayfinder_paths.core.utils.wallets import (
8
+ load_wallets,
9
+ make_random_wallet,
10
+ write_wallet_to_json,
11
+ )
12
+
13
+
14
+ def to_keystore_json(private_key_hex: str, password: str):
15
+ return Account.encrypt(private_key_hex, password)
16
+
17
+
18
+ def write_env(rows: list[dict[str, str]], out_dir: Path) -> None:
19
+ with open(out_dir / ".env.example", "w") as f:
20
+ if rows:
21
+ label_to_wallet = {r.get("label"): r for r in rows if r.get("label")}
22
+ main_w = (
23
+ label_to_wallet.get("main") or label_to_wallet.get("default") or rows[0]
24
+ )
25
+ vault_w = label_to_wallet.get("vault")
26
+
27
+ f.write("RPC_URL=https://rpc.ankr.com/eth\n")
28
+ # Back-compat defaults
29
+ f.write(f"PRIVATE_KEY={main_w['private_key_hex']}\n")
30
+ f.write(f"FROM_ADDRESS={main_w['address']}\n")
31
+ # Explicit main/vault variables
32
+ f.write(f"MAIN_WALLET_ADDRESS={main_w['address']}\n")
33
+ if vault_w:
34
+ f.write(f"VAULT_WALLET_ADDRESS={vault_w['address']}\n")
35
+ # Optional: expose vault private key for local dev only
36
+ f.write(f"PRIVATE_KEY_VAULT={vault_w['private_key_hex']}\n")
37
+
38
+
39
+ def main():
40
+ parser = argparse.ArgumentParser(description="Generate local dev wallets")
41
+ parser.add_argument(
42
+ "-n",
43
+ type=int,
44
+ default=0,
45
+ help="Number of wallets to create (ignored if --label is used)",
46
+ )
47
+ parser.add_argument(
48
+ "--out-dir",
49
+ type=Path,
50
+ default=Path("."),
51
+ help="Output directory for wallets.json (and .env/keystore)",
52
+ )
53
+ parser.add_argument(
54
+ "--keystore-password",
55
+ type=str,
56
+ default=None,
57
+ help="Optional password to write geth-compatible keystores",
58
+ )
59
+ parser.add_argument(
60
+ "--label",
61
+ type=str,
62
+ default=None,
63
+ help="Create a wallet with a custom label (e.g., strategy name). If not provided, auto-generates labels.",
64
+ )
65
+ args = parser.parse_args()
66
+
67
+ args.out_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ # Load existing wallets
70
+ existing = load_wallets(args.out_dir, "wallets.json")
71
+ has_main = any(w.get("label") in ("main", "default") for w in existing)
72
+
73
+ rows: list[dict[str, str]] = []
74
+ index = 0
75
+
76
+ # Custom labeled wallet (e.g., for strategy name)
77
+ if args.label:
78
+ # Check if label already exists - if so, skip (don't create duplicate)
79
+ if any(w.get("label") == args.label for w in existing):
80
+ print(f"Wallet with label '{args.label}' already exists, skipping...")
81
+ else:
82
+ # Create wallet with specified label
83
+ w = make_random_wallet()
84
+ w["label"] = args.label
85
+ rows.append(w)
86
+ print(f"[{index}] {w['address']} (label: {args.label})")
87
+ write_wallet_to_json(w, out_dir=args.out_dir, filename="wallets.json")
88
+ if args.keystore_password:
89
+ ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
90
+ ks_path = args.out_dir / f"keystore_{w['address']}.json"
91
+ ks_path.write_text(json.dumps(ks))
92
+ index += 1
93
+
94
+ # If no wallets existed before, also create a "main" wallet
95
+ if not existing:
96
+ main_w = make_random_wallet()
97
+ main_w["label"] = "main"
98
+ rows.append(main_w)
99
+ print(f"[{index}] {main_w['address']} (main)")
100
+ write_wallet_to_json(
101
+ main_w, out_dir=args.out_dir, filename="wallets.json"
102
+ )
103
+ if args.keystore_password:
104
+ ks = to_keystore_json(
105
+ main_w["private_key_hex"], args.keystore_password
106
+ )
107
+ ks_path = args.out_dir / f"keystore_{main_w['address']}.json"
108
+ ks_path.write_text(json.dumps(ks))
109
+ index += 1
110
+ else:
111
+ # Create wallets with auto-generated labels: first one is "main" if main doesn't exist, others are "temporary_N"
112
+ if args.n == 0:
113
+ args.n = 1 # Default to 1 wallet if neither -n nor --label specified
114
+
115
+ # Find next temporary number
116
+ existing_labels = {
117
+ w.get("label", "")
118
+ for w in existing
119
+ if w.get("label", "").startswith("temporary_")
120
+ }
121
+ temp_numbers = set()
122
+ for label in existing_labels:
123
+ try:
124
+ num = int(label.replace("temporary_", ""))
125
+ temp_numbers.add(num)
126
+ except ValueError:
127
+ pass
128
+ next_temp_num = 1
129
+ if temp_numbers:
130
+ next_temp_num = max(temp_numbers) + 1
131
+
132
+ for i in range(args.n):
133
+ w = make_random_wallet()
134
+ # Label first wallet as "main" if main doesn't exist, otherwise use temporary_N
135
+ if i == 0 and not has_main:
136
+ w["label"] = "main"
137
+ rows.append(w)
138
+ print(f"[{index}] {w['address']} (main)")
139
+ else:
140
+ # Find next available temporary number
141
+ while next_temp_num in temp_numbers:
142
+ next_temp_num += 1
143
+ w["label"] = f"temporary_{next_temp_num}"
144
+ temp_numbers.add(next_temp_num)
145
+ rows.append(w)
146
+ print(f"[{index}] {w['address']} (label: temporary_{next_temp_num})")
147
+
148
+ write_wallet_to_json(w, out_dir=args.out_dir, filename="wallets.json")
149
+ if args.keystore_password:
150
+ ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
151
+ ks_path = args.out_dir / f"keystore_{w['address']}.json"
152
+ ks_path.write_text(json.dumps(ks))
153
+ index += 1
154
+
155
+ # Convenience outputs
156
+ write_env(rows, args.out_dir)
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Manifest Validator
4
+
5
+ Validates all adapter and strategy manifests in the repository.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from loguru import logger
12
+
13
+ # Add parent to path for imports
14
+ sys.path.insert(0, str(Path(__file__).parent.parent))
15
+
16
+ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
17
+ from wayfinder_paths.core.engine.manifest import (
18
+ load_adapter_manifest,
19
+ load_strategy_manifest,
20
+ )
21
+ from wayfinder_paths.core.strategies.Strategy import Strategy
22
+
23
+
24
+ def verify_entrypoint(entrypoint: str) -> tuple[bool, str | None]:
25
+ """Verify entrypoint is importable. Returns (success, error_message)."""
26
+ try:
27
+ module_path, class_name = entrypoint.rsplit(".", 1)
28
+ module = __import__(module_path, fromlist=[class_name], level=0)
29
+ getattr(module, class_name) # Verify class exists
30
+ return True, None
31
+ except ImportError as e:
32
+ return False, f"Import error: {str(e)}"
33
+ except AttributeError as e:
34
+ return False, f"Class not found: {str(e)}"
35
+ except Exception as e:
36
+ return False, f"Unexpected error: {str(e)}"
37
+
38
+
39
+ def verify_adapter_class(entrypoint: str) -> tuple[bool, str | None]:
40
+ """Verify entrypoint is an adapter class."""
41
+ valid, error = verify_entrypoint(entrypoint)
42
+ if not valid:
43
+ return False, error
44
+
45
+ try:
46
+ module_path, class_name = entrypoint.rsplit(".", 1)
47
+ module = __import__(module_path, fromlist=[class_name], level=0)
48
+ adapter_class = getattr(module, class_name)
49
+
50
+ if not issubclass(adapter_class, BaseAdapter):
51
+ return False, f"{class_name} is not a BaseAdapter"
52
+ return True, None
53
+ except Exception as e:
54
+ return False, f"Error verifying adapter: {str(e)}"
55
+
56
+
57
+ def verify_strategy_class(entrypoint: str) -> tuple[bool, str | None]:
58
+ """Verify entrypoint is a strategy class."""
59
+ valid, error = verify_entrypoint(entrypoint)
60
+ if not valid:
61
+ return False, error
62
+
63
+ try:
64
+ module_path, class_name = entrypoint.rsplit(".", 1)
65
+ module = __import__(module_path, fromlist=[class_name], level=0)
66
+ strategy_class = getattr(module, class_name)
67
+
68
+ if not issubclass(strategy_class, Strategy):
69
+ return False, f"{class_name} is not a Strategy"
70
+ return True, None
71
+ except Exception as e:
72
+ return False, f"Error verifying strategy: {str(e)}"
73
+
74
+
75
+ # Capabilities are only declared in manifest - no code validation needed
76
+ # Manifest is the single source of truth for capabilities
77
+
78
+
79
+ def verify_dependencies(dependencies: list[str]) -> tuple[bool, list[str]]:
80
+ """Verify dependencies are importable. Returns (valid, error_messages)."""
81
+ errors = []
82
+
83
+ for dep in dependencies:
84
+ # Try to import from core.clients
85
+ try:
86
+ __import__(f"core.clients.{dep}", fromlist=[dep], level=0)
87
+ except ImportError:
88
+ errors.append(f"Dependency '{dep}' not found in core.clients")
89
+ except Exception as e:
90
+ errors.append(f"Error importing dependency '{dep}': {str(e)}")
91
+
92
+ return len(errors) == 0, errors
93
+
94
+
95
+ def validate_adapter_manifest(manifest_path: str) -> tuple[bool, list[str]]:
96
+ """Validate adapter manifest. Returns (valid, error_messages)."""
97
+ errors = []
98
+
99
+ try:
100
+ manifest = load_adapter_manifest(manifest_path)
101
+ except Exception as e:
102
+ return False, [f"Schema error: {str(e)}"]
103
+
104
+ # Verify entrypoint
105
+ valid, error = verify_adapter_class(manifest.entrypoint)
106
+ if not valid:
107
+ errors.append(f"Entrypoint validation failed: {error}")
108
+ return False, errors
109
+
110
+ # Verify dependencies
111
+ valid, dep_errors = verify_dependencies(manifest.dependencies)
112
+ if not valid:
113
+ errors.extend(dep_errors)
114
+
115
+ return len(errors) == 0, errors
116
+
117
+
118
+ def validate_strategy_manifest(manifest_path: str) -> tuple[bool, list[str]]:
119
+ """Validate strategy manifest. Returns (valid, error_messages)."""
120
+ errors = []
121
+
122
+ try:
123
+ manifest = load_strategy_manifest(manifest_path)
124
+ except Exception as e:
125
+ return False, [f"Schema error: {str(e)}"]
126
+
127
+ # Verify entrypoint
128
+ valid, error = verify_strategy_class(manifest.entrypoint)
129
+ if not valid:
130
+ errors.append(f"Entrypoint validation failed: {error}")
131
+ return False, errors
132
+
133
+ # Permissions are already validated by Pydantic model
134
+
135
+ return len(errors) == 0, errors
136
+
137
+
138
+ def find_adapter_manifests() -> list[Path]:
139
+ """Find all adapter manifest files."""
140
+ manifests = []
141
+ adapter_dir = Path(__file__).parent.parent / "vaults" / "adapters"
142
+ if adapter_dir.exists():
143
+ for adapter_path in adapter_dir.iterdir():
144
+ manifest_path = adapter_path / "manifest.yaml"
145
+ if manifest_path.exists():
146
+ manifests.append(manifest_path)
147
+ return manifests
148
+
149
+
150
+ def find_strategy_manifests() -> list[Path]:
151
+ """Find all strategy manifest files."""
152
+ manifests = []
153
+ strategy_dir = Path(__file__).parent.parent / "vaults" / "strategies"
154
+ if strategy_dir.exists():
155
+ for strategy_path in strategy_dir.iterdir():
156
+ manifest_path = strategy_path / "manifest.yaml"
157
+ if manifest_path.exists():
158
+ manifests.append(manifest_path)
159
+ return manifests
160
+
161
+
162
+ def main() -> int:
163
+ """Main validation function. Returns 0 on success, 1 on failure."""
164
+ logger.info("Validating all manifests...")
165
+
166
+ all_valid = True
167
+ error_count = 0
168
+
169
+ # Validate adapter manifests
170
+ logger.info("\n=== Validating Adapter Manifests ===")
171
+ adapter_manifests = find_adapter_manifests()
172
+ for manifest_path in sorted(adapter_manifests):
173
+ logger.info(f"Validating {manifest_path}...")
174
+ valid, errors = validate_adapter_manifest(str(manifest_path))
175
+ if valid:
176
+ logger.success(f"✅ {manifest_path.name} - Valid")
177
+ else:
178
+ logger.error(f"❌ {manifest_path.name} - Invalid")
179
+ for error in errors:
180
+ logger.error(f" {error}")
181
+ all_valid = False
182
+ error_count += len(errors)
183
+
184
+ # Validate strategy manifests
185
+ logger.info("\n=== Validating Strategy Manifests ===")
186
+ strategy_manifests = find_strategy_manifests()
187
+ for manifest_path in sorted(strategy_manifests):
188
+ logger.info(f"Validating {manifest_path}...")
189
+ valid, errors = validate_strategy_manifest(str(manifest_path))
190
+ if valid:
191
+ logger.success(f"✅ {manifest_path.name} - Valid")
192
+ else:
193
+ logger.error(f"❌ {manifest_path.name} - Invalid")
194
+ for error in errors:
195
+ logger.error(f" {error}")
196
+ all_valid = False
197
+ error_count += len(errors)
198
+
199
+ # Summary
200
+ logger.info("\n=== Summary ===")
201
+ if all_valid:
202
+ logger.success(
203
+ f"✅ All manifests valid! ({len(adapter_manifests)} adapters, "
204
+ f"{len(strategy_manifests)} strategies)"
205
+ )
206
+ return 0
207
+ else:
208
+ logger.error(f"❌ Validation failed with {error_count} error(s)")
209
+ return 1
210
+
211
+
212
+ if __name__ == "__main__":
213
+ sys.exit(main())
File without changes
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
4
+ from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
5
+
6
+
7
+ class FakeAdapter(BaseAdapter):
8
+ adapter_type = "FAKE"
9
+
10
+ async def connect(self) -> bool:
11
+ return True
12
+
13
+ async def get_balance(self, asset: str):
14
+ return {"asset": asset, "amount": 100}
15
+
16
+
17
+ class FakeStrategy(Strategy):
18
+ name = "Fake Strategy"
19
+
20
+ async def deposit(self, amount: float = 0) -> StatusTuple:
21
+ return (True, "deposited")
22
+
23
+ async def update(self) -> StatusTuple:
24
+ return (True, "updated")
25
+
26
+ async def withdraw(self, amount: float = 0) -> StatusTuple:
27
+ return (True, "withdrew")
28
+
29
+ async def status(self) -> StatusDict:
30
+ return {"total_earned": 0.0, "strategy_status": {"ok": True}}
31
+
32
+ @staticmethod
33
+ def policy() -> str:
34
+ return "wallet.id == 'TEST'"
35
+
36
+
37
+ @pytest.mark.asyncio
38
+ async def test_smoke_deposit_update_withdraw_status():
39
+ s = FakeStrategy()
40
+ s.register_adapters([FakeAdapter("fake")])
41
+ ok, msg = await s.deposit(amount=1)
42
+ assert ok
43
+ ok, msg = await s.update()
44
+ assert ok
45
+ ok, msg = await s.withdraw(amount=1)
46
+ assert ok
47
+ st = await s.status()
48
+ assert "total_earned" in st
@@ -0,0 +1,212 @@
1
+ """Test to ensure all adapters and strategies have corresponding test files."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+
8
+ def test_all_adapters_have_tests():
9
+ """Verify that all adapters have a test_adapter.py file."""
10
+ adapters_dir = Path(__file__).parent.parent / "vaults" / "adapters"
11
+
12
+ if not adapters_dir.exists():
13
+ pytest.skip("Adapters directory not found")
14
+
15
+ missing_tests = []
16
+
17
+ # Find all adapter directories (directories containing adapter.py)
18
+ for adapter_dir in adapters_dir.iterdir():
19
+ if not adapter_dir.is_dir() or adapter_dir.name.startswith("_"):
20
+ continue
21
+
22
+ adapter_py = adapter_dir / "adapter.py"
23
+ test_adapter_py = adapter_dir / "test_adapter.py"
24
+
25
+ # If adapter.py exists, test_adapter_py must exist
26
+ if adapter_py.exists() and not test_adapter_py.exists():
27
+ missing_tests.append(adapter_dir.name)
28
+
29
+ if missing_tests:
30
+ pytest.fail(
31
+ f"The following adapters are missing test files:\n"
32
+ f"{', '.join(missing_tests)}\n"
33
+ f"Please create test_adapter.py for each adapter."
34
+ )
35
+
36
+
37
+ def test_all_strategies_have_tests():
38
+ """Verify that all strategies have a test_strategy.py file."""
39
+ strategies_dir = Path(__file__).parent.parent / "vaults" / "strategies"
40
+
41
+ if not strategies_dir.exists():
42
+ pytest.skip("Strategies directory not found")
43
+
44
+ missing_tests = []
45
+
46
+ # Find all strategy directories (directories containing strategy.py)
47
+ for strategy_dir in strategies_dir.iterdir():
48
+ if not strategy_dir.is_dir() or strategy_dir.name.startswith("_"):
49
+ continue
50
+
51
+ strategy_py = strategy_dir / "strategy.py"
52
+ test_strategy_py = strategy_dir / "test_strategy.py"
53
+
54
+ # If strategy.py exists, test_strategy.py must exist
55
+ if strategy_py.exists() and not test_strategy_py.exists():
56
+ missing_tests.append(strategy_dir.name)
57
+
58
+ if missing_tests:
59
+ pytest.fail(
60
+ f"The following strategies are missing test files:\n"
61
+ f"{', '.join(missing_tests)}\n"
62
+ f"Please create test_strategy.py for each strategy."
63
+ )
64
+
65
+
66
+ def test_all_strategies_have_examples_json():
67
+ """Verify that all strategies have an examples.json file (REQUIRED)."""
68
+ strategies_dir = Path(__file__).parent.parent / "vaults" / "strategies"
69
+
70
+ if not strategies_dir.exists():
71
+ pytest.skip("Strategies directory not found")
72
+
73
+ missing_examples = []
74
+
75
+ # Find all strategy directories (directories containing strategy.py)
76
+ for strategy_dir in strategies_dir.iterdir():
77
+ if not strategy_dir.is_dir() or strategy_dir.name.startswith("_"):
78
+ continue
79
+
80
+ strategy_py = strategy_dir / "strategy.py"
81
+ examples_json = strategy_dir / "examples.json"
82
+
83
+ # If strategy.py exists, examples.json must exist
84
+ if strategy_py.exists() and not examples_json.exists():
85
+ missing_examples.append(strategy_dir.name)
86
+
87
+ if missing_examples:
88
+ pytest.fail(
89
+ f"The following strategies are missing examples.json files:\n"
90
+ f"{', '.join(missing_examples)}\n"
91
+ f"examples.json is REQUIRED for all strategies.\n"
92
+ f"See TESTING.md for the required structure."
93
+ )
94
+
95
+
96
+ def test_strategy_tests_use_examples_json():
97
+ """Verify that strategy test files load examples.json using the shared utility."""
98
+ strategies_dir = Path(__file__).parent.parent / "vaults" / "strategies"
99
+
100
+ if not strategies_dir.exists():
101
+ pytest.skip("Strategies directory not found")
102
+
103
+ violations = []
104
+
105
+ # Find all strategy test files
106
+ for strategy_dir in strategies_dir.iterdir():
107
+ if not strategy_dir.is_dir() or strategy_dir.name.startswith("_"):
108
+ continue
109
+
110
+ test_file = strategy_dir / "test_strategy.py"
111
+ if not test_file.exists():
112
+ continue
113
+
114
+ # Read the test file content
115
+ try:
116
+ content = test_file.read_text()
117
+
118
+ # Check if it imports load_strategy_examples from tests.test_utils
119
+ # (wayfinder_paths/ is added to path by conftest.py or inline)
120
+ has_import = (
121
+ "from tests.test_utils import load_strategy_examples" in content
122
+ or "from wayfinder_paths.tests.test_utils import load_strategy_examples"
123
+ in content # alternative
124
+ or "import tests.test_utils" in content
125
+ or "import wayfinder_paths.tests.test_utils" in content # alternative
126
+ or (
127
+ "tests.test_utils" in content
128
+ and "load_strategy_examples" in content
129
+ ) # fallback importlib pattern
130
+ )
131
+
132
+ # Check if it calls load_strategy_examples
133
+ has_usage = "load_strategy_examples" in content
134
+
135
+ # If it doesn't use the shared utility, check for alternative patterns
136
+ if not (has_import and has_usage):
137
+ # Check for hardcoded examples.json loading (old pattern)
138
+ has_hardcoded = (
139
+ 'Path(__file__).parent / "examples.json"' in content
140
+ or "examples.json" in content
141
+ ) and "load_strategy_examples" not in content
142
+
143
+ if has_hardcoded:
144
+ violations.append(
145
+ f"{strategy_dir.name}: Uses hardcoded examples.json loading "
146
+ f"instead of load_strategy_examples() from tests.test_utils"
147
+ )
148
+ elif has_usage and not has_import:
149
+ violations.append(
150
+ f"{strategy_dir.name}: Uses load_strategy_examples but missing import"
151
+ )
152
+ elif not has_usage:
153
+ violations.append(
154
+ f"{strategy_dir.name}: Test file does not appear to load examples.json"
155
+ )
156
+ except Exception as e:
157
+ violations.append(f"{strategy_dir.name}: Error reading test file: {e}")
158
+
159
+ if violations:
160
+ pytest.fail(
161
+ f"The following strategy tests need to use load_strategy_examples():\n"
162
+ f"{chr(10).join(violations)}\n"
163
+ f"All strategy tests MUST use load_strategy_examples() from tests.test_utils.\n"
164
+ f"See TESTING.md for examples."
165
+ )
166
+
167
+
168
+ def test_strategy_examples_have_smoke():
169
+ """Verify that all strategy examples.json files have a 'smoke' entry."""
170
+ strategies_dir = Path(__file__).parent.parent / "vaults" / "strategies"
171
+
172
+ if not strategies_dir.exists():
173
+ pytest.skip("Strategies directory not found")
174
+
175
+ import json
176
+
177
+ missing_smoke = []
178
+
179
+ # Find all strategy directories
180
+ for strategy_dir in strategies_dir.iterdir():
181
+ if not strategy_dir.is_dir() or strategy_dir.name.startswith("_"):
182
+ continue
183
+
184
+ strategy_py = strategy_dir / "strategy.py"
185
+ examples_json = strategy_dir / "examples.json"
186
+
187
+ # Only check strategies that exist
188
+ if not strategy_py.exists():
189
+ continue
190
+
191
+ if not examples_json.exists():
192
+ # This will be caught by test_all_strategies_have_examples_json
193
+ continue
194
+
195
+ try:
196
+ with open(examples_json) as f:
197
+ examples = json.load(f)
198
+
199
+ if "smoke" not in examples:
200
+ missing_smoke.append(strategy_dir.name)
201
+ except json.JSONDecodeError:
202
+ missing_smoke.append(f"{strategy_dir.name} (invalid JSON)")
203
+ except Exception as e:
204
+ missing_smoke.append(f"{strategy_dir.name} (error: {e})")
205
+
206
+ if missing_smoke:
207
+ pytest.fail(
208
+ f"The following strategies' examples.json are missing 'smoke' entry:\n"
209
+ f"{', '.join(missing_smoke)}\n"
210
+ f"All strategies MUST have a 'smoke' example in examples.json.\n"
211
+ f"See TESTING.md for the required structure."
212
+ )