wayfinder-paths 0.1.7__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 +399 -0
- wayfinder_paths/__init__.py +22 -0
- wayfinder_paths/abis/generic/erc20.json +383 -0
- wayfinder_paths/adapters/__init__.py +0 -0
- wayfinder_paths/adapters/balance_adapter/README.md +94 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
- wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
- wayfinder_paths/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
- wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
- wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
- wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
- wayfinder_paths/adapters/pool_adapter/README.md +206 -0
- wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
- wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
- wayfinder_paths/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
- wayfinder_paths/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
- wayfinder_paths/config.example.json +22 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +18 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/adapters/models.py +46 -0
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +109 -0
- wayfinder_paths/core/clients/ClientManager.py +210 -0
- wayfinder_paths/core/clients/HyperlendClient.py +192 -0
- wayfinder_paths/core/clients/LedgerClient.py +443 -0
- wayfinder_paths/core/clients/PoolClient.py +128 -0
- wayfinder_paths/core/clients/SimulationClient.py +192 -0
- wayfinder_paths/core/clients/TokenClient.py +89 -0
- wayfinder_paths/core/clients/TransactionClient.py +63 -0
- wayfinder_paths/core/clients/WalletClient.py +94 -0
- wayfinder_paths/core/clients/WayfinderClient.py +269 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +392 -0
- wayfinder_paths/core/clients/sdk_example.py +110 -0
- wayfinder_paths/core/config.py +458 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +42 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/StrategyJob.py +188 -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 +179 -0
- wayfinder_paths/core/services/local_evm_txn.py +430 -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 +280 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/strategies/descriptors.py +81 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +206 -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/policies/enso.py +17 -0
- wayfinder_paths/policies/erc20.py +34 -0
- wayfinder_paths/policies/evm.py +21 -0
- wayfinder_paths/policies/hyper_evm.py +19 -0
- wayfinder_paths/policies/hyperlend.py +12 -0
- wayfinder_paths/policies/hyperliquid.py +30 -0
- wayfinder_paths/policies/moonwell.py +54 -0
- wayfinder_paths/policies/prjx.py +30 -0
- wayfinder_paths/policies/util.py +27 -0
- wayfinder_paths/run_strategy.py +411 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +169 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/strategies/__init__.py +0 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/config.py +85 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
- wayfinder_paths/templates/adapter/README.md +105 -0
- wayfinder_paths/templates/adapter/adapter.py +26 -0
- wayfinder_paths/templates/adapter/examples.json +8 -0
- wayfinder_paths/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/templates/strategy/README.md +153 -0
- wayfinder_paths/templates/strategy/examples.json +11 -0
- wayfinder_paths/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/templates/strategy/strategy.py +57 -0
- wayfinder_paths/templates/strategy/test_strategy.py +197 -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-0.1.7.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
- wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
- wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Create a new strategy from template and generate a dedicated wallet for it.
|
|
4
|
+
|
|
5
|
+
This script:
|
|
6
|
+
1. Copies the strategy template to a new directory
|
|
7
|
+
2. Generates a wallet with label matching the strategy name
|
|
8
|
+
3. Updates the manifest with the strategy name
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from wayfinder_paths.core.utils.wallets import make_random_wallet, write_wallet_to_json
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def sanitize_name(name: str) -> str:
|
|
22
|
+
"""Convert a strategy name to a valid directory/identifier name."""
|
|
23
|
+
# Replace spaces and special chars with underscores, lowercase
|
|
24
|
+
name = re.sub(r"[^a-zA-Z0-9_-]", "_", name)
|
|
25
|
+
# Remove multiple underscores
|
|
26
|
+
name = re.sub(r"_+", "_", name)
|
|
27
|
+
# Remove leading/trailing underscores
|
|
28
|
+
name = name.strip("_")
|
|
29
|
+
return name.lower()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def update_manifest(manifest_path: Path, strategy_name: str, entrypoint: str) -> None:
|
|
33
|
+
"""Update manifest.yaml with strategy name and entrypoint."""
|
|
34
|
+
with open(manifest_path) as f:
|
|
35
|
+
manifest_data = yaml.safe_load(f)
|
|
36
|
+
|
|
37
|
+
manifest_data["name"] = strategy_name
|
|
38
|
+
manifest_data["entrypoint"] = entrypoint
|
|
39
|
+
|
|
40
|
+
with open(manifest_path, "w") as f:
|
|
41
|
+
yaml.dump(manifest_data, f, default_flow_style=False, sort_keys=False)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def update_strategy_file(strategy_path: Path, class_name: str) -> None:
|
|
45
|
+
"""Update strategy.py with new class name."""
|
|
46
|
+
content = strategy_path.read_text()
|
|
47
|
+
# Replace MyStrategy with the new class name
|
|
48
|
+
content = content.replace("MyStrategy", class_name)
|
|
49
|
+
# Replace my_strategy references in docstrings/comments
|
|
50
|
+
content = re.sub(
|
|
51
|
+
r"my_strategy", class_name.lower().replace("Strategy", ""), content
|
|
52
|
+
)
|
|
53
|
+
strategy_path.write_text(content)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
parser = argparse.ArgumentParser(
|
|
58
|
+
description="Create a new strategy from template with dedicated wallet"
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"name",
|
|
62
|
+
help="Strategy name (e.g., 'my_awesome_strategy' or 'My Awesome Strategy')",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--template-dir",
|
|
66
|
+
type=Path,
|
|
67
|
+
default=Path(__file__).parent.parent / "templates" / "strategy",
|
|
68
|
+
help="Path to strategy template directory",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--strategies-dir",
|
|
72
|
+
type=Path,
|
|
73
|
+
default=Path(__file__).parent.parent / "strategies",
|
|
74
|
+
help="Path to strategies directory",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--wallets-file",
|
|
78
|
+
type=Path,
|
|
79
|
+
default=Path(__file__).parent.parent.parent / "wallets.json",
|
|
80
|
+
help="Path to wallets.json file",
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument(
|
|
83
|
+
"--override",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Override existing strategy directory if it exists",
|
|
86
|
+
)
|
|
87
|
+
args = parser.parse_args()
|
|
88
|
+
|
|
89
|
+
# Sanitize name for directory
|
|
90
|
+
dir_name = sanitize_name(args.name)
|
|
91
|
+
strategy_dir = args.strategies_dir / dir_name
|
|
92
|
+
|
|
93
|
+
# Check if directory already exists
|
|
94
|
+
if strategy_dir.exists() and not args.override:
|
|
95
|
+
raise SystemExit(
|
|
96
|
+
f"Strategy directory already exists: {strategy_dir}\n"
|
|
97
|
+
"Use --override to replace it"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Check template exists
|
|
101
|
+
if not args.template_dir.exists():
|
|
102
|
+
raise SystemExit(f"Template directory not found: {args.template_dir}")
|
|
103
|
+
|
|
104
|
+
# Create strategy directory
|
|
105
|
+
if strategy_dir.exists():
|
|
106
|
+
print(f"Removing existing directory: {strategy_dir}")
|
|
107
|
+
shutil.rmtree(strategy_dir)
|
|
108
|
+
strategy_dir.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
print(f"Created strategy directory: {strategy_dir}")
|
|
110
|
+
|
|
111
|
+
# Copy template files
|
|
112
|
+
template_files = [
|
|
113
|
+
"strategy.py",
|
|
114
|
+
"manifest.yaml",
|
|
115
|
+
"test_strategy.py",
|
|
116
|
+
"examples.json",
|
|
117
|
+
"README.md",
|
|
118
|
+
]
|
|
119
|
+
for filename in template_files:
|
|
120
|
+
src = args.template_dir / filename
|
|
121
|
+
if src.exists():
|
|
122
|
+
dst = strategy_dir / filename
|
|
123
|
+
shutil.copy2(src, dst)
|
|
124
|
+
print(f" Copied {filename}")
|
|
125
|
+
|
|
126
|
+
# Generate class name from strategy name
|
|
127
|
+
# Convert "my_awesome_strategy" -> "MyAwesomeStrategy"
|
|
128
|
+
class_name = "".join(word.capitalize() for word in dir_name.split("_"))
|
|
129
|
+
if not class_name.endswith("Strategy"):
|
|
130
|
+
class_name += "Strategy"
|
|
131
|
+
|
|
132
|
+
# Update strategy.py with new class name
|
|
133
|
+
strategy_file = strategy_dir / "strategy.py"
|
|
134
|
+
if strategy_file.exists():
|
|
135
|
+
update_strategy_file(strategy_file, class_name)
|
|
136
|
+
print(f" Updated strategy.py with class name: {class_name}")
|
|
137
|
+
|
|
138
|
+
# Generate entrypoint path
|
|
139
|
+
entrypoint = f"strategies.{dir_name}.strategy.{class_name}"
|
|
140
|
+
|
|
141
|
+
# Update manifest with name (using directory name) and entrypoint
|
|
142
|
+
manifest_path = strategy_dir / "manifest.yaml"
|
|
143
|
+
if manifest_path.exists():
|
|
144
|
+
update_manifest(manifest_path, dir_name, entrypoint)
|
|
145
|
+
print(f" Updated manifest.yaml with name: {dir_name}")
|
|
146
|
+
|
|
147
|
+
# Generate wallet with label matching directory name (strategy identifier)
|
|
148
|
+
# If wallets.json doesn't exist, create it with a main wallet first
|
|
149
|
+
if not args.wallets_file.exists():
|
|
150
|
+
print(" Creating new wallets.json with main wallet...")
|
|
151
|
+
main_wallet = make_random_wallet()
|
|
152
|
+
main_wallet["label"] = "main"
|
|
153
|
+
write_wallet_to_json(
|
|
154
|
+
main_wallet,
|
|
155
|
+
out_dir=args.wallets_file.parent,
|
|
156
|
+
filename=args.wallets_file.name,
|
|
157
|
+
)
|
|
158
|
+
print(f" Generated main wallet: {main_wallet['address']}")
|
|
159
|
+
|
|
160
|
+
# Generate strategy wallet (will append to existing wallets.json)
|
|
161
|
+
wallet = make_random_wallet()
|
|
162
|
+
wallet["label"] = dir_name
|
|
163
|
+
write_wallet_to_json(
|
|
164
|
+
wallet, out_dir=args.wallets_file.parent, filename=args.wallets_file.name
|
|
165
|
+
)
|
|
166
|
+
print(f" Generated strategy wallet: {wallet['address']} (label: {dir_name})")
|
|
167
|
+
|
|
168
|
+
print("\n✅ Strategy created successfully!")
|
|
169
|
+
print(f" Directory: {strategy_dir}")
|
|
170
|
+
print(f" Name: {dir_name}")
|
|
171
|
+
print(f" Class: {class_name}")
|
|
172
|
+
print(f" Entrypoint: {entrypoint}")
|
|
173
|
+
print(f" Wallet: {wallet['address']}")
|
|
174
|
+
print("\nNext steps:")
|
|
175
|
+
print(f" 1. Edit {strategy_dir / 'strategy.py'} to implement your strategy")
|
|
176
|
+
print(f" 2. Update {strategy_dir / 'manifest.yaml'} with required adapters")
|
|
177
|
+
print(f" 3. Test with: just test-strategy {dir_name}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
strategy_w = label_to_wallet.get("strategy")
|
|
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/strategy variables
|
|
32
|
+
f.write(f"MAIN_WALLET_ADDRESS={main_w['address']}\n")
|
|
33
|
+
if strategy_w:
|
|
34
|
+
f.write(f"STRATEGY_WALLET_ADDRESS={strategy_w['address']}\n")
|
|
35
|
+
# Optional: expose strategy private key for local dev only
|
|
36
|
+
f.write(f"PRIVATE_KEY_STRATEGY={strategy_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
|
+
parser.add_argument(
|
|
66
|
+
"--default",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Create a default 'main' wallet if none exists (used by CI)",
|
|
69
|
+
)
|
|
70
|
+
args = parser.parse_args()
|
|
71
|
+
|
|
72
|
+
# --default is equivalent to -n 1 (create main wallet if needed)
|
|
73
|
+
if args.default and args.n == 0 and not args.label:
|
|
74
|
+
args.n = 1
|
|
75
|
+
|
|
76
|
+
args.out_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
+
|
|
78
|
+
# Load existing wallets
|
|
79
|
+
existing = load_wallets(args.out_dir, "wallets.json")
|
|
80
|
+
has_main = any(w.get("label") in ("main", "default") for w in existing)
|
|
81
|
+
|
|
82
|
+
rows: list[dict[str, str]] = []
|
|
83
|
+
index = 0
|
|
84
|
+
|
|
85
|
+
# Custom labeled wallet (e.g., for strategy name)
|
|
86
|
+
if args.label:
|
|
87
|
+
# Check if label already exists - if so, skip (don't create duplicate)
|
|
88
|
+
if any(w.get("label") == args.label for w in existing):
|
|
89
|
+
print(f"Wallet with label '{args.label}' already exists, skipping...")
|
|
90
|
+
else:
|
|
91
|
+
# Create wallet with specified label
|
|
92
|
+
w = make_random_wallet()
|
|
93
|
+
w["label"] = args.label
|
|
94
|
+
rows.append(w)
|
|
95
|
+
print(f"[{index}] {w['address']} (label: {args.label})")
|
|
96
|
+
write_wallet_to_json(w, out_dir=args.out_dir, filename="wallets.json")
|
|
97
|
+
if args.keystore_password:
|
|
98
|
+
ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
|
|
99
|
+
ks_path = args.out_dir / f"keystore_{w['address']}.json"
|
|
100
|
+
ks_path.write_text(json.dumps(ks))
|
|
101
|
+
index += 1
|
|
102
|
+
|
|
103
|
+
# If no wallets existed before, also create a "main" wallet
|
|
104
|
+
if not existing:
|
|
105
|
+
main_w = make_random_wallet()
|
|
106
|
+
main_w["label"] = "main"
|
|
107
|
+
rows.append(main_w)
|
|
108
|
+
print(f"[{index}] {main_w['address']} (main)")
|
|
109
|
+
write_wallet_to_json(
|
|
110
|
+
main_w, out_dir=args.out_dir, filename="wallets.json"
|
|
111
|
+
)
|
|
112
|
+
if args.keystore_password:
|
|
113
|
+
ks = to_keystore_json(
|
|
114
|
+
main_w["private_key_hex"], args.keystore_password
|
|
115
|
+
)
|
|
116
|
+
ks_path = args.out_dir / f"keystore_{main_w['address']}.json"
|
|
117
|
+
ks_path.write_text(json.dumps(ks))
|
|
118
|
+
index += 1
|
|
119
|
+
else:
|
|
120
|
+
# Create wallets with auto-generated labels: first one is "main" if main doesn't exist, others are "temporary_N"
|
|
121
|
+
if args.n == 0:
|
|
122
|
+
args.n = 1 # Default to 1 wallet if neither -n nor --label specified
|
|
123
|
+
|
|
124
|
+
# Find next temporary number
|
|
125
|
+
existing_labels = {
|
|
126
|
+
w.get("label", "")
|
|
127
|
+
for w in existing
|
|
128
|
+
if w.get("label", "").startswith("temporary_")
|
|
129
|
+
}
|
|
130
|
+
temp_numbers = set()
|
|
131
|
+
for label in existing_labels:
|
|
132
|
+
try:
|
|
133
|
+
num = int(label.replace("temporary_", ""))
|
|
134
|
+
temp_numbers.add(num)
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass
|
|
137
|
+
next_temp_num = 1
|
|
138
|
+
if temp_numbers:
|
|
139
|
+
next_temp_num = max(temp_numbers) + 1
|
|
140
|
+
|
|
141
|
+
for i in range(args.n):
|
|
142
|
+
w = make_random_wallet()
|
|
143
|
+
# Label first wallet as "main" if main doesn't exist, otherwise use temporary_N
|
|
144
|
+
if i == 0 and not has_main:
|
|
145
|
+
w["label"] = "main"
|
|
146
|
+
rows.append(w)
|
|
147
|
+
print(f"[{index}] {w['address']} (main)")
|
|
148
|
+
else:
|
|
149
|
+
# Find next available temporary number
|
|
150
|
+
while next_temp_num in temp_numbers:
|
|
151
|
+
next_temp_num += 1
|
|
152
|
+
w["label"] = f"temporary_{next_temp_num}"
|
|
153
|
+
temp_numbers.add(next_temp_num)
|
|
154
|
+
rows.append(w)
|
|
155
|
+
print(f"[{index}] {w['address']} (label: temporary_{next_temp_num})")
|
|
156
|
+
|
|
157
|
+
write_wallet_to_json(w, out_dir=args.out_dir, filename="wallets.json")
|
|
158
|
+
if args.keystore_password:
|
|
159
|
+
ks = to_keystore_json(w["private_key_hex"], args.keystore_password)
|
|
160
|
+
ks_path = args.out_dir / f"keystore_{w['address']}.json"
|
|
161
|
+
ks_path.write_text(json.dumps(ks))
|
|
162
|
+
index += 1
|
|
163
|
+
|
|
164
|
+
# Convenience outputs
|
|
165
|
+
write_env(rows, args.out_dir)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_wallets(path: Path) -> list[dict[str, Any]]:
|
|
11
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
12
|
+
if not isinstance(data, list):
|
|
13
|
+
raise ValueError(f"Expected a list in {path}")
|
|
14
|
+
return [w for w in data if isinstance(w, dict)]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_wallet(wallets: list[dict[str, Any]], label: str) -> dict[str, Any]:
|
|
18
|
+
for w in wallets:
|
|
19
|
+
if w.get("label") == label:
|
|
20
|
+
return w
|
|
21
|
+
raise ValueError(f"Wallet label not found in wallets.json: {label}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_strategy_class(strategy: str):
|
|
25
|
+
if strategy == "basis_trading_strategy":
|
|
26
|
+
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
27
|
+
BasisTradingStrategy,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return BasisTradingStrategy
|
|
31
|
+
|
|
32
|
+
if strategy == "hyperlend_stable_yield_strategy":
|
|
33
|
+
from wayfinder_paths.strategies.hyperlend_stable_yield_strategy.strategy import (
|
|
34
|
+
HyperlendStableYieldStrategy,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return HyperlendStableYieldStrategy
|
|
38
|
+
|
|
39
|
+
raise ValueError(f"Unknown strategy: {strategy}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def _run(args: argparse.Namespace) -> int:
|
|
43
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
44
|
+
wallets_path = (
|
|
45
|
+
Path(args.wallets).resolve() if args.wallets else repo_root / "wallets.json"
|
|
46
|
+
)
|
|
47
|
+
wallets = _load_wallets(wallets_path)
|
|
48
|
+
|
|
49
|
+
main_wallet = _find_wallet(wallets, args.main_wallet_label)
|
|
50
|
+
strategy_wallet = _find_wallet(wallets, args.strategy_wallet_label)
|
|
51
|
+
|
|
52
|
+
strategy_class = _get_strategy_class(args.strategy)
|
|
53
|
+
s = strategy_class(
|
|
54
|
+
{
|
|
55
|
+
"main_wallet": main_wallet,
|
|
56
|
+
"strategy_wallet": strategy_wallet,
|
|
57
|
+
},
|
|
58
|
+
simulation=args.simulation,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
await s.setup()
|
|
62
|
+
|
|
63
|
+
if args.command == "deposit":
|
|
64
|
+
ok, msg = await s.deposit(
|
|
65
|
+
main_token_amount=float(args.usdc), gas_token_amount=float(args.eth)
|
|
66
|
+
)
|
|
67
|
+
print(msg)
|
|
68
|
+
return 0 if ok else 1
|
|
69
|
+
|
|
70
|
+
if args.command == "update":
|
|
71
|
+
ok, msg = await s.update()
|
|
72
|
+
print(msg)
|
|
73
|
+
return 0 if ok else 1
|
|
74
|
+
|
|
75
|
+
if args.command == "withdraw":
|
|
76
|
+
ok, msg = await s.withdraw(
|
|
77
|
+
amount=float(args.amount) if args.amount is not None else None
|
|
78
|
+
)
|
|
79
|
+
print(msg)
|
|
80
|
+
return 0 if ok else 1
|
|
81
|
+
|
|
82
|
+
if args.command == "status":
|
|
83
|
+
st = await s.status()
|
|
84
|
+
print(json.dumps(st, indent=2, sort_keys=True))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
raise ValueError(f"Unknown command: {args.command}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> int:
|
|
91
|
+
p = argparse.ArgumentParser(
|
|
92
|
+
description="Run a strategy locally (deposit/update/withdraw/status)."
|
|
93
|
+
)
|
|
94
|
+
p.add_argument(
|
|
95
|
+
"--strategy",
|
|
96
|
+
default="basis_trading_strategy",
|
|
97
|
+
choices=["basis_trading_strategy", "hyperlend_stable_yield_strategy"],
|
|
98
|
+
)
|
|
99
|
+
p.add_argument(
|
|
100
|
+
"--wallets", default=None, help="Path to wallets.json (default: repo root)"
|
|
101
|
+
)
|
|
102
|
+
p.add_argument("--main-wallet-label", default="main")
|
|
103
|
+
p.add_argument("--strategy-wallet-label", default="basis_trading_strategy")
|
|
104
|
+
p.add_argument("--simulation", action="store_true")
|
|
105
|
+
|
|
106
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
107
|
+
|
|
108
|
+
dep = sub.add_parser("deposit")
|
|
109
|
+
dep.add_argument("--usdc", required=True, type=float)
|
|
110
|
+
dep.add_argument("--eth", default=0.0, type=float)
|
|
111
|
+
|
|
112
|
+
sub.add_parser("update")
|
|
113
|
+
|
|
114
|
+
wd = sub.add_parser("withdraw")
|
|
115
|
+
wd.add_argument("--amount", default=None, type=float)
|
|
116
|
+
|
|
117
|
+
sub.add_parser("status")
|
|
118
|
+
|
|
119
|
+
args = p.parse_args()
|
|
120
|
+
return asyncio.run(_run(args))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
raise SystemExit(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 / "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 / "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
|