wayfinder-paths 0.1.11__py3-none-any.whl → 0.1.14__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/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +36 -39
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +87 -75
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +121 -59
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +22 -23
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +114 -60
- 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 +11 -27
- wayfinder_paths/adapters/pool_adapter/adapter.py +11 -37
- wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +5 -3
- wayfinder_paths/core/clients/BRAPClient.py +103 -62
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +127 -66
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +126 -88
- wayfinder_paths/core/clients/TokenClient.py +92 -37
- wayfinder_paths/core/clients/WalletClient.py +28 -58
- wayfinder_paths/core/clients/WayfinderClient.py +33 -166
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -52
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/config.py +60 -224
- wayfinder_paths/core/engine/StrategyJob.py +7 -55
- wayfinder_paths/core/services/local_evm_txn.py +28 -10
- wayfinder_paths/core/services/local_token_txn.py +1 -1
- wayfinder_paths/core/strategies/Strategy.py +3 -5
- wayfinder_paths/core/strategies/descriptors.py +7 -0
- wayfinder_paths/core/utils/evm_helpers.py +7 -3
- wayfinder_paths/core/utils/wallets.py +12 -19
- wayfinder_paths/core/wallets/README.md +1 -1
- wayfinder_paths/run_strategy.py +8 -17
- 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 +206 -526
- 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 +41 -25
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +10 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +12 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +3 -3
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +110 -78
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +44 -21
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/METADATA +21 -59
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/RECORD +64 -65
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- wayfinder_paths/core/settings.py +0 -61
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.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 []
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import os
|
|
3
2
|
from typing import Any
|
|
4
3
|
|
|
5
4
|
from loguru import logger
|
|
@@ -16,7 +15,6 @@ class StrategyJob:
|
|
|
16
15
|
config: StrategyJobConfig,
|
|
17
16
|
clients: dict[str, Any] | None = None,
|
|
18
17
|
skip_auth: bool = False,
|
|
19
|
-
api_key: str | None = None,
|
|
20
18
|
):
|
|
21
19
|
"""
|
|
22
20
|
Initialize a StrategyJob.
|
|
@@ -26,16 +24,12 @@ class StrategyJob:
|
|
|
26
24
|
config: Strategy job configuration.
|
|
27
25
|
clients: Optional dict of pre-instantiated clients to inject directly.
|
|
28
26
|
skip_auth: If True, skips authentication (for SDK usage).
|
|
29
|
-
api_key: Optional API key for service account authentication.
|
|
30
|
-
If provided, will be passed to ClientManager and strategy.
|
|
31
27
|
"""
|
|
32
28
|
self.strategy = strategy
|
|
33
29
|
self.config = config
|
|
34
30
|
|
|
35
31
|
self.job_id = strategy.name or "unknown"
|
|
36
|
-
self.clients = ClientManager(
|
|
37
|
-
clients=clients, skip_auth=skip_auth, api_key=api_key
|
|
38
|
-
)
|
|
32
|
+
self.clients = ClientManager(clients=clients, skip_auth=skip_auth)
|
|
39
33
|
|
|
40
34
|
def _setup_strategy(self):
|
|
41
35
|
"""Setup the strategy instance"""
|
|
@@ -44,23 +38,6 @@ class StrategyJob:
|
|
|
44
38
|
|
|
45
39
|
self.strategy.log = self.log
|
|
46
40
|
|
|
47
|
-
def _is_using_api_key(self) -> bool:
|
|
48
|
-
"""Check if API key authentication is being used."""
|
|
49
|
-
if self.clients._api_key:
|
|
50
|
-
return True
|
|
51
|
-
|
|
52
|
-
if self.clients.auth:
|
|
53
|
-
try:
|
|
54
|
-
creds = self.clients.auth._load_config_credentials()
|
|
55
|
-
if creds.get("api_key"):
|
|
56
|
-
return True
|
|
57
|
-
if os.getenv("WAYFINDER_API_KEY"):
|
|
58
|
-
return True
|
|
59
|
-
except Exception:
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
return False
|
|
63
|
-
|
|
64
41
|
async def setup(self):
|
|
65
42
|
"""
|
|
66
43
|
Initialize the strategy job and strategy.
|
|
@@ -69,38 +46,13 @@ class StrategyJob:
|
|
|
69
46
|
"""
|
|
70
47
|
self._setup_strategy()
|
|
71
48
|
|
|
72
|
-
# Ensure
|
|
49
|
+
# Ensure API key is set for API calls
|
|
50
|
+
# All clients inherit from WayfinderClient and have _ensure_api_key()
|
|
73
51
|
if not self.clients._skip_auth:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
-
if self.clients.auth:
|
|
79
|
-
await self.clients.auth._ensure_bearer_token()
|
|
80
|
-
else:
|
|
81
|
-
# Try to ensure bearer token is set, authenticate if needed
|
|
82
|
-
try:
|
|
83
|
-
if self.clients.auth:
|
|
84
|
-
await self.clients.auth._ensure_bearer_token()
|
|
85
|
-
except (PermissionError, Exception) as e:
|
|
86
|
-
if not isinstance(e, PermissionError):
|
|
87
|
-
logger.warning(
|
|
88
|
-
f"Authentication failed: {e}, trying OAuth fallback"
|
|
89
|
-
)
|
|
90
|
-
username = self.config.user.username
|
|
91
|
-
password = self.config.user.password
|
|
92
|
-
refresh_token = self.config.user.refresh_token
|
|
93
|
-
if refresh_token or (username and password):
|
|
94
|
-
await self.clients.authenticate(
|
|
95
|
-
username=username,
|
|
96
|
-
password=password,
|
|
97
|
-
refresh_token=refresh_token,
|
|
98
|
-
)
|
|
99
|
-
else:
|
|
100
|
-
raise ValueError(
|
|
101
|
-
"Authentication required: provide api_key parameter for service account auth, "
|
|
102
|
-
"or username+password/refresh_token in config.json for personal access"
|
|
103
|
-
) from e
|
|
52
|
+
# Ensure API key on any client (they all share the same method)
|
|
53
|
+
token_client = self.clients.token
|
|
54
|
+
if token_client:
|
|
55
|
+
token_client._ensure_api_key()
|
|
104
56
|
|
|
105
57
|
existing_cfg = dict(getattr(self.strategy, "config", {}) or {})
|
|
106
58
|
strategy_cfg = dict(self.config.strategy_config or {})
|
|
@@ -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)
|
|
@@ -137,13 +155,13 @@ class LocalEvmTxn(EvmTxn):
|
|
|
137
155
|
gas_price = await self._get_gas_price(w3)
|
|
138
156
|
|
|
139
157
|
transaction["gasPrice"] = int(gas_price * SUGGESTED_GAS_PRICE_MULTIPLIER)
|
|
140
|
-
elif chain_id == 999:
|
|
141
|
-
|
|
158
|
+
# elif chain_id == 999:
|
|
159
|
+
# big_block_gas_price = await w3.hype.big_block_gas_price()
|
|
142
160
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
# transaction["maxFeePerGas"] = int(
|
|
162
|
+
# big_block_gas_price * SUGGESTED_PRIORITY_FEE_MULTIPLIER
|
|
163
|
+
# )
|
|
164
|
+
# transaction["maxPriorityFeePerGas"] = 0
|
|
147
165
|
else:
|
|
148
166
|
base_fee = await self._get_base_fee(w3)
|
|
149
167
|
priority_fee = await self._get_priority_fee(w3)
|
|
@@ -154,7 +154,7 @@ class LocalTokenTxnService(TokenTxn):
|
|
|
154
154
|
|
|
155
155
|
def _chain_id(self, chain: Any) -> int:
|
|
156
156
|
if isinstance(chain, dict):
|
|
157
|
-
chain_id = chain.get("id")
|
|
157
|
+
chain_id = chain.get("id")
|
|
158
158
|
else:
|
|
159
159
|
chain_id = getattr(chain, "id", None)
|
|
160
160
|
if chain_id is None:
|
|
@@ -57,12 +57,10 @@ class Strategy(ABC):
|
|
|
57
57
|
main_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
58
58
|
strategy_wallet: WalletConfig | dict[str, Any] | None = None,
|
|
59
59
|
web3_service: Web3Service | None = None,
|
|
60
|
-
api_key: str | None = None,
|
|
61
60
|
):
|
|
62
61
|
self.adapters = {}
|
|
63
62
|
self.ledger_adapter = None
|
|
64
63
|
self.logger = logger.bind(strategy=self.__class__.__name__)
|
|
65
|
-
# Note: api_key is passed to ClientManager, not set in environment
|
|
66
64
|
self.config = config
|
|
67
65
|
|
|
68
66
|
async def setup(self) -> None:
|
|
@@ -152,21 +150,21 @@ class Strategy(ABC):
|
|
|
152
150
|
|
|
153
151
|
@staticmethod
|
|
154
152
|
async def policies() -> list[str]:
|
|
155
|
-
"""Return policy strings for this strategy
|
|
153
|
+
"""Return policy strings for this strategy."""
|
|
156
154
|
raise NotImplementedError
|
|
157
155
|
|
|
158
156
|
@abstractmethod
|
|
159
157
|
async def _status(self) -> StatusDict:
|
|
160
158
|
"""
|
|
161
159
|
Return status payload. Subclasses should implement this.
|
|
162
|
-
Should include
|
|
160
|
+
Should include keys (portfolio_value, net_deposit, strategy_status).
|
|
163
161
|
Backward-compatible keys (active_amount, total_earned) may also be included.
|
|
164
162
|
"""
|
|
165
163
|
pass
|
|
166
164
|
|
|
167
165
|
async def status(self) -> StatusDict:
|
|
168
166
|
"""
|
|
169
|
-
Wrapper to compute and return strategy status
|
|
167
|
+
Wrapper to compute and return strategy status and record a snapshot.
|
|
170
168
|
Here we simply delegate to _status for compatibility.
|
|
171
169
|
"""
|
|
172
170
|
|
|
@@ -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
|