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.
- wayfinder_paths/CONFIG_GUIDE.md +394 -0
- wayfinder_paths/__init__.py +21 -0
- wayfinder_paths/config.example.json +20 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +13 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +48 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +90 -0
- wayfinder_paths/core/clients/ClientManager.py +231 -0
- wayfinder_paths/core/clients/HyperlendClient.py +151 -0
- wayfinder_paths/core/clients/LedgerClient.py +222 -0
- wayfinder_paths/core/clients/PoolClient.py +96 -0
- wayfinder_paths/core/clients/SimulationClient.py +180 -0
- wayfinder_paths/core/clients/TokenClient.py +73 -0
- wayfinder_paths/core/clients/TransactionClient.py +47 -0
- wayfinder_paths/core/clients/WalletClient.py +90 -0
- wayfinder_paths/core/clients/WayfinderClient.py +258 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +295 -0
- wayfinder_paths/core/clients/sdk_example.py +115 -0
- wayfinder_paths/core/config.py +369 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +25 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/VaultJob.py +182 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +177 -0
- wayfinder_paths/core/services/local_evm_txn.py +429 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +183 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +165 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/run_strategy.py +409 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +160 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths/vaults/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/__init__.py +0 -0
- wayfinder_paths/vaults/adapters/balance_adapter/README.md +104 -0
- wayfinder_paths/vaults/adapters/balance_adapter/adapter.py +257 -0
- wayfinder_paths/vaults/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/vaults/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/vaults/adapters/balance_adapter/test_adapter.py +83 -0
- wayfinder_paths/vaults/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/vaults/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/brap_adapter/adapter.py +717 -0
- wayfinder_paths/vaults/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/vaults/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/brap_adapter/test_adapter.py +288 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/adapter.py +298 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/test_adapter.py +267 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/README.md +158 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/adapter.py +286 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/examples.json +131 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/vaults/adapters/ledger_adapter/test_adapter.py +202 -0
- wayfinder_paths/vaults/adapters/pool_adapter/README.md +218 -0
- wayfinder_paths/vaults/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/vaults/adapters/pool_adapter/adapter.py +289 -0
- wayfinder_paths/vaults/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/vaults/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/vaults/adapters/pool_adapter/test_adapter.py +222 -0
- wayfinder_paths/vaults/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/vaults/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/vaults/adapters/token_adapter/adapter.py +92 -0
- wayfinder_paths/vaults/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/vaults/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/adapters/token_adapter/test_adapter.py +135 -0
- wayfinder_paths/vaults/strategies/__init__.py +0 -0
- wayfinder_paths/vaults/strategies/config.py +85 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/README.md +99 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/examples.json +16 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/strategy.py +2328 -0
- wayfinder_paths/vaults/strategies/hyperlend_stable_yield_strategy/test_strategy.py +319 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/README.md +95 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/strategy.py +1684 -0
- wayfinder_paths/vaults/strategies/stablecoin_yield_strategy/test_strategy.py +350 -0
- wayfinder_paths/vaults/templates/adapter/README.md +105 -0
- wayfinder_paths/vaults/templates/adapter/adapter.py +26 -0
- wayfinder_paths/vaults/templates/adapter/examples.json +8 -0
- wayfinder_paths/vaults/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/vaults/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/vaults/templates/strategy/README.md +152 -0
- wayfinder_paths/vaults/templates/strategy/examples.json +11 -0
- wayfinder_paths/vaults/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/vaults/templates/strategy/strategy.py +57 -0
- wayfinder_paths/vaults/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths-0.1.1.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.1.dist-info/METADATA +727 -0
- wayfinder_paths-0.1.1.dist-info/RECORD +115 -0
- 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
|
+
)
|