wayfinder-paths 0.1.3__py3-none-any.whl → 0.1.5__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 +37 -32
- wayfinder_paths/__init__.py +3 -3
- wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/README.md +12 -12
- wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/adapter.py +12 -11
- wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/examples.json +1 -1
- wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/manifest.yaml +1 -1
- wayfinder_paths/{vaults/adapters → adapters}/balance_adapter/test_adapter.py +12 -6
- wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/README.md +2 -2
- wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/adapter.py +30 -23
- wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/manifest.yaml +1 -1
- wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/{vaults/adapters → adapters}/hyperlend_adapter/adapter.py +33 -26
- wayfinder_paths/{vaults/adapters → adapters}/hyperlend_adapter/manifest.yaml +1 -1
- wayfinder_paths/{vaults/adapters → adapters}/hyperlend_adapter/test_adapter.py +2 -2
- wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/README.md +27 -40
- wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/adapter.py +78 -75
- wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/examples.json +10 -4
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/test_adapter.py +33 -28
- wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/README.md +2 -14
- wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/adapter.py +12 -19
- wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/manifest.yaml +1 -1
- wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/{vaults/adapters → adapters}/token_adapter/README.md +1 -1
- wayfinder_paths/{vaults/adapters → adapters}/token_adapter/adapter.py +8 -4
- wayfinder_paths/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/{vaults/adapters → adapters}/token_adapter/manifest.yaml +1 -1
- wayfinder_paths/{vaults/adapters → adapters}/token_adapter/test_adapter.py +1 -1
- wayfinder_paths/config.example.json +3 -1
- wayfinder_paths/core/__init__.py +3 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +20 -3
- wayfinder_paths/core/adapters/models.py +41 -0
- wayfinder_paths/core/clients/BRAPClient.py +21 -2
- wayfinder_paths/core/clients/ClientManager.py +42 -63
- wayfinder_paths/core/clients/HyperlendClient.py +46 -5
- wayfinder_paths/core/clients/LedgerClient.py +350 -124
- wayfinder_paths/core/clients/PoolClient.py +51 -19
- wayfinder_paths/core/clients/SimulationClient.py +16 -4
- wayfinder_paths/core/clients/TokenClient.py +34 -18
- wayfinder_paths/core/clients/TransactionClient.py +18 -2
- wayfinder_paths/core/clients/WalletClient.py +35 -4
- wayfinder_paths/core/clients/WayfinderClient.py +16 -5
- wayfinder_paths/core/clients/protocols.py +69 -62
- wayfinder_paths/core/clients/sdk_example.py +0 -5
- wayfinder_paths/core/config.py +192 -103
- wayfinder_paths/core/constants/base.py +17 -0
- wayfinder_paths/core/engine/{VaultJob.py → StrategyJob.py} +25 -19
- wayfinder_paths/core/engine/__init__.py +2 -2
- wayfinder_paths/core/engine/manifest.py +1 -1
- wayfinder_paths/core/services/base.py +6 -4
- wayfinder_paths/core/services/local_evm_txn.py +3 -2
- wayfinder_paths/core/settings.py +2 -2
- wayfinder_paths/core/strategies/Strategy.py +123 -37
- wayfinder_paths/core/utils/evm_helpers.py +12 -10
- wayfinder_paths/core/wallets/README.md +3 -3
- wayfinder_paths/core/wallets/WalletManager.py +3 -3
- wayfinder_paths/{vaults/policies → policies}/enso.py +1 -1
- wayfinder_paths/{vaults/policies → policies}/hyper_evm.py +2 -2
- wayfinder_paths/{vaults/policies → policies}/hyperlend.py +1 -1
- wayfinder_paths/{vaults/policies → policies}/moonwell.py +1 -1
- wayfinder_paths/{vaults/policies → policies}/prjx.py +1 -1
- wayfinder_paths/run_strategy.py +29 -27
- wayfinder_paths/scripts/create_strategy.py +3 -3
- wayfinder_paths/scripts/make_wallets.py +6 -6
- wayfinder_paths/scripts/validate_manifests.py +2 -2
- wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/README.md +10 -9
- wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/manifest.yaml +1 -1
- wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/strategy.py +47 -167
- wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/test_strategy.py +10 -8
- wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/README.md +15 -14
- wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/manifest.yaml +2 -2
- wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/strategy.py +97 -97
- wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/test_strategy.py +8 -8
- wayfinder_paths/{vaults/templates → templates}/adapter/README.md +5 -5
- wayfinder_paths/{vaults/templates → templates}/adapter/manifest.yaml +1 -1
- wayfinder_paths/{vaults/templates → templates}/adapter/test_adapter.py +1 -1
- wayfinder_paths/{vaults/templates → templates}/strategy/README.md +10 -9
- wayfinder_paths/{vaults/templates → templates}/strategy/manifest.yaml +1 -1
- wayfinder_paths/{vaults/templates → templates}/strategy/test_strategy.py +8 -8
- wayfinder_paths/tests/test_test_coverage.py +5 -5
- {wayfinder_paths-0.1.3.dist-info → wayfinder_paths-0.1.5.dist-info}/METADATA +146 -69
- wayfinder_paths-0.1.5.dist-info/RECORD +126 -0
- wayfinder_paths/vaults/adapters/hyperlend_adapter/__init__.py +0 -7
- wayfinder_paths/vaults/adapters/ledger_adapter/manifest.yaml +0 -11
- wayfinder_paths/vaults/adapters/token_adapter/examples.json +0 -26
- wayfinder_paths/vaults/strategies/__init__.py +0 -0
- wayfinder_paths-0.1.3.dist-info/RECORD +0 -126
- /wayfinder_paths/{vaults → adapters}/__init__.py +0 -0
- /wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/__init__.py +0 -0
- /wayfinder_paths/{vaults/adapters → adapters}/brap_adapter/examples.json +0 -0
- /wayfinder_paths/{vaults/adapters → adapters}/ledger_adapter/__init__.py +0 -0
- /wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/__init__.py +0 -0
- /wayfinder_paths/{vaults/adapters → adapters}/pool_adapter/examples.json +0 -0
- /wayfinder_paths/{vaults/adapters → adapters}/token_adapter/__init__.py +0 -0
- /wayfinder_paths/{vaults/policies → policies}/erc20.py +0 -0
- /wayfinder_paths/{vaults/policies → policies}/evm.py +0 -0
- /wayfinder_paths/{vaults/policies → policies}/hyperliquid.py +0 -0
- /wayfinder_paths/{vaults/policies → policies}/util.py +0 -0
- /wayfinder_paths/{vaults/adapters → strategies}/__init__.py +0 -0
- /wayfinder_paths/{vaults/strategies → strategies}/config.py +0 -0
- /wayfinder_paths/{vaults/strategies → strategies}/hyperlend_stable_yield_strategy/examples.json +0 -0
- /wayfinder_paths/{vaults/strategies → strategies}/stablecoin_yield_strategy/examples.json +0 -0
- /wayfinder_paths/{vaults/templates → templates}/adapter/adapter.py +0 -0
- /wayfinder_paths/{vaults/templates → templates}/adapter/examples.json +0 -0
- /wayfinder_paths/{vaults/templates → templates}/strategy/examples.json +0 -0
- /wayfinder_paths/{vaults/templates → templates}/strategy/strategy.py +0 -0
- {wayfinder_paths-0.1.3.dist-info → wayfinder_paths-0.1.5.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.3.dist-info → wayfinder_paths-0.1.5.dist-info}/WHEEL +0 -0
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import traceback
|
|
3
5
|
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
4
7
|
from typing import Any, TypedDict
|
|
5
8
|
|
|
6
9
|
from loguru import logger
|
|
@@ -13,32 +16,53 @@ class StatusDict(TypedDict):
|
|
|
13
16
|
portfolio_value: float
|
|
14
17
|
net_deposit: float
|
|
15
18
|
strategy_status: Any
|
|
19
|
+
gas_available: float
|
|
20
|
+
gassed_up: bool
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
StatusTuple = tuple[bool, str]
|
|
19
24
|
|
|
20
25
|
|
|
26
|
+
class WalletConfig(TypedDict, total=False):
|
|
27
|
+
"""Wallet configuration structure - allows additional fields for flexibility"""
|
|
28
|
+
|
|
29
|
+
address: str
|
|
30
|
+
private_key: str | None
|
|
31
|
+
private_key_hex: str | None
|
|
32
|
+
wallet_type: str | None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StrategyConfig(TypedDict, total=False):
|
|
36
|
+
"""Base strategy configuration structure - allows additional fields for flexibility"""
|
|
37
|
+
|
|
38
|
+
main_wallet: WalletConfig | None
|
|
39
|
+
strategy_wallet: WalletConfig | None
|
|
40
|
+
wallet_type: str | None
|
|
41
|
+
|
|
42
|
+
|
|
21
43
|
class Strategy(ABC):
|
|
22
|
-
name: str = None
|
|
23
|
-
INFO: StratDescriptor = None
|
|
44
|
+
name: str | None = None
|
|
45
|
+
INFO: StratDescriptor | None = None
|
|
24
46
|
|
|
25
47
|
def __init__(
|
|
26
48
|
self,
|
|
27
|
-
config: dict[str, Any] | None = None,
|
|
49
|
+
config: StrategyConfig | dict[str, Any] | None = None,
|
|
28
50
|
*,
|
|
29
|
-
main_wallet: dict[str, Any] | None = None,
|
|
30
|
-
|
|
51
|
+
main_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
52
|
+
strategy_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
31
53
|
simulation: bool = False,
|
|
32
|
-
web3_service: Web3Service = None,
|
|
54
|
+
web3_service: Web3Service | None = None,
|
|
33
55
|
api_key: str | None = None,
|
|
34
56
|
):
|
|
35
57
|
self.adapters = {}
|
|
36
|
-
self.
|
|
58
|
+
self.ledger_adapter = None
|
|
37
59
|
self.logger = logger.bind(strategy=self.__class__.__name__)
|
|
38
60
|
if api_key:
|
|
39
61
|
os.environ["WAYFINDER_API_KEY"] = api_key
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
self.config = config
|
|
64
|
+
|
|
65
|
+
async def setup(self) -> None:
|
|
42
66
|
"""Initialize strategy-specific setup after construction"""
|
|
43
67
|
pass
|
|
44
68
|
|
|
@@ -46,38 +70,72 @@ class Strategy(ABC):
|
|
|
46
70
|
"""Log messages - can be overridden by subclasses"""
|
|
47
71
|
self.logger.info(msg)
|
|
48
72
|
|
|
49
|
-
async def
|
|
50
|
-
"""Hook for temporary UI messages (e.g., progress) to the chat window."""
|
|
51
|
-
# No-op by default; strategies/hosts can override
|
|
52
|
-
return None
|
|
53
|
-
|
|
54
|
-
async def quote(self):
|
|
73
|
+
async def quote(self) -> None:
|
|
55
74
|
"""Get quotes for potential trades - optional for strategies"""
|
|
56
75
|
pass
|
|
57
76
|
|
|
77
|
+
def _get_strategy_wallet_address(self) -> str:
|
|
78
|
+
"""Get strategy wallet address with validation."""
|
|
79
|
+
strategy_wallet = self.config.get("strategy_wallet")
|
|
80
|
+
if not strategy_wallet or not isinstance(strategy_wallet, dict):
|
|
81
|
+
raise ValueError("strategy_wallet not configured in strategy config")
|
|
82
|
+
address = strategy_wallet.get("address")
|
|
83
|
+
if not address:
|
|
84
|
+
raise ValueError("strategy_wallet address not found in config")
|
|
85
|
+
return str(address)
|
|
86
|
+
|
|
87
|
+
def _get_main_wallet_address(self) -> str:
|
|
88
|
+
"""Get main wallet address with validation."""
|
|
89
|
+
main_wallet = self.config.get("main_wallet")
|
|
90
|
+
if not main_wallet or not isinstance(main_wallet, dict):
|
|
91
|
+
raise ValueError("main_wallet not configured in strategy config")
|
|
92
|
+
address = main_wallet.get("address")
|
|
93
|
+
if not address:
|
|
94
|
+
raise ValueError("main_wallet address not found in config")
|
|
95
|
+
return str(address)
|
|
96
|
+
|
|
58
97
|
@abstractmethod
|
|
59
98
|
async def deposit(self, **kwargs) -> StatusTuple:
|
|
60
99
|
"""
|
|
61
|
-
Deposit funds into the strategy
|
|
62
|
-
|
|
100
|
+
Deposit funds into the strategy.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
**kwargs: Strategy-specific deposit parameters. Common parameters include:
|
|
104
|
+
- main_token_amount: Amount of main token to deposit (float)
|
|
105
|
+
- gas_token_amount: Amount of gas token to deposit (float)
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Tuple of (success: bool, message: str)
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ValueError: If required parameters are missing or invalid.
|
|
63
112
|
"""
|
|
64
113
|
pass
|
|
65
114
|
|
|
66
115
|
async def withdraw(self, **kwargs) -> StatusTuple:
|
|
67
116
|
"""
|
|
68
|
-
Withdraw funds from the strategy
|
|
69
|
-
Default implementation unwinds all operations
|
|
70
|
-
|
|
117
|
+
Withdraw funds from the strategy.
|
|
118
|
+
Default implementation unwinds all operations.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
**kwargs: Strategy-specific withdrawal parameters (optional).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Tuple of (success: bool, message: str)
|
|
125
|
+
|
|
126
|
+
Note:
|
|
127
|
+
Subclasses may override this method to add validation or custom
|
|
128
|
+
withdrawal logic. The base implementation unwinds all ledger operations.
|
|
71
129
|
"""
|
|
72
|
-
if hasattr(self, "
|
|
73
|
-
while self.
|
|
74
|
-
node = self.
|
|
130
|
+
if hasattr(self, "ledger_adapter") and self.ledger_adapter:
|
|
131
|
+
while self.ledger_adapter.positions.operations:
|
|
132
|
+
node = self.ledger_adapter.positions.operations[-1]
|
|
75
133
|
adapter = self.adapters.get(node.adapter)
|
|
76
134
|
if adapter and hasattr(adapter, "unwind_op"):
|
|
77
135
|
await adapter.unwind_op(node)
|
|
78
|
-
self.
|
|
136
|
+
self.ledger_adapter.positions.operations.pop()
|
|
79
137
|
|
|
80
|
-
await self.
|
|
138
|
+
await self.ledger_adapter.save()
|
|
81
139
|
|
|
82
140
|
return (True, "Withdrawal complete")
|
|
83
141
|
|
|
@@ -108,7 +166,12 @@ class Strategy(ABC):
|
|
|
108
166
|
Wrapper to compute and return strategy status. In Django, this also snapshots.
|
|
109
167
|
Here we simply delegate to _status for compatibility.
|
|
110
168
|
"""
|
|
111
|
-
|
|
169
|
+
|
|
170
|
+
status = await self._status()
|
|
171
|
+
await self.ledger_adapter.record_strategy_snapshot(
|
|
172
|
+
wallet_address=self._get_strategy_wallet_address(),
|
|
173
|
+
strategy_status=status,
|
|
174
|
+
)
|
|
112
175
|
|
|
113
176
|
def register_adapters(self, adapters: list[Any]) -> None:
|
|
114
177
|
"""Register adapters for use by the strategy"""
|
|
@@ -119,13 +182,15 @@ class Strategy(ABC):
|
|
|
119
182
|
elif hasattr(adapter, "__class__"):
|
|
120
183
|
self.adapters[adapter.__class__.__name__] = adapter
|
|
121
184
|
|
|
122
|
-
def unwind_on_error(
|
|
185
|
+
def unwind_on_error(
|
|
186
|
+
self, func: Callable[..., Awaitable[StatusTuple]]
|
|
187
|
+
) -> Callable[..., Awaitable[StatusTuple]]:
|
|
123
188
|
"""
|
|
124
189
|
Decorator to unwind operations on error
|
|
125
190
|
Useful for deposit operations that need cleanup on failure
|
|
126
191
|
"""
|
|
127
192
|
|
|
128
|
-
async def wrapper(*args, **kwargs):
|
|
193
|
+
async def wrapper(*args: Any, **kwargs: Any) -> StatusTuple:
|
|
129
194
|
try:
|
|
130
195
|
return await func(*args, **kwargs)
|
|
131
196
|
except Exception:
|
|
@@ -143,21 +208,28 @@ class Strategy(ABC):
|
|
|
143
208
|
f"Strategy failed and unwinding also failed. Operation error: {trace}. Unwind error: {trace2}",
|
|
144
209
|
)
|
|
145
210
|
finally:
|
|
146
|
-
if hasattr(self, "
|
|
147
|
-
await self.
|
|
211
|
+
if hasattr(self, "ledger_adapter") and self.ledger_adapter:
|
|
212
|
+
await self.ledger_adapter.save()
|
|
148
213
|
|
|
149
214
|
return wrapper
|
|
150
215
|
|
|
151
216
|
@classmethod
|
|
152
217
|
def get_metadata(cls) -> dict[str, Any]:
|
|
153
218
|
"""
|
|
154
|
-
Return metadata about this strategy
|
|
155
|
-
Can be overridden to provide discovery information
|
|
219
|
+
Return metadata about this strategy.
|
|
220
|
+
Can be overridden to provide discovery information.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dictionary containing strategy metadata. The following keys are optional
|
|
224
|
+
and will be None if not defined on the class:
|
|
225
|
+
- name: Strategy name
|
|
226
|
+
- description: Strategy description
|
|
227
|
+
- summary: Strategy summary
|
|
156
228
|
"""
|
|
157
229
|
return {
|
|
158
|
-
"name": cls
|
|
159
|
-
"description": cls
|
|
160
|
-
"summary": cls
|
|
230
|
+
"name": getattr(cls, "name", None),
|
|
231
|
+
"description": getattr(cls, "description", None),
|
|
232
|
+
"summary": getattr(cls, "summary", None),
|
|
161
233
|
}
|
|
162
234
|
|
|
163
235
|
async def health_check(self) -> dict[str, Any]:
|
|
@@ -176,8 +248,22 @@ class Strategy(ABC):
|
|
|
176
248
|
|
|
177
249
|
async def partial_liquidate(self, usd_value: float) -> StatusTuple:
|
|
178
250
|
"""
|
|
179
|
-
Partially liquidate strategy positions by USD value
|
|
180
|
-
Optional method that can be overridden by subclasses
|
|
181
|
-
|
|
251
|
+
Partially liquidate strategy positions by USD value.
|
|
252
|
+
Optional method that can be overridden by subclasses.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
usd_value: USD value to liquidate (must be positive).
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Tuple of (success: bool, message: str)
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
ValueError: If usd_value is not positive.
|
|
262
|
+
|
|
263
|
+
Note:
|
|
264
|
+
Base implementation returns failure. Subclasses should override
|
|
265
|
+
to implement partial liquidation logic.
|
|
182
266
|
"""
|
|
267
|
+
if usd_value <= 0:
|
|
268
|
+
raise ValueError(f"usd_value must be positive, got {usd_value}")
|
|
183
269
|
return (False, "Partial liquidation not implemented for this strategy")
|
|
@@ -130,41 +130,43 @@ def resolve_private_key_for_from_address(
|
|
|
130
130
|
"""
|
|
131
131
|
from_addr_norm = (from_address or "").lower()
|
|
132
132
|
main_wallet = config.get("main_wallet")
|
|
133
|
-
|
|
133
|
+
strategy_wallet = config.get("strategy_wallet")
|
|
134
134
|
|
|
135
135
|
main_pk = None
|
|
136
|
-
|
|
136
|
+
strategy_pk = None
|
|
137
137
|
try:
|
|
138
138
|
if isinstance(main_wallet, dict):
|
|
139
139
|
main_pk = main_wallet.get("private_key") or main_wallet.get(
|
|
140
140
|
"private_key_hex"
|
|
141
141
|
)
|
|
142
|
-
if isinstance(
|
|
143
|
-
|
|
142
|
+
if isinstance(strategy_wallet, dict):
|
|
143
|
+
strategy_pk = strategy_wallet.get("private_key") or strategy_wallet.get(
|
|
144
144
|
"private_key_hex"
|
|
145
145
|
)
|
|
146
146
|
except (AttributeError, TypeError) as e:
|
|
147
147
|
logger.debug("Error resolving private keys from wallet config: %s", e)
|
|
148
148
|
|
|
149
149
|
main_addr = None
|
|
150
|
-
|
|
150
|
+
strategy_addr = None
|
|
151
151
|
try:
|
|
152
152
|
main_addr = (main_wallet or {}).get("address") or (
|
|
153
153
|
(main_wallet or {}).get("evm") or {}
|
|
154
154
|
).get("address")
|
|
155
|
-
|
|
156
|
-
(
|
|
155
|
+
strategy_addr = (strategy_wallet or {}).get("address") or (
|
|
156
|
+
(strategy_wallet or {}).get("evm") or {}
|
|
157
157
|
).get("address")
|
|
158
158
|
except (AttributeError, TypeError) as e:
|
|
159
159
|
logger.debug("Error resolving addresses from wallet config: %s", e)
|
|
160
160
|
|
|
161
161
|
if main_addr and from_addr_norm == (main_addr or "").lower():
|
|
162
162
|
return main_pk or os.getenv("PRIVATE_KEY")
|
|
163
|
-
if
|
|
164
|
-
return
|
|
163
|
+
if strategy_addr and from_addr_norm == (strategy_addr or "").lower():
|
|
164
|
+
return (
|
|
165
|
+
strategy_pk or os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
|
|
166
|
+
)
|
|
165
167
|
|
|
166
168
|
# Fallback to environment variables
|
|
167
|
-
return os.getenv("
|
|
169
|
+
return os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
|
|
168
170
|
|
|
169
171
|
|
|
170
172
|
async def _get_abi(chain_id: int, address: str) -> str | None:
|
|
@@ -15,7 +15,7 @@ Wayfinder strategies interact with blockchains through a single abstraction: the
|
|
|
15
15
|
from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
|
|
16
16
|
from wayfinder_paths.core.wallets.WalletManager import WalletManager
|
|
17
17
|
|
|
18
|
-
config = {...} # contains main_wallet /
|
|
18
|
+
config = {...} # contains main_wallet / strategy_wallet entries
|
|
19
19
|
wallet_provider = WalletManager.get_provider(config)
|
|
20
20
|
web3_service = DefaultWeb3Service(config, wallet_provider=wallet_provider)
|
|
21
21
|
|
|
@@ -70,14 +70,14 @@ web3_service = DefaultWeb3Service(config, wallet_provider=custom_wallet)
|
|
|
70
70
|
|
|
71
71
|
## Configuration hints
|
|
72
72
|
|
|
73
|
-
`WalletManager.get_provider` looks for `wallet_type` on the top-level config, `main_wallet`, and `
|
|
73
|
+
`WalletManager.get_provider` looks for `wallet_type` on the top-level config, `main_wallet`, and `strategy_wallet`. Example:
|
|
74
74
|
|
|
75
75
|
```json
|
|
76
76
|
{
|
|
77
77
|
"strategy": {
|
|
78
78
|
"wallet_type": "local",
|
|
79
79
|
"main_wallet": {"address": "0x...", "wallet_type": "local"},
|
|
80
|
-
"
|
|
80
|
+
"strategy_wallet": {"address": "0x..."}
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
```
|
|
@@ -41,9 +41,9 @@ class WalletManager:
|
|
|
41
41
|
wallet_type = main_wallet.get("wallet_type")
|
|
42
42
|
|
|
43
43
|
if not wallet_type:
|
|
44
|
-
|
|
45
|
-
if isinstance(
|
|
46
|
-
wallet_type =
|
|
44
|
+
strategy_wallet = config.get("strategy_wallet")
|
|
45
|
+
if isinstance(strategy_wallet, dict):
|
|
46
|
+
wallet_type = strategy_wallet.get("wallet_type")
|
|
47
47
|
|
|
48
48
|
if not wallet_type or wallet_type == "local":
|
|
49
49
|
logger.debug("Using LocalWalletProvider (default)")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from wayfinder_paths.
|
|
2
|
-
from wayfinder_paths.
|
|
1
|
+
from wayfinder_paths.policies.evm import native_transfer
|
|
2
|
+
from wayfinder_paths.policies.util import allow_functions
|
|
3
3
|
|
|
4
4
|
WHYPE_TOKEN = "0x5555555555555555555555555555555555555555"
|
|
5
5
|
HYPERCORE_SENTINEL_ADDRESS = "0x2222222222222222222222222222222222222222"
|
wayfinder_paths/run_strategy.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Strategy Runner
|
|
4
|
-
Main entry point for running
|
|
4
|
+
Main entry point for running strategies locally
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
@@ -12,9 +12,9 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
from loguru import logger
|
|
14
14
|
|
|
15
|
-
from wayfinder_paths.core.config import
|
|
15
|
+
from wayfinder_paths.core.config import StrategyJobConfig, load_config_from_env
|
|
16
16
|
from wayfinder_paths.core.engine.manifest import load_manifest, validate_manifest
|
|
17
|
-
from wayfinder_paths.core.engine.
|
|
17
|
+
from wayfinder_paths.core.engine.StrategyJob import StrategyJob
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def load_strategy(
|
|
@@ -28,7 +28,7 @@ def load_strategy(
|
|
|
28
28
|
Dynamically load a strategy by name using its manifest
|
|
29
29
|
|
|
30
30
|
Args:
|
|
31
|
-
strategy_name: Name of the strategy to load (directory name in
|
|
31
|
+
strategy_name: Name of the strategy to load (directory name in strategies/)
|
|
32
32
|
strategy_config: Configuration dict for the strategy
|
|
33
33
|
simulation: Enable simulation mode for testing
|
|
34
34
|
api_key: Optional API key for service account authentication
|
|
@@ -37,7 +37,7 @@ def load_strategy(
|
|
|
37
37
|
Strategy instance
|
|
38
38
|
"""
|
|
39
39
|
# Find strategy manifest by scanning for manifest.yaml in the strategy directory
|
|
40
|
-
strategies_dir = Path(__file__).parent / "
|
|
40
|
+
strategies_dir = Path(__file__).parent / "strategies"
|
|
41
41
|
strategy_dir = strategies_dir / strategy_name
|
|
42
42
|
manifest_path = strategy_dir / "manifest.yaml"
|
|
43
43
|
|
|
@@ -66,7 +66,7 @@ def load_strategy(
|
|
|
66
66
|
|
|
67
67
|
def load_config(
|
|
68
68
|
config_path: str | None = None, strategy_name: str | None = None
|
|
69
|
-
) ->
|
|
69
|
+
) -> StrategyJobConfig:
|
|
70
70
|
"""
|
|
71
71
|
Load configuration from file or environment
|
|
72
72
|
|
|
@@ -75,13 +75,13 @@ def load_config(
|
|
|
75
75
|
strategy_name: Optional strategy name for per-strategy wallet lookup
|
|
76
76
|
|
|
77
77
|
Returns:
|
|
78
|
-
|
|
78
|
+
StrategyJobConfig instance
|
|
79
79
|
"""
|
|
80
80
|
if config_path and Path(config_path).exists():
|
|
81
81
|
logger.info(f"Loading config from {config_path}")
|
|
82
82
|
with open(config_path) as f:
|
|
83
83
|
config_data = json.load(f)
|
|
84
|
-
return
|
|
84
|
+
return StrategyJobConfig.from_dict(config_data, strategy_name=strategy_name)
|
|
85
85
|
else:
|
|
86
86
|
logger.info("Loading config from environment variables")
|
|
87
87
|
config = load_config_from_env()
|
|
@@ -100,7 +100,7 @@ async def run_strategy(
|
|
|
100
100
|
**kwargs,
|
|
101
101
|
):
|
|
102
102
|
"""
|
|
103
|
-
Run a
|
|
103
|
+
Run a strategy
|
|
104
104
|
|
|
105
105
|
Args:
|
|
106
106
|
strategy_name: Name of the strategy to run
|
|
@@ -120,7 +120,7 @@ async def run_strategy(
|
|
|
120
120
|
# Extract directory name from manifest path for wallet lookup
|
|
121
121
|
# Use the directory name (strategy identifier) for wallet lookup
|
|
122
122
|
manifest_dir = Path(manifest_path).parent
|
|
123
|
-
strategies_dir = Path(__file__).parent / "
|
|
123
|
+
strategies_dir = Path(__file__).parent / "strategies"
|
|
124
124
|
try:
|
|
125
125
|
# Try to get relative path - if it's under strategies_dir, use directory name
|
|
126
126
|
rel_path = manifest_dir.relative_to(strategies_dir)
|
|
@@ -141,13 +141,13 @@ async def run_strategy(
|
|
|
141
141
|
logger.debug(f"Config path provided: {config_path}")
|
|
142
142
|
config = load_config(config_path, strategy_name=strategy_name_for_wallet)
|
|
143
143
|
logger.debug(
|
|
144
|
-
"Loaded config: creds=%s wallets(main=%s
|
|
144
|
+
"Loaded config: creds=%s wallets(main=%s strategy=%s)",
|
|
145
145
|
"yes"
|
|
146
146
|
if (config.user.username and config.user.password)
|
|
147
147
|
or config.user.refresh_token
|
|
148
148
|
else "no",
|
|
149
149
|
(config.user.main_wallet_address or "none"),
|
|
150
|
-
(config.user.
|
|
150
|
+
(config.user.strategy_wallet_address or "none"),
|
|
151
151
|
)
|
|
152
152
|
|
|
153
153
|
# Validate required configuration
|
|
@@ -173,11 +173,11 @@ async def run_strategy(
|
|
|
173
173
|
)
|
|
174
174
|
logger.info(f"Loaded strategy: {strategy.name}")
|
|
175
175
|
|
|
176
|
-
# Create
|
|
177
|
-
|
|
176
|
+
# Create strategy job
|
|
177
|
+
strategy_job = StrategyJob(strategy, config)
|
|
178
178
|
|
|
179
|
-
# Setup
|
|
180
|
-
logger.info("Setting up
|
|
179
|
+
# Setup strategy job
|
|
180
|
+
logger.info("Setting up strategy job...")
|
|
181
181
|
logger.debug(
|
|
182
182
|
"Auth mode: %s",
|
|
183
183
|
"credentials"
|
|
@@ -185,12 +185,12 @@ async def run_strategy(
|
|
|
185
185
|
or config.user.refresh_token
|
|
186
186
|
else "missing",
|
|
187
187
|
)
|
|
188
|
-
await
|
|
188
|
+
await strategy_job.setup()
|
|
189
189
|
|
|
190
190
|
# Execute action
|
|
191
191
|
if action == "run":
|
|
192
192
|
logger.info("Starting continuous execution...")
|
|
193
|
-
await
|
|
193
|
+
await strategy_job.run_continuous(interval_seconds=kwargs.get("interval"))
|
|
194
194
|
|
|
195
195
|
elif action == "deposit":
|
|
196
196
|
main_token_amount = kwargs.get("main_token_amount")
|
|
@@ -207,7 +207,7 @@ async def run_strategy(
|
|
|
207
207
|
if gas_token_amount is None:
|
|
208
208
|
gas_token_amount = 0.0
|
|
209
209
|
|
|
210
|
-
result = await
|
|
210
|
+
result = await strategy_job.execute_strategy(
|
|
211
211
|
"deposit",
|
|
212
212
|
main_token_amount=main_token_amount,
|
|
213
213
|
gas_token_amount=gas_token_amount,
|
|
@@ -216,22 +216,22 @@ async def run_strategy(
|
|
|
216
216
|
|
|
217
217
|
elif action == "withdraw":
|
|
218
218
|
amount = kwargs.get("amount")
|
|
219
|
-
result = await
|
|
219
|
+
result = await strategy_job.execute_strategy("withdraw", amount=amount)
|
|
220
220
|
logger.info(f"Withdraw result: {result}")
|
|
221
221
|
|
|
222
222
|
elif action == "status":
|
|
223
|
-
result = await
|
|
223
|
+
result = await strategy_job.execute_strategy("status")
|
|
224
224
|
logger.info(f"Status: {json.dumps(result, indent=2)}")
|
|
225
225
|
|
|
226
226
|
elif action == "update":
|
|
227
|
-
result = await
|
|
227
|
+
result = await strategy_job.execute_strategy("update")
|
|
228
228
|
logger.info(f"Update result: {result}")
|
|
229
229
|
|
|
230
230
|
elif action == "partial-liquidate":
|
|
231
231
|
usd_value = kwargs.get("amount")
|
|
232
232
|
if not usd_value:
|
|
233
233
|
raise ValueError("Amount (USD value) required for partial-liquidate")
|
|
234
|
-
result = await
|
|
234
|
+
result = await strategy_job.execute_strategy(
|
|
235
235
|
"partial_liquidate", usd_value=usd_value
|
|
236
236
|
)
|
|
237
237
|
logger.info(f"Partial liquidation result: {result}")
|
|
@@ -289,7 +289,9 @@ async def run_strategy(
|
|
|
289
289
|
duration = kwargs.get("duration") or 300
|
|
290
290
|
logger.info(f"Running script mode for {duration}s...")
|
|
291
291
|
task = asyncio.create_task(
|
|
292
|
-
|
|
292
|
+
strategy_job.run_continuous(
|
|
293
|
+
interval_seconds=kwargs.get("interval") or 60
|
|
294
|
+
)
|
|
293
295
|
)
|
|
294
296
|
await asyncio.sleep(duration)
|
|
295
297
|
task.cancel()
|
|
@@ -307,13 +309,13 @@ async def run_strategy(
|
|
|
307
309
|
logger.error(f"Error: {e}")
|
|
308
310
|
sys.exit(1)
|
|
309
311
|
finally:
|
|
310
|
-
if "
|
|
311
|
-
await
|
|
312
|
+
if "strategy_job" in locals():
|
|
313
|
+
await strategy_job.stop()
|
|
312
314
|
|
|
313
315
|
|
|
314
316
|
def main():
|
|
315
317
|
"""Main entry point"""
|
|
316
|
-
parser = argparse.ArgumentParser(description="Run
|
|
318
|
+
parser = argparse.ArgumentParser(description="Run strategy strategies")
|
|
317
319
|
parser.add_argument(
|
|
318
320
|
"strategy",
|
|
319
321
|
nargs="?",
|
|
@@ -64,13 +64,13 @@ def main():
|
|
|
64
64
|
parser.add_argument(
|
|
65
65
|
"--template-dir",
|
|
66
66
|
type=Path,
|
|
67
|
-
default=Path(__file__).parent.parent / "
|
|
67
|
+
default=Path(__file__).parent.parent / "templates" / "strategy",
|
|
68
68
|
help="Path to strategy template directory",
|
|
69
69
|
)
|
|
70
70
|
parser.add_argument(
|
|
71
71
|
"--strategies-dir",
|
|
72
72
|
type=Path,
|
|
73
|
-
default=Path(__file__).parent.parent / "
|
|
73
|
+
default=Path(__file__).parent.parent / "strategies",
|
|
74
74
|
help="Path to strategies directory",
|
|
75
75
|
)
|
|
76
76
|
parser.add_argument(
|
|
@@ -136,7 +136,7 @@ def main():
|
|
|
136
136
|
print(f" Updated strategy.py with class name: {class_name}")
|
|
137
137
|
|
|
138
138
|
# Generate entrypoint path
|
|
139
|
-
entrypoint = f"
|
|
139
|
+
entrypoint = f"strategies.{dir_name}.strategy.{class_name}"
|
|
140
140
|
|
|
141
141
|
# Update manifest with name (using directory name) and entrypoint
|
|
142
142
|
manifest_path = strategy_dir / "manifest.yaml"
|
|
@@ -22,18 +22,18 @@ def write_env(rows: list[dict[str, str]], out_dir: Path) -> None:
|
|
|
22
22
|
main_w = (
|
|
23
23
|
label_to_wallet.get("main") or label_to_wallet.get("default") or rows[0]
|
|
24
24
|
)
|
|
25
|
-
|
|
25
|
+
strategy_w = label_to_wallet.get("strategy")
|
|
26
26
|
|
|
27
27
|
f.write("RPC_URL=https://rpc.ankr.com/eth\n")
|
|
28
28
|
# Back-compat defaults
|
|
29
29
|
f.write(f"PRIVATE_KEY={main_w['private_key_hex']}\n")
|
|
30
30
|
f.write(f"FROM_ADDRESS={main_w['address']}\n")
|
|
31
|
-
# Explicit main/
|
|
31
|
+
# Explicit main/strategy variables
|
|
32
32
|
f.write(f"MAIN_WALLET_ADDRESS={main_w['address']}\n")
|
|
33
|
-
if
|
|
34
|
-
f.write(f"
|
|
35
|
-
# Optional: expose
|
|
36
|
-
f.write(f"
|
|
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
37
|
|
|
38
38
|
|
|
39
39
|
def main():
|
|
@@ -138,7 +138,7 @@ def validate_strategy_manifest(manifest_path: str) -> tuple[bool, list[str]]:
|
|
|
138
138
|
def find_adapter_manifests() -> list[Path]:
|
|
139
139
|
"""Find all adapter manifest files."""
|
|
140
140
|
manifests = []
|
|
141
|
-
adapter_dir = Path(__file__).parent.parent / "
|
|
141
|
+
adapter_dir = Path(__file__).parent.parent / "adapters"
|
|
142
142
|
if adapter_dir.exists():
|
|
143
143
|
for adapter_path in adapter_dir.iterdir():
|
|
144
144
|
manifest_path = adapter_path / "manifest.yaml"
|
|
@@ -150,7 +150,7 @@ def find_adapter_manifests() -> list[Path]:
|
|
|
150
150
|
def find_strategy_manifests() -> list[Path]:
|
|
151
151
|
"""Find all strategy manifest files."""
|
|
152
152
|
manifests = []
|
|
153
|
-
strategy_dir = Path(__file__).parent.parent / "
|
|
153
|
+
strategy_dir = Path(__file__).parent.parent / "strategies"
|
|
154
154
|
if strategy_dir.exists():
|
|
155
155
|
for strategy_path in strategy_dir.iterdir():
|
|
156
156
|
manifest_path = strategy_path / "manifest.yaml"
|