wayfinder-paths 0.1.11__py3-none-any.whl → 0.1.13__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/adapters/balance_adapter/adapter.py +3 -7
- wayfinder_paths/adapters/brap_adapter/adapter.py +10 -13
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +6 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
- wayfinder_paths/adapters/pool_adapter/README.md +4 -19
- wayfinder_paths/adapters/pool_adapter/adapter.py +4 -29
- wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
- wayfinder_paths/core/clients/AuthClient.py +2 -2
- wayfinder_paths/core/clients/BRAPClient.py +2 -2
- wayfinder_paths/core/clients/HyperlendClient.py +2 -2
- wayfinder_paths/core/clients/PoolClient.py +18 -54
- wayfinder_paths/core/clients/TokenClient.py +3 -3
- wayfinder_paths/core/clients/WalletClient.py +2 -2
- wayfinder_paths/core/clients/WayfinderClient.py +9 -10
- wayfinder_paths/core/clients/protocols.py +1 -7
- wayfinder_paths/core/config.py +60 -224
- wayfinder_paths/core/services/local_evm_txn.py +22 -4
- wayfinder_paths/core/strategies/Strategy.py +3 -3
- wayfinder_paths/core/strategies/descriptors.py +7 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -1
- wayfinder_paths/core/utils/wallets.py +12 -19
- wayfinder_paths/core/wallets/README.md +1 -1
- wayfinder_paths/run_strategy.py +10 -8
- wayfinder_paths/scripts/create_strategy.py +5 -5
- wayfinder_paths/scripts/make_wallets.py +5 -5
- wayfinder_paths/scripts/run_strategy.py +3 -3
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +196 -515
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +8 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +25 -25
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +28 -9
- wayfinder_paths/templates/adapter/README.md +1 -1
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/METADATA +9 -12
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/RECORD +45 -45
- wayfinder_paths/core/settings.py +0 -61
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/WHEEL +0 -0
wayfinder_paths/core/config.py
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Core Configuration System
|
|
3
|
-
Separates user-provided configuration from system configuration
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
1
|
import json
|
|
7
2
|
from dataclasses import dataclass, field
|
|
8
3
|
from pathlib import Path
|
|
@@ -18,143 +13,51 @@ from wayfinder_paths.core.constants.base import (
|
|
|
18
13
|
)
|
|
19
14
|
|
|
20
15
|
|
|
16
|
+
def _load_config_file() -> dict[str, Any]:
|
|
17
|
+
path = Path("config.json")
|
|
18
|
+
if not path.exists():
|
|
19
|
+
return {}
|
|
20
|
+
try:
|
|
21
|
+
return json.loads(path.read_text())
|
|
22
|
+
except Exception as e:
|
|
23
|
+
logger.warning(f"Failed to read config file at config.json: {e}")
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
CONFIG = _load_config_file()
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
@dataclass
|
|
22
31
|
class UserConfig:
|
|
23
|
-
"""
|
|
24
|
-
User-provided configuration
|
|
25
|
-
These are values that users MUST provide to run strategies
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
# Credential-based auth (JWT)
|
|
29
32
|
username: str | None = None
|
|
30
33
|
password: str | None = None
|
|
31
34
|
refresh_token: str | None = None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Optional user preferences
|
|
38
|
-
default_slippage: float = 0.005 # Default slippage tolerance (0.5%)
|
|
39
|
-
gas_multiplier: float = 1.2 # Gas limit multiplier for safety
|
|
40
|
-
|
|
41
|
-
@classmethod
|
|
42
|
-
def from_dict(cls, data: dict[str, Any]) -> "UserConfig":
|
|
43
|
-
"""Create UserConfig from dictionary"""
|
|
44
|
-
return cls(
|
|
45
|
-
username=data.get("username"),
|
|
46
|
-
password=data.get("password"),
|
|
47
|
-
refresh_token=data.get("refresh_token"),
|
|
48
|
-
main_wallet_address=data.get("main_wallet_address"),
|
|
49
|
-
strategy_wallet_address=data.get("strategy_wallet_address"),
|
|
50
|
-
default_slippage=data.get("default_slippage", 0.005),
|
|
51
|
-
gas_multiplier=data.get("gas_multiplier", 1.2),
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
def to_dict(self) -> dict[str, Any]:
|
|
55
|
-
"""Convert to dictionary"""
|
|
56
|
-
return {
|
|
57
|
-
"username": self.username,
|
|
58
|
-
"password": self.password,
|
|
59
|
-
"refresh_token": self.refresh_token,
|
|
60
|
-
"main_wallet_address": self.main_wallet_address,
|
|
61
|
-
"strategy_wallet_address": self.strategy_wallet_address,
|
|
62
|
-
"default_slippage": self.default_slippage,
|
|
63
|
-
"gas_multiplier": self.gas_multiplier,
|
|
64
|
-
}
|
|
35
|
+
main_wallet_address: str | None = None
|
|
36
|
+
strategy_wallet_address: str | None = None
|
|
37
|
+
default_slippage: float = 0.005
|
|
38
|
+
gas_multiplier: float = 1.2
|
|
65
39
|
|
|
66
40
|
|
|
67
41
|
@dataclass
|
|
68
42
|
class SystemConfig:
|
|
69
|
-
"""
|
|
70
|
-
System-level configuration
|
|
71
|
-
These are values managed by the Wayfinder system
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
# API endpoints (populated from config.json or defaults)
|
|
75
43
|
api_base_url: str = field(default="https://api.wayfinder.ai")
|
|
76
|
-
|
|
77
|
-
# Job configuration
|
|
78
44
|
job_id: str | None = None
|
|
79
45
|
job_type: str = "strategy"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
max_retries: int = 3 # Maximum retries for failed operations
|
|
84
|
-
retry_delay: int = 5 # Delay between retries in seconds
|
|
85
|
-
|
|
86
|
-
# System paths
|
|
46
|
+
update_interval: int = 60
|
|
47
|
+
max_retries: int = 3
|
|
48
|
+
retry_delay: int = 5
|
|
87
49
|
log_path: str | None = None
|
|
88
50
|
data_path: str | None = None
|
|
89
|
-
|
|
90
|
-
# Local wallets.json path used to auto-populate wallet addresses when not provided
|
|
91
|
-
wallets_path: str | None = "wallets.json"
|
|
92
|
-
|
|
93
|
-
# Optional wallet_id for policy rendering
|
|
94
51
|
wallet_id: str | None = None
|
|
95
52
|
|
|
96
|
-
@classmethod
|
|
97
|
-
def from_dict(cls, data: dict[str, Any]) -> "SystemConfig":
|
|
98
|
-
"""Create SystemConfig from dictionary"""
|
|
99
|
-
return cls(
|
|
100
|
-
api_base_url=data.get("api_base_url", "https://api.wayfinder.ai"),
|
|
101
|
-
job_id=data.get("job_id"),
|
|
102
|
-
job_type=data.get("job_type", "strategy"),
|
|
103
|
-
update_interval=data.get("update_interval", 60),
|
|
104
|
-
max_retries=data.get("max_retries", 3),
|
|
105
|
-
retry_delay=data.get("retry_delay", 5),
|
|
106
|
-
log_path=data.get("log_path"),
|
|
107
|
-
data_path=data.get("data_path"),
|
|
108
|
-
wallets_path=data.get("wallets_path", "wallets.json"),
|
|
109
|
-
wallet_id=data.get("wallet_id"),
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
def to_dict(self) -> dict[str, Any]:
|
|
113
|
-
"""Convert to dictionary"""
|
|
114
|
-
return {
|
|
115
|
-
"api_base_url": self.api_base_url,
|
|
116
|
-
"job_id": self.job_id,
|
|
117
|
-
"job_type": self.job_type,
|
|
118
|
-
"update_interval": self.update_interval,
|
|
119
|
-
"max_retries": self.max_retries,
|
|
120
|
-
"retry_delay": self.retry_delay,
|
|
121
|
-
"log_path": self.log_path,
|
|
122
|
-
"data_path": self.data_path,
|
|
123
|
-
"wallets_path": self.wallets_path,
|
|
124
|
-
"wallet_id": self.wallet_id,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
53
|
|
|
128
54
|
@dataclass
|
|
129
55
|
class StrategyJobConfig:
|
|
130
|
-
"""
|
|
131
|
-
Complete configuration for a strategy job
|
|
132
|
-
Combines user and system configurations
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
56
|
user: UserConfig
|
|
136
57
|
system: SystemConfig
|
|
137
|
-
strategy_config: dict[str, Any] = field(
|
|
138
|
-
default_factory=dict
|
|
139
|
-
) # Strategy-specific configuration
|
|
58
|
+
strategy_config: dict[str, Any] = field(default_factory=dict)
|
|
140
59
|
|
|
141
60
|
def __post_init__(self) -> None:
|
|
142
|
-
"""
|
|
143
|
-
Enrich strategy_config with wallet addresses and private keys from wallets.json.
|
|
144
|
-
|
|
145
|
-
This method automatically loads wallet information from wallets.json to populate
|
|
146
|
-
main_wallet and strategy_wallet addresses in strategy_config. Only uses wallets
|
|
147
|
-
with exact label matches (no fallbacks).
|
|
148
|
-
|
|
149
|
-
Wallet enrichment is conditional and can be skipped:
|
|
150
|
-
- Skipped if wallet_type is explicitly set to a non-"local" value
|
|
151
|
-
- Only performed if wallet_type is None, "local", or not specified
|
|
152
|
-
- Allows custom wallet providers (Privy/Turnkey) to opt out of file-based enrichment
|
|
153
|
-
|
|
154
|
-
Note:
|
|
155
|
-
This method never raises exceptions - all errors are logged but do not
|
|
156
|
-
prevent config construction failures.
|
|
157
|
-
"""
|
|
158
61
|
try:
|
|
159
62
|
if not isinstance(self.strategy_config, dict):
|
|
160
63
|
self.strategy_config = {}
|
|
@@ -169,21 +72,11 @@ class StrategyJobConfig:
|
|
|
169
72
|
if wallet_type in (None, "local"):
|
|
170
73
|
self._enrich_wallet_private_keys(by_addr)
|
|
171
74
|
except Exception as e:
|
|
172
|
-
# Defensive: never allow config construction to fail on enrichment
|
|
173
75
|
logger.warning(
|
|
174
76
|
f"Failed to enrich strategy config with wallet information: {e}"
|
|
175
77
|
)
|
|
176
78
|
|
|
177
79
|
def _get_wallet_type(self) -> str | None:
|
|
178
|
-
"""
|
|
179
|
-
Determine the wallet type from strategy config.
|
|
180
|
-
|
|
181
|
-
Checks strategy_config, main_wallet, and strategy_wallet for wallet_type.
|
|
182
|
-
Returns the first wallet_type found, or None if not specified.
|
|
183
|
-
|
|
184
|
-
Returns:
|
|
185
|
-
Wallet type string or None if not specified.
|
|
186
|
-
"""
|
|
187
80
|
wallet_type = self.strategy_config.get("wallet_type")
|
|
188
81
|
if wallet_type:
|
|
189
82
|
return wallet_type
|
|
@@ -205,26 +98,16 @@ class StrategyJobConfig:
|
|
|
205
98
|
def _load_wallets_from_file(
|
|
206
99
|
self,
|
|
207
100
|
) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
|
|
208
|
-
|
|
209
|
-
Load wallets from wallets.json file and index by label and address.
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
Tuple of (by_label, by_addr) dictionaries:
|
|
213
|
-
- by_label: Maps wallet label to wallet entry
|
|
214
|
-
- by_addr: Maps wallet address (lowercase) to wallet entry
|
|
215
|
-
"""
|
|
216
|
-
entries = _read_wallets_file(self.system.wallets_path)
|
|
101
|
+
entries = _read_wallets_from_config()
|
|
217
102
|
by_label: dict[str, dict[str, Any]] = {}
|
|
218
103
|
by_addr: dict[str, dict[str, Any]] = {}
|
|
219
104
|
|
|
220
105
|
if entries and isinstance(entries, list):
|
|
221
106
|
for e in entries:
|
|
222
107
|
if isinstance(e, dict):
|
|
223
|
-
# Index by label
|
|
224
108
|
label = e.get("label")
|
|
225
109
|
if isinstance(label, str):
|
|
226
110
|
by_label[label] = e
|
|
227
|
-
# Index by address
|
|
228
111
|
addr = e.get("address")
|
|
229
112
|
if isinstance(addr, str):
|
|
230
113
|
by_addr[addr.lower()] = e
|
|
@@ -232,16 +115,6 @@ class StrategyJobConfig:
|
|
|
232
115
|
return by_label, by_addr
|
|
233
116
|
|
|
234
117
|
def _enrich_wallet_addresses(self, by_label: dict[str, dict[str, Any]]) -> None:
|
|
235
|
-
"""
|
|
236
|
-
Enrich strategy_config with wallet addresses from wallets.json.
|
|
237
|
-
|
|
238
|
-
Loads main_wallet and strategy_wallet addresses by exact label match.
|
|
239
|
-
Only sets addresses if they are not already present in strategy_config.
|
|
240
|
-
|
|
241
|
-
Args:
|
|
242
|
-
by_label: Dictionary mapping wallet labels to wallet entries.
|
|
243
|
-
"""
|
|
244
|
-
# Load main wallet by exact label match only
|
|
245
118
|
if "main_wallet" not in self.strategy_config:
|
|
246
119
|
main_wallet = by_label.get("main")
|
|
247
120
|
if main_wallet:
|
|
@@ -249,33 +122,21 @@ class StrategyJobConfig:
|
|
|
249
122
|
"address": main_wallet["address"]
|
|
250
123
|
}
|
|
251
124
|
|
|
252
|
-
# Load strategy wallet by strategy name label match only
|
|
253
125
|
strategy_name = self.strategy_config.get("_strategy_name")
|
|
254
126
|
if strategy_name and isinstance(strategy_name, str):
|
|
255
127
|
strategy_wallet = by_label.get(strategy_name)
|
|
256
128
|
if strategy_wallet:
|
|
257
|
-
# Use strategy-specific wallet as strategy_wallet
|
|
258
129
|
if "strategy_wallet" not in self.strategy_config:
|
|
259
130
|
self.strategy_config["strategy_wallet"] = {
|
|
260
131
|
"address": strategy_wallet["address"]
|
|
261
132
|
}
|
|
262
133
|
elif isinstance(self.strategy_config.get("strategy_wallet"), dict):
|
|
263
|
-
# Ensure address is set if not already
|
|
264
134
|
if not self.strategy_config["strategy_wallet"].get("address"):
|
|
265
135
|
self.strategy_config["strategy_wallet"]["address"] = (
|
|
266
136
|
strategy_wallet["address"]
|
|
267
137
|
)
|
|
268
138
|
|
|
269
139
|
def _enrich_wallet_private_keys(self, by_addr: dict[str, dict[str, Any]]) -> None:
|
|
270
|
-
"""
|
|
271
|
-
Enrich wallet configs with private keys from wallets.json.
|
|
272
|
-
|
|
273
|
-
Only enriches private keys if using local wallet type (or defaulting to local).
|
|
274
|
-
This ensures custom wallet providers don't get private keys from files.
|
|
275
|
-
|
|
276
|
-
Args:
|
|
277
|
-
by_addr: Dictionary mapping wallet addresses (lowercase) to wallet entries.
|
|
278
|
-
"""
|
|
279
140
|
try:
|
|
280
141
|
for key in ("main_wallet", "strategy_wallet"):
|
|
281
142
|
wallet_obj = self.strategy_config.get(key)
|
|
@@ -292,24 +153,38 @@ class StrategyJobConfig:
|
|
|
292
153
|
wallet_obj["private_key_hex"] = pk
|
|
293
154
|
except Exception as e:
|
|
294
155
|
logger.warning(
|
|
295
|
-
f"Failed to enrich wallet private keys from
|
|
156
|
+
f"Failed to enrich wallet private keys from config.json: {e}"
|
|
296
157
|
)
|
|
297
158
|
|
|
298
159
|
@classmethod
|
|
299
160
|
def from_dict(
|
|
300
161
|
cls, data: dict[str, Any], strategy_name: str | None = None
|
|
301
162
|
) -> "StrategyJobConfig":
|
|
302
|
-
""
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
163
|
+
user_data = data.get("user", {})
|
|
164
|
+
user_cfg = UserConfig(
|
|
165
|
+
username=user_data.get("username"),
|
|
166
|
+
password=user_data.get("password"),
|
|
167
|
+
refresh_token=user_data.get("refresh_token"),
|
|
168
|
+
main_wallet_address=user_data.get("main_wallet_address"),
|
|
169
|
+
strategy_wallet_address=user_data.get("strategy_wallet_address"),
|
|
170
|
+
default_slippage=user_data.get("default_slippage", 0.005),
|
|
171
|
+
gas_multiplier=user_data.get("gas_multiplier", 1.2),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
system_data = data.get("system", {})
|
|
175
|
+
sys_cfg = SystemConfig(
|
|
176
|
+
api_base_url=system_data.get("api_base_url", "https://api.wayfinder.ai"),
|
|
177
|
+
job_id=system_data.get("job_id"),
|
|
178
|
+
job_type=system_data.get("job_type", "strategy"),
|
|
179
|
+
update_interval=system_data.get("update_interval", 60),
|
|
180
|
+
max_retries=system_data.get("max_retries", 3),
|
|
181
|
+
retry_delay=system_data.get("retry_delay", 5),
|
|
182
|
+
log_path=system_data.get("log_path"),
|
|
183
|
+
data_path=system_data.get("data_path"),
|
|
184
|
+
wallet_id=system_data.get("wallet_id"),
|
|
185
|
+
)
|
|
186
|
+
|
|
311
187
|
strategy_config = data.get("strategy", {})
|
|
312
|
-
# Store strategy name in config for wallet lookup
|
|
313
188
|
if strategy_name:
|
|
314
189
|
strategy_config["_strategy_name"] = strategy_name
|
|
315
190
|
return cls(
|
|
@@ -318,25 +193,11 @@ class StrategyJobConfig:
|
|
|
318
193
|
strategy_config=strategy_config,
|
|
319
194
|
)
|
|
320
195
|
|
|
321
|
-
def to_dict(self) -> dict[str, Any]:
|
|
322
|
-
"""Convert to dictionary"""
|
|
323
|
-
return {
|
|
324
|
-
"user": self.user.to_dict(),
|
|
325
|
-
"system": self.system.to_dict(),
|
|
326
|
-
"strategy": self.strategy_config,
|
|
327
|
-
}
|
|
328
|
-
|
|
329
196
|
def get_adapter_config(self, adapter_name: str) -> dict[str, Any]:
|
|
330
|
-
"""
|
|
331
|
-
Get configuration for a specific adapter
|
|
332
|
-
Combines relevant user and system settings
|
|
333
|
-
"""
|
|
334
197
|
config = {
|
|
335
198
|
"api_base_url": self.system.api_base_url,
|
|
336
199
|
}
|
|
337
200
|
|
|
338
|
-
# Add wallet configuration if needed
|
|
339
|
-
# Only use wallets from strategy_config (matched by label) - no fallbacks
|
|
340
201
|
if adapter_name in [
|
|
341
202
|
ADAPTER_BALANCE,
|
|
342
203
|
ADAPTER_BRAP,
|
|
@@ -359,60 +220,35 @@ class StrategyJobConfig:
|
|
|
359
220
|
and main_wallet.get("address")
|
|
360
221
|
else {}
|
|
361
222
|
)
|
|
362
|
-
# user_wallet uses strategy_wallet if available, otherwise main_wallet
|
|
363
223
|
config["user_wallet"] = (
|
|
364
224
|
config.get("strategy_wallet") or config.get("main_wallet") or {}
|
|
365
225
|
)
|
|
366
226
|
|
|
367
|
-
# Add specific settings
|
|
368
227
|
if adapter_name == ADAPTER_BRAP:
|
|
369
228
|
config["default_slippage"] = self.user.default_slippage
|
|
370
229
|
config["gas_multiplier"] = self.user.gas_multiplier
|
|
371
230
|
|
|
372
|
-
# Add any strategy-specific adapter config
|
|
373
231
|
if adapter_name in self.strategy_config.get("adapters", {}):
|
|
374
232
|
config.update(self.strategy_config["adapters"][adapter_name])
|
|
375
233
|
|
|
376
234
|
return config
|
|
377
235
|
|
|
378
236
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
237
|
+
def get_api_base_url() -> str:
|
|
238
|
+
system = CONFIG.get("system", {}) if isinstance(CONFIG, dict) else {}
|
|
239
|
+
api_url = system.get("api_base_url")
|
|
240
|
+
if api_url and isinstance(api_url, str):
|
|
241
|
+
return api_url.strip()
|
|
242
|
+
return "https://wayfinder.ai/api/v1"
|
|
385
243
|
|
|
386
|
-
Args:
|
|
387
|
-
wallets_path: Path to the wallets.json file. If None or empty, returns empty list.
|
|
388
244
|
|
|
389
|
-
|
|
390
|
-
List of wallet dictionaries. Each wallet dict should contain:
|
|
391
|
-
- label: Wallet label (str)
|
|
392
|
-
- address: Wallet address (str)
|
|
393
|
-
- private_key or private_key_hex: Private key (str, optional)
|
|
394
|
-
|
|
395
|
-
Returns empty list if file doesn't exist, is invalid JSON, or contains
|
|
396
|
-
non-list data.
|
|
397
|
-
|
|
398
|
-
Note:
|
|
399
|
-
All errors are logged but do not raise exceptions. This allows the
|
|
400
|
-
config system to continue functioning even if wallets.json is missing
|
|
401
|
-
or malformed.
|
|
402
|
-
"""
|
|
403
|
-
if not wallets_path:
|
|
404
|
-
return []
|
|
405
|
-
path = Path(wallets_path)
|
|
406
|
-
if not path.exists():
|
|
407
|
-
return []
|
|
245
|
+
def _read_wallets_from_config() -> list[dict[str, Any]]:
|
|
408
246
|
try:
|
|
409
|
-
|
|
410
|
-
if isinstance(
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
except (FileNotFoundError, json.JSONDecodeError, OSError) as e:
|
|
414
|
-
logger.warning(f"Failed to read wallets file at {wallets_path}: {e}")
|
|
247
|
+
wallets = CONFIG.get("wallets", [])
|
|
248
|
+
if isinstance(wallets, list):
|
|
249
|
+
return wallets
|
|
250
|
+
logger.warning("Wallets section in config.json is not a list")
|
|
415
251
|
return []
|
|
416
252
|
except Exception as e:
|
|
417
|
-
logger.warning(f"
|
|
253
|
+
logger.warning(f"Failed to read wallets from config.json: {e}")
|
|
418
254
|
return []
|
|
@@ -5,6 +5,8 @@ from eth_account import Account
|
|
|
5
5
|
from eth_utils import to_checksum_address
|
|
6
6
|
from loguru import logger
|
|
7
7
|
from web3 import AsyncHTTPProvider, AsyncWeb3
|
|
8
|
+
from web3.middleware import ExtraDataToPOAMiddleware
|
|
9
|
+
from web3.module import Module
|
|
8
10
|
|
|
9
11
|
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
10
12
|
from wayfinder_paths.core.services.base import EvmTxn
|
|
@@ -20,6 +22,7 @@ GAS_LIMIT_BUFFER_MULTIPLIER = 1.5
|
|
|
20
22
|
|
|
21
23
|
# Chains that don't support EIP-1559 (London) and need legacy gas pricing
|
|
22
24
|
PRE_LONDON_GAS_CHAIN_IDS: set[int] = {56, 42161}
|
|
25
|
+
POA_MIDDLEWARE_CHAIN_IDS: set = {56, 137, 43114}
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
def _looks_like_revert_error(error: Any) -> bool:
|
|
@@ -40,12 +43,23 @@ def _looks_like_revert_error(error: Any) -> bool:
|
|
|
40
43
|
)
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
class HyperModule(Module):
|
|
47
|
+
def __init__(self, w3):
|
|
48
|
+
super().__init__(w3)
|
|
49
|
+
|
|
50
|
+
async def big_block_gas_price(self):
|
|
51
|
+
big_block_gas_price = await self.w3.manager.coro_request(
|
|
52
|
+
"eth_bigBlockGasPrice", []
|
|
53
|
+
)
|
|
54
|
+
return int(big_block_gas_price, 16)
|
|
55
|
+
|
|
56
|
+
|
|
43
57
|
class LocalEvmTxn(EvmTxn):
|
|
44
58
|
"""
|
|
45
|
-
Local wallet provider using private keys stored in config.json or
|
|
59
|
+
Local wallet provider using private keys stored in config.json or config.json.
|
|
46
60
|
|
|
47
61
|
This provider implements the current default behavior:
|
|
48
|
-
- Resolves private keys from config.json or
|
|
62
|
+
- Resolves private keys from config.json or config.json
|
|
49
63
|
- Signs transactions using eth_account
|
|
50
64
|
- Broadcasts transactions via RPC
|
|
51
65
|
"""
|
|
@@ -56,8 +70,12 @@ class LocalEvmTxn(EvmTxn):
|
|
|
56
70
|
|
|
57
71
|
def get_web3(self, chain_id: int) -> AsyncWeb3:
|
|
58
72
|
rpc_url = self._resolve_rpc_url(chain_id)
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
web3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
|
|
74
|
+
if chain_id in POA_MIDDLEWARE_CHAIN_IDS:
|
|
75
|
+
web3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0)
|
|
76
|
+
if chain_id == 999:
|
|
77
|
+
web3.attach_modules({"hype": (HyperModule)})
|
|
78
|
+
return web3
|
|
61
79
|
|
|
62
80
|
def _validate_transaction(self, transaction: dict[str, Any]) -> dict[str, Any]:
|
|
63
81
|
tx = dict(transaction)
|
|
@@ -152,21 +152,21 @@ class Strategy(ABC):
|
|
|
152
152
|
|
|
153
153
|
@staticmethod
|
|
154
154
|
async def policies() -> list[str]:
|
|
155
|
-
"""Return policy strings for this strategy
|
|
155
|
+
"""Return policy strings for this strategy."""
|
|
156
156
|
raise NotImplementedError
|
|
157
157
|
|
|
158
158
|
@abstractmethod
|
|
159
159
|
async def _status(self) -> StatusDict:
|
|
160
160
|
"""
|
|
161
161
|
Return status payload. Subclasses should implement this.
|
|
162
|
-
Should include
|
|
162
|
+
Should include keys (portfolio_value, net_deposit, strategy_status).
|
|
163
163
|
Backward-compatible keys (active_amount, total_earned) may also be included.
|
|
164
164
|
"""
|
|
165
165
|
pass
|
|
166
166
|
|
|
167
167
|
async def status(self) -> StatusDict:
|
|
168
168
|
"""
|
|
169
|
-
Wrapper to compute and return strategy status
|
|
169
|
+
Wrapper to compute and return strategy status and record a snapshot.
|
|
170
170
|
Here we simply delegate to _status for compatibility.
|
|
171
171
|
"""
|
|
172
172
|
|
|
@@ -46,12 +46,19 @@ DEFAULT_TOKEN_REWARDS = [
|
|
|
46
46
|
},
|
|
47
47
|
]
|
|
48
48
|
|
|
49
|
+
DEFAULT_FEE_DESCRIPTION = """Wayfinder deducts a 10% performance fee on profits generated by this vault. Fees are collected from the assets in the vault.
|
|
50
|
+
|
|
51
|
+
If fees remain unpaid, Wayfinder may pause automated management of this vault."""
|
|
52
|
+
|
|
49
53
|
|
|
50
54
|
class StratDescriptor(BaseModel):
|
|
51
55
|
description: str
|
|
52
56
|
|
|
53
57
|
summary: str
|
|
54
58
|
|
|
59
|
+
risk_description: str
|
|
60
|
+
fee_description: str = DEFAULT_FEE_DESCRIPTION
|
|
61
|
+
|
|
55
62
|
gas_token_symbol: str
|
|
56
63
|
gas_token_id: str
|
|
57
64
|
deposit_token_id: str
|
|
@@ -78,9 +78,13 @@ def resolve_rpc_url(
|
|
|
78
78
|
if chain_id is not None and isinstance(mapping, dict):
|
|
79
79
|
by_int = mapping.get(chain_id)
|
|
80
80
|
if by_int:
|
|
81
|
+
if isinstance(by_int, list):
|
|
82
|
+
return str(by_int[0])
|
|
81
83
|
return str(by_int)
|
|
82
84
|
by_str = mapping.get(str(chain_id))
|
|
83
85
|
if by_str:
|
|
86
|
+
if isinstance(by_str, list):
|
|
87
|
+
return str(by_str[0])
|
|
84
88
|
return str(by_str)
|
|
85
89
|
raise ValueError("RPC URL not provided. Set strategy.rpc_urls in config.json.")
|
|
86
90
|
|
|
@@ -133,7 +137,7 @@ def resolve_private_key_for_from_address(
|
|
|
133
137
|
if strategy_addr and from_addr_norm == (strategy_addr or "").lower():
|
|
134
138
|
return strategy_pk
|
|
135
139
|
|
|
136
|
-
# No fallback - private keys must be in config or
|
|
140
|
+
# No fallback - private keys must be in config or config.json
|
|
137
141
|
return None
|
|
138
142
|
|
|
139
143
|
|
|
@@ -6,11 +6,7 @@ from eth_account import Account
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def make_random_wallet() -> dict[str, str]:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Returns a mapping with keys: "address" and "private_key_hex" (0x-prefixed).
|
|
12
|
-
"""
|
|
13
|
-
acct = Account.create() # uses os.urandom
|
|
9
|
+
acct = Account.create()
|
|
14
10
|
return {
|
|
15
11
|
"address": acct.address,
|
|
16
12
|
"private_key_hex": acct.key.hex(),
|
|
@@ -22,33 +18,31 @@ def _load_existing_wallets(file_path: Path) -> list[dict[str, Any]]:
|
|
|
22
18
|
return []
|
|
23
19
|
try:
|
|
24
20
|
parsed = json.loads(file_path.read_text())
|
|
25
|
-
if isinstance(parsed, list):
|
|
26
|
-
return parsed
|
|
27
21
|
if isinstance(parsed, dict):
|
|
28
22
|
wallets = parsed.get("wallets")
|
|
29
23
|
if isinstance(wallets, list):
|
|
30
24
|
return wallets
|
|
31
25
|
return []
|
|
32
26
|
except Exception:
|
|
33
|
-
# If the file is malformed, start fresh rather than raising.
|
|
34
27
|
return []
|
|
35
28
|
|
|
36
29
|
|
|
37
30
|
def _save_wallets(file_path: Path, wallets: list[dict[str, Any]]) -> None:
|
|
38
|
-
|
|
31
|
+
config = {}
|
|
32
|
+
if file_path.exists():
|
|
33
|
+
try:
|
|
34
|
+
config = json.loads(file_path.read_text())
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
39
38
|
sorted_wallets = sorted(wallets, key=lambda w: w.get("address", ""))
|
|
40
|
-
|
|
39
|
+
config["wallets"] = sorted_wallets
|
|
40
|
+
file_path.write_text(json.dumps(config, indent=2))
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def write_wallet_to_json(
|
|
44
|
-
wallet: dict[str, str], out_dir: str | Path = ".", filename: str = "
|
|
44
|
+
wallet: dict[str, str], out_dir: str | Path = ".", filename: str = "config.json"
|
|
45
45
|
) -> Path:
|
|
46
|
-
"""Create or update a wallets.json with the provided wallet.
|
|
47
|
-
|
|
48
|
-
- Ensures the output directory exists.
|
|
49
|
-
- Merges with existing entries keyed by address (updates if present, appends otherwise).
|
|
50
|
-
- Writes a pretty-printed JSON list of wallet objects.
|
|
51
|
-
"""
|
|
52
46
|
out_dir_path = Path(out_dir)
|
|
53
47
|
out_dir_path.mkdir(parents=True, exist_ok=True)
|
|
54
48
|
file_path = out_dir_path / filename
|
|
@@ -71,7 +65,6 @@ def write_wallet_to_json(
|
|
|
71
65
|
|
|
72
66
|
|
|
73
67
|
def load_wallets(
|
|
74
|
-
out_dir: str | Path = ".", filename: str = "
|
|
68
|
+
out_dir: str | Path = ".", filename: str = "config.json"
|
|
75
69
|
) -> list[dict[str, Any]]:
|
|
76
|
-
"""Public helper to read wallets.json as a list of wallet dicts."""
|
|
77
70
|
return _load_existing_wallets(Path(out_dir) / filename)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Wallet Abstraction Layer
|
|
2
2
|
|
|
3
|
-
Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config.json or
|
|
3
|
+
Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config.json or config.json, while `WalletManager` resolves which provider to use at runtime.
|
|
4
4
|
|
|
5
5
|
## Pieces
|
|
6
6
|
|
wayfinder_paths/run_strategy.py
CHANGED
|
@@ -135,14 +135,16 @@ async def run_strategy(
|
|
|
135
135
|
# Load configuration with strategy name for wallet lookup
|
|
136
136
|
logger.debug(f"Config path provided: {config_path}")
|
|
137
137
|
config = load_config(config_path, strategy_name=strategy_name)
|
|
138
|
-
|
|
139
|
-
"Loaded config: creds=%s wallets(main=%s strategy=%s)",
|
|
138
|
+
creds = (
|
|
140
139
|
"yes"
|
|
141
140
|
if (config.user.username and config.user.password)
|
|
142
141
|
or config.user.refresh_token
|
|
143
|
-
else "no"
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
else "no"
|
|
143
|
+
)
|
|
144
|
+
main_wallet = config.user.main_wallet_address or "none"
|
|
145
|
+
strategy_wallet = config.user.strategy_wallet_address or "none"
|
|
146
|
+
logger.debug(
|
|
147
|
+
f"Loaded config: creds={creds} wallets(main={main_wallet} strategy={strategy_wallet})"
|
|
146
148
|
)
|
|
147
149
|
|
|
148
150
|
# Load strategy with the enriched config
|
|
@@ -157,13 +159,13 @@ async def run_strategy(
|
|
|
157
159
|
|
|
158
160
|
# Setup strategy job
|
|
159
161
|
logger.info("Setting up strategy job...")
|
|
160
|
-
|
|
161
|
-
"Auth mode: %s",
|
|
162
|
+
auth_mode = (
|
|
162
163
|
"credentials"
|
|
163
164
|
if (config.user.username and config.user.password)
|
|
164
165
|
or config.user.refresh_token
|
|
165
|
-
else "missing"
|
|
166
|
+
else "missing"
|
|
166
167
|
)
|
|
168
|
+
logger.debug(f"Auth mode: {auth_mode}")
|
|
167
169
|
await strategy_job.setup()
|
|
168
170
|
|
|
169
171
|
# Execute action
|