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,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wallet Manager
|
|
3
|
+
|
|
4
|
+
Factory class for resolving and instantiating wallet providers based on configuration.
|
|
5
|
+
Provides convenience methods for config-based wallet provider resolution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from wayfinder_paths.core.services.base import EvmTxn
|
|
13
|
+
from wayfinder_paths.core.services.local_evm_txn import LocalEvmTxn
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WalletManager:
|
|
17
|
+
"""
|
|
18
|
+
Factory class for wallet providers.
|
|
19
|
+
|
|
20
|
+
Resolves appropriate wallet provider based on config, defaulting to LocalWalletProvider.
|
|
21
|
+
This is a convenience helper - adapters support direct injection, making this optional.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def get_provider(config: dict[str, Any] | None = None) -> EvmTxn:
|
|
26
|
+
"""
|
|
27
|
+
Get wallet provider based on configuration.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
config: Configuration dictionary. May contain wallet_type in wallet configs.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
WalletProvider instance. Defaults to LocalWalletProvider if no type specified.
|
|
34
|
+
"""
|
|
35
|
+
config = config or {}
|
|
36
|
+
wallet_type = config.get("wallet_type")
|
|
37
|
+
|
|
38
|
+
if not wallet_type:
|
|
39
|
+
main_wallet = config.get("main_wallet")
|
|
40
|
+
if isinstance(main_wallet, dict):
|
|
41
|
+
wallet_type = main_wallet.get("wallet_type")
|
|
42
|
+
|
|
43
|
+
if not wallet_type:
|
|
44
|
+
vault_wallet = config.get("vault_wallet")
|
|
45
|
+
if isinstance(vault_wallet, dict):
|
|
46
|
+
wallet_type = vault_wallet.get("wallet_type")
|
|
47
|
+
|
|
48
|
+
if not wallet_type or wallet_type == "local":
|
|
49
|
+
logger.debug("Using LocalWalletProvider (default)")
|
|
50
|
+
return LocalEvmTxn(config)
|
|
51
|
+
|
|
52
|
+
logger.warning(
|
|
53
|
+
f"Unknown wallet_type '{wallet_type}', defaulting to LocalWalletProvider. "
|
|
54
|
+
"To use custom wallet providers, inject them directly into adapters."
|
|
55
|
+
)
|
|
56
|
+
return LocalEvmTxn(config)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Wallet abstraction layer for supporting multiple wallet types."""
|
|
2
|
+
|
|
3
|
+
from wayfinder_paths.core.services.base import EvmTxn
|
|
4
|
+
from wayfinder_paths.core.services.local_evm_txn import LocalEvmTxn
|
|
5
|
+
from wayfinder_paths.core.wallets.WalletManager import WalletManager
|
|
6
|
+
|
|
7
|
+
__all__ = ["EvmTxn", "LocalEvmTxn", "WalletManager"]
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Strategy Runner
|
|
4
|
+
Main entry point for running vault strategies locally
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from wayfinder_paths.core.config import VaultConfig, load_config_from_env
|
|
16
|
+
from wayfinder_paths.core.engine.manifest import load_manifest, validate_manifest
|
|
17
|
+
from wayfinder_paths.core.engine.VaultJob import VaultJob
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_strategy(
|
|
21
|
+
strategy_name: str,
|
|
22
|
+
*,
|
|
23
|
+
strategy_config: dict | None = None,
|
|
24
|
+
simulation: bool = False,
|
|
25
|
+
api_key: str | None = None,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Dynamically load a strategy by name using its manifest
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
strategy_name: Name of the strategy to load (directory name in vaults/strategies/)
|
|
32
|
+
strategy_config: Configuration dict for the strategy
|
|
33
|
+
simulation: Enable simulation mode for testing
|
|
34
|
+
api_key: Optional API key for service account authentication
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Strategy instance
|
|
38
|
+
"""
|
|
39
|
+
# Find strategy manifest by scanning for manifest.yaml in the strategy directory
|
|
40
|
+
strategies_dir = Path(__file__).parent / "vaults" / "strategies"
|
|
41
|
+
strategy_dir = strategies_dir / strategy_name
|
|
42
|
+
manifest_path = strategy_dir / "manifest.yaml"
|
|
43
|
+
|
|
44
|
+
if not manifest_path.exists():
|
|
45
|
+
# List available strategies for better error message
|
|
46
|
+
available = []
|
|
47
|
+
if strategies_dir.exists():
|
|
48
|
+
for path in strategies_dir.iterdir():
|
|
49
|
+
if path.is_dir() and (path / "manifest.yaml").exists():
|
|
50
|
+
available.append(path.name)
|
|
51
|
+
available_str = ", ".join(available) if available else "none"
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Unknown strategy: {strategy_name}. Available strategies: {available_str}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Load manifest and use its entrypoint
|
|
57
|
+
manifest = load_manifest(str(manifest_path))
|
|
58
|
+
module_path, class_name = manifest.entrypoint.rsplit(".", 1)
|
|
59
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
60
|
+
strategy_class = getattr(module, class_name)
|
|
61
|
+
|
|
62
|
+
return strategy_class(
|
|
63
|
+
config=strategy_config, simulation=simulation, api_key=api_key
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_config(
|
|
68
|
+
config_path: str | None = None, strategy_name: str | None = None
|
|
69
|
+
) -> VaultConfig:
|
|
70
|
+
"""
|
|
71
|
+
Load configuration from file or environment
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
config_path: Optional path to config file
|
|
75
|
+
strategy_name: Optional strategy name for per-strategy wallet lookup
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
VaultConfig instance
|
|
79
|
+
"""
|
|
80
|
+
if config_path and Path(config_path).exists():
|
|
81
|
+
logger.info(f"Loading config from {config_path}")
|
|
82
|
+
with open(config_path) as f:
|
|
83
|
+
config_data = json.load(f)
|
|
84
|
+
return VaultConfig.from_dict(config_data, strategy_name=strategy_name)
|
|
85
|
+
else:
|
|
86
|
+
logger.info("Loading config from environment variables")
|
|
87
|
+
config = load_config_from_env()
|
|
88
|
+
if strategy_name:
|
|
89
|
+
config.strategy_config["_strategy_name"] = strategy_name
|
|
90
|
+
config.__post_init__()
|
|
91
|
+
return config
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def run_strategy(
|
|
95
|
+
strategy_name: str | None = None,
|
|
96
|
+
config_path: str | None = None,
|
|
97
|
+
action: str = "run",
|
|
98
|
+
manifest_path: str | None = None,
|
|
99
|
+
simulation: bool = False,
|
|
100
|
+
**kwargs,
|
|
101
|
+
):
|
|
102
|
+
"""
|
|
103
|
+
Run a vault strategy
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
strategy_name: Name of the strategy to run
|
|
107
|
+
config_path: Optional path to config file
|
|
108
|
+
action: Action to perform (run, deposit, withdraw, status)
|
|
109
|
+
**kwargs: Additional arguments for the action
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
# Determine strategy name for wallet lookup BEFORE loading config
|
|
113
|
+
# This ensures wallets are properly matched during config initialization
|
|
114
|
+
manifest = None
|
|
115
|
+
strategy_name_for_wallet = None
|
|
116
|
+
if manifest_path:
|
|
117
|
+
logger.debug(f"Loading strategy via manifest: {manifest_path}")
|
|
118
|
+
manifest = load_manifest(manifest_path)
|
|
119
|
+
validate_manifest(manifest)
|
|
120
|
+
# Extract directory name from manifest path for wallet lookup
|
|
121
|
+
# Use the directory name (strategy identifier) for wallet lookup
|
|
122
|
+
manifest_dir = Path(manifest_path).parent
|
|
123
|
+
strategies_dir = Path(__file__).parent / "vaults" / "strategies"
|
|
124
|
+
try:
|
|
125
|
+
# Try to get relative path - if it's under strategies_dir, use directory name
|
|
126
|
+
rel_path = manifest_dir.relative_to(strategies_dir)
|
|
127
|
+
strategy_name_for_wallet = (
|
|
128
|
+
rel_path.parts[0] if rel_path.parts else manifest_dir.name
|
|
129
|
+
)
|
|
130
|
+
except ValueError:
|
|
131
|
+
# Not under strategies_dir, fallback to directory name or manifest name
|
|
132
|
+
strategy_name_for_wallet = manifest_dir.name or manifest.name
|
|
133
|
+
else:
|
|
134
|
+
if not strategy_name:
|
|
135
|
+
raise ValueError("Either strategy_name or --manifest must be provided")
|
|
136
|
+
logger.debug(f"Loading strategy by name: {strategy_name}")
|
|
137
|
+
# Use directory name (strategy_name) directly for wallet lookup
|
|
138
|
+
strategy_name_for_wallet = strategy_name
|
|
139
|
+
|
|
140
|
+
# Load configuration with strategy name for wallet lookup
|
|
141
|
+
logger.debug(f"Config path provided: {config_path}")
|
|
142
|
+
config = load_config(config_path, strategy_name=strategy_name_for_wallet)
|
|
143
|
+
logger.debug(
|
|
144
|
+
"Loaded config: creds=%s wallets(main=%s vault=%s)",
|
|
145
|
+
"yes"
|
|
146
|
+
if (config.user.username and config.user.password)
|
|
147
|
+
or config.user.refresh_token
|
|
148
|
+
else "no",
|
|
149
|
+
(config.user.main_wallet_address or "none"),
|
|
150
|
+
(config.user.vault_wallet_address or "none"),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# Validate required configuration
|
|
154
|
+
# No user id required; authentication is via credentials or refresh token
|
|
155
|
+
|
|
156
|
+
# Load strategy with the enriched config
|
|
157
|
+
if manifest_path:
|
|
158
|
+
# Load strategy class from manifest
|
|
159
|
+
module_path, class_name = manifest.entrypoint.rsplit(".", 1)
|
|
160
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
161
|
+
strategy_class = getattr(module, class_name)
|
|
162
|
+
strategy = strategy_class(
|
|
163
|
+
config=config.strategy_config, simulation=simulation
|
|
164
|
+
)
|
|
165
|
+
logger.info(
|
|
166
|
+
f"Loaded strategy from manifest: {strategy_name_for_wallet or 'unnamed'}"
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
strategy = load_strategy(
|
|
170
|
+
strategy_name,
|
|
171
|
+
strategy_config=config.strategy_config,
|
|
172
|
+
simulation=simulation,
|
|
173
|
+
)
|
|
174
|
+
logger.info(f"Loaded strategy: {strategy.name}")
|
|
175
|
+
|
|
176
|
+
# Create vault job
|
|
177
|
+
vault_job = VaultJob(strategy, config)
|
|
178
|
+
|
|
179
|
+
# Setup vault job
|
|
180
|
+
logger.info("Setting up vault job...")
|
|
181
|
+
logger.debug(
|
|
182
|
+
"Auth mode: %s",
|
|
183
|
+
"credentials"
|
|
184
|
+
if (config.user.username and config.user.password)
|
|
185
|
+
or config.user.refresh_token
|
|
186
|
+
else "missing",
|
|
187
|
+
)
|
|
188
|
+
await vault_job.setup()
|
|
189
|
+
|
|
190
|
+
# Execute action
|
|
191
|
+
if action == "run":
|
|
192
|
+
logger.info("Starting continuous execution...")
|
|
193
|
+
await vault_job.run_continuous(interval_seconds=kwargs.get("interval"))
|
|
194
|
+
|
|
195
|
+
elif action == "deposit":
|
|
196
|
+
main_token_amount = kwargs.get("main_token_amount")
|
|
197
|
+
gas_token_amount = kwargs.get("gas_token_amount")
|
|
198
|
+
|
|
199
|
+
if main_token_amount is None and gas_token_amount is None:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
"Either main token amount or gas token amount required for deposit (use --main-token-amount and/or --gas-token-amount)"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Default to 0.0 if not provided
|
|
205
|
+
if main_token_amount is None:
|
|
206
|
+
main_token_amount = 0.0
|
|
207
|
+
if gas_token_amount is None:
|
|
208
|
+
gas_token_amount = 0.0
|
|
209
|
+
|
|
210
|
+
result = await vault_job.execute_strategy(
|
|
211
|
+
"deposit",
|
|
212
|
+
main_token_amount=main_token_amount,
|
|
213
|
+
gas_token_amount=gas_token_amount,
|
|
214
|
+
)
|
|
215
|
+
logger.info(f"Deposit result: {result}")
|
|
216
|
+
|
|
217
|
+
elif action == "withdraw":
|
|
218
|
+
amount = kwargs.get("amount")
|
|
219
|
+
result = await vault_job.execute_strategy("withdraw", amount=amount)
|
|
220
|
+
logger.info(f"Withdraw result: {result}")
|
|
221
|
+
|
|
222
|
+
elif action == "status":
|
|
223
|
+
result = await vault_job.execute_strategy("status")
|
|
224
|
+
logger.info(f"Status: {json.dumps(result, indent=2)}")
|
|
225
|
+
|
|
226
|
+
elif action == "update":
|
|
227
|
+
result = await vault_job.execute_strategy("update")
|
|
228
|
+
logger.info(f"Update result: {result}")
|
|
229
|
+
|
|
230
|
+
elif action == "partial-liquidate":
|
|
231
|
+
usd_value = kwargs.get("amount")
|
|
232
|
+
if not usd_value:
|
|
233
|
+
raise ValueError("Amount (USD value) required for partial-liquidate")
|
|
234
|
+
result = await vault_job.execute_strategy(
|
|
235
|
+
"partial_liquidate", usd_value=usd_value
|
|
236
|
+
)
|
|
237
|
+
logger.info(f"Partial liquidation result: {result}")
|
|
238
|
+
|
|
239
|
+
elif action == "policy":
|
|
240
|
+
policies: list[str] = []
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
spols = getattr(strategy, "policies", None)
|
|
244
|
+
if callable(spols):
|
|
245
|
+
result = spols() # type: ignore[misc]
|
|
246
|
+
if isinstance(result, list) and result:
|
|
247
|
+
policies = [p for p in result if isinstance(p, str)]
|
|
248
|
+
except Exception:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
if not policies and manifest and getattr(manifest, "permissions", None):
|
|
252
|
+
try:
|
|
253
|
+
mpol = manifest.permissions.get("policy")
|
|
254
|
+
if isinstance(mpol, str):
|
|
255
|
+
policies = [mpol]
|
|
256
|
+
elif isinstance(mpol, list):
|
|
257
|
+
policies = [p for p in mpol if isinstance(p, str)]
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
seen = set()
|
|
262
|
+
deduped: list[str] = []
|
|
263
|
+
for p in policies:
|
|
264
|
+
if p not in seen:
|
|
265
|
+
seen.add(p)
|
|
266
|
+
deduped.append(p)
|
|
267
|
+
|
|
268
|
+
# Get wallet_id from CLI arg, config, or leave as None
|
|
269
|
+
wallet_id = kwargs.get("wallet_id")
|
|
270
|
+
if not wallet_id:
|
|
271
|
+
wallet_id = config.strategy_config.get("wallet_id")
|
|
272
|
+
if not wallet_id:
|
|
273
|
+
wallet_id = config.system.wallet_id
|
|
274
|
+
|
|
275
|
+
# Render policies with wallet_id if available
|
|
276
|
+
if wallet_id:
|
|
277
|
+
rendered = [
|
|
278
|
+
p.replace("FORMAT_WALLET_ID", str(wallet_id)) for p in deduped
|
|
279
|
+
]
|
|
280
|
+
else:
|
|
281
|
+
rendered = deduped
|
|
282
|
+
logger.info(
|
|
283
|
+
"Policy rendering without wallet_id - policies contain FORMAT_WALLET_ID placeholder"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
logger.info(json.dumps({"policies": rendered}, indent=2))
|
|
287
|
+
|
|
288
|
+
elif action == "script":
|
|
289
|
+
duration = kwargs.get("duration") or 300
|
|
290
|
+
logger.info(f"Running script mode for {duration}s...")
|
|
291
|
+
task = asyncio.create_task(
|
|
292
|
+
vault_job.run_continuous(interval_seconds=kwargs.get("interval") or 60)
|
|
293
|
+
)
|
|
294
|
+
await asyncio.sleep(duration)
|
|
295
|
+
task.cancel()
|
|
296
|
+
try:
|
|
297
|
+
await task
|
|
298
|
+
except asyncio.CancelledError:
|
|
299
|
+
logger.info("Script mode execution completed")
|
|
300
|
+
|
|
301
|
+
else:
|
|
302
|
+
raise ValueError(f"Unknown action: {action}")
|
|
303
|
+
|
|
304
|
+
except KeyboardInterrupt:
|
|
305
|
+
logger.info("Shutting down...")
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Error: {e}")
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
finally:
|
|
310
|
+
if "vault_job" in locals():
|
|
311
|
+
await vault_job.stop()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def main():
|
|
315
|
+
"""Main entry point"""
|
|
316
|
+
parser = argparse.ArgumentParser(description="Run vault strategies")
|
|
317
|
+
parser.add_argument(
|
|
318
|
+
"strategy",
|
|
319
|
+
nargs="?",
|
|
320
|
+
help="Strategy to run (stablecoin_yield_strategy)",
|
|
321
|
+
)
|
|
322
|
+
parser.add_argument(
|
|
323
|
+
"--manifest",
|
|
324
|
+
help="Path to strategy manifest YAML (alternative to strategy name)",
|
|
325
|
+
)
|
|
326
|
+
parser.add_argument(
|
|
327
|
+
"--config", help="Path to config file (defaults to environment variables)"
|
|
328
|
+
)
|
|
329
|
+
parser.add_argument(
|
|
330
|
+
"--action",
|
|
331
|
+
default="run",
|
|
332
|
+
choices=[
|
|
333
|
+
"run",
|
|
334
|
+
"deposit",
|
|
335
|
+
"withdraw",
|
|
336
|
+
"status",
|
|
337
|
+
"update",
|
|
338
|
+
"policy",
|
|
339
|
+
"script",
|
|
340
|
+
"partial-liquidate",
|
|
341
|
+
],
|
|
342
|
+
help="Action to perform (default: run)",
|
|
343
|
+
)
|
|
344
|
+
parser.add_argument(
|
|
345
|
+
"--amount",
|
|
346
|
+
type=float,
|
|
347
|
+
help="Amount for withdraw/partial-liquidate actions",
|
|
348
|
+
)
|
|
349
|
+
parser.add_argument(
|
|
350
|
+
"--main-token-amount",
|
|
351
|
+
"--main_token_amount",
|
|
352
|
+
type=float,
|
|
353
|
+
dest="main_token_amount",
|
|
354
|
+
help="Main token amount for deposit action",
|
|
355
|
+
)
|
|
356
|
+
parser.add_argument(
|
|
357
|
+
"--gas-token-amount",
|
|
358
|
+
"--gas_token_amount",
|
|
359
|
+
type=float,
|
|
360
|
+
dest="gas_token_amount",
|
|
361
|
+
default=0.0,
|
|
362
|
+
help="Gas token amount for deposit action (default: 0.0)",
|
|
363
|
+
)
|
|
364
|
+
parser.add_argument(
|
|
365
|
+
"--interval",
|
|
366
|
+
type=int,
|
|
367
|
+
help="Update interval in seconds for continuous/script modes",
|
|
368
|
+
)
|
|
369
|
+
parser.add_argument(
|
|
370
|
+
"--duration", type=int, help="Duration in seconds for script action"
|
|
371
|
+
)
|
|
372
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"--simulation",
|
|
375
|
+
action="store_true",
|
|
376
|
+
help="Run in simulation mode (no real transactions)",
|
|
377
|
+
)
|
|
378
|
+
parser.add_argument(
|
|
379
|
+
"--wallet-id",
|
|
380
|
+
help="Wallet ID for policy rendering (replaces FORMAT_WALLET_ID in policies)",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
args = parser.parse_args()
|
|
384
|
+
|
|
385
|
+
# Configure logging
|
|
386
|
+
log_level = "DEBUG" if args.debug else "INFO"
|
|
387
|
+
logger.remove()
|
|
388
|
+
logger.add(sys.stderr, level=log_level)
|
|
389
|
+
|
|
390
|
+
# Run strategy
|
|
391
|
+
asyncio.run(
|
|
392
|
+
run_strategy(
|
|
393
|
+
strategy_name=args.strategy,
|
|
394
|
+
config_path=args.config,
|
|
395
|
+
action=args.action,
|
|
396
|
+
manifest_path=args.manifest,
|
|
397
|
+
amount=args.amount,
|
|
398
|
+
main_token_amount=args.main_token_amount,
|
|
399
|
+
gas_token_amount=args.gas_token_amount,
|
|
400
|
+
interval=args.interval,
|
|
401
|
+
duration=args.duration,
|
|
402
|
+
simulation=args.simulation,
|
|
403
|
+
wallet_id=getattr(args, "wallet_id", None),
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
main()
|
|
File without changes
|
|
@@ -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 / "vaults" / "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 / "vaults" / "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"vaults.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()
|