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
wayfinder_paths/core/config.py
CHANGED
|
@@ -9,6 +9,15 @@ from dataclasses import dataclass, field
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from wayfinder_paths.core.constants.base import (
|
|
15
|
+
ADAPTER_BALANCE,
|
|
16
|
+
ADAPTER_BRAP,
|
|
17
|
+
ADAPTER_HYPERLIQUID,
|
|
18
|
+
ADAPTER_MOONWELL,
|
|
19
|
+
)
|
|
20
|
+
|
|
12
21
|
|
|
13
22
|
@dataclass
|
|
14
23
|
class UserConfig:
|
|
@@ -24,7 +33,7 @@ class UserConfig:
|
|
|
24
33
|
|
|
25
34
|
# Wallet configuration
|
|
26
35
|
main_wallet_address: str | None = None # User's main wallet address
|
|
27
|
-
|
|
36
|
+
strategy_wallet_address: str | None = None # Dedicated strategy wallet address
|
|
28
37
|
|
|
29
38
|
# Optional user preferences
|
|
30
39
|
default_slippage: float = 0.005 # Default slippage tolerance (0.5%)
|
|
@@ -38,7 +47,7 @@ class UserConfig:
|
|
|
38
47
|
password=data.get("password"),
|
|
39
48
|
refresh_token=data.get("refresh_token"),
|
|
40
49
|
main_wallet_address=data.get("main_wallet_address"),
|
|
41
|
-
|
|
50
|
+
strategy_wallet_address=data.get("strategy_wallet_address"),
|
|
42
51
|
default_slippage=data.get("default_slippage", 0.005),
|
|
43
52
|
gas_multiplier=data.get("gas_multiplier", 1.2),
|
|
44
53
|
)
|
|
@@ -50,7 +59,7 @@ class UserConfig:
|
|
|
50
59
|
"password": self.password,
|
|
51
60
|
"refresh_token": self.refresh_token,
|
|
52
61
|
"main_wallet_address": self.main_wallet_address,
|
|
53
|
-
"
|
|
62
|
+
"strategy_wallet_address": self.strategy_wallet_address,
|
|
54
63
|
"default_slippage": self.default_slippage,
|
|
55
64
|
"gas_multiplier": self.gas_multiplier,
|
|
56
65
|
}
|
|
@@ -72,7 +81,7 @@ class SystemConfig:
|
|
|
72
81
|
|
|
73
82
|
# Job configuration
|
|
74
83
|
job_id: str | None = None
|
|
75
|
-
job_type: str = "
|
|
84
|
+
job_type: str = "strategy"
|
|
76
85
|
|
|
77
86
|
# Execution settings
|
|
78
87
|
update_interval: int = 60 # Default update interval in seconds
|
|
@@ -98,7 +107,7 @@ class SystemConfig:
|
|
|
98
107
|
os.getenv("WAYFINDER_API_URL", "https://api.wayfinder.ai"),
|
|
99
108
|
),
|
|
100
109
|
job_id=data.get("job_id"),
|
|
101
|
-
job_type=data.get("job_type", "
|
|
110
|
+
job_type=data.get("job_type", "strategy"),
|
|
102
111
|
update_interval=data.get("update_interval", 60),
|
|
103
112
|
max_retries=data.get("max_retries", 3),
|
|
104
113
|
retry_delay=data.get("retry_delay", 5),
|
|
@@ -127,9 +136,9 @@ class SystemConfig:
|
|
|
127
136
|
|
|
128
137
|
|
|
129
138
|
@dataclass
|
|
130
|
-
class
|
|
139
|
+
class StrategyJobConfig:
|
|
131
140
|
"""
|
|
132
|
-
Complete configuration for a
|
|
141
|
+
Complete configuration for a strategy job
|
|
133
142
|
Combines user and system configurations
|
|
134
143
|
"""
|
|
135
144
|
|
|
@@ -144,7 +153,7 @@ class VaultConfig:
|
|
|
144
153
|
Enrich strategy_config with wallet addresses and private keys from wallets.json.
|
|
145
154
|
|
|
146
155
|
This method automatically loads wallet information from wallets.json to populate
|
|
147
|
-
main_wallet and
|
|
156
|
+
main_wallet and strategy_wallet addresses in strategy_config. Only uses wallets
|
|
148
157
|
with exact label matches (no fallbacks).
|
|
149
158
|
|
|
150
159
|
Wallet enrichment is conditional and can be skipped:
|
|
@@ -153,103 +162,154 @@ class VaultConfig:
|
|
|
153
162
|
- Allows custom wallet providers (Privy/Turnkey) to opt out of file-based enrichment
|
|
154
163
|
|
|
155
164
|
Note:
|
|
156
|
-
This method never raises exceptions - all errors are
|
|
165
|
+
This method never raises exceptions - all errors are logged but do not
|
|
157
166
|
prevent config construction failures.
|
|
158
167
|
"""
|
|
159
168
|
try:
|
|
160
169
|
if not isinstance(self.strategy_config, dict):
|
|
161
170
|
self.strategy_config = {}
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
wallet_type = self.strategy_config.get("wallet_type")
|
|
165
|
-
# Also check in main_wallet and vault_wallet configs
|
|
166
|
-
if not wallet_type:
|
|
167
|
-
main_wallet = self.strategy_config.get("main_wallet")
|
|
168
|
-
if isinstance(main_wallet, dict):
|
|
169
|
-
wallet_type = main_wallet.get("wallet_type")
|
|
170
|
-
if not wallet_type:
|
|
171
|
-
vault_wallet = self.strategy_config.get("vault_wallet")
|
|
172
|
-
if isinstance(vault_wallet, dict):
|
|
173
|
-
wallet_type = vault_wallet.get("wallet_type")
|
|
174
|
-
|
|
175
|
-
# Skip wallets.json enrichment if explicitly using non-local wallet provider
|
|
172
|
+
wallet_type = self._get_wallet_type()
|
|
176
173
|
if wallet_type and wallet_type != "local":
|
|
177
174
|
return
|
|
178
175
|
|
|
179
|
-
|
|
180
|
-
strategy_name = self.strategy_config.get("_strategy_name")
|
|
181
|
-
|
|
182
|
-
# Load wallets from file for enrichment (only for local wallet types)
|
|
183
|
-
entries = _read_wallets_file(self.system.wallets_path)
|
|
184
|
-
by_label = {}
|
|
185
|
-
by_addr = {}
|
|
186
|
-
if entries and isinstance(entries, list):
|
|
187
|
-
for e in entries:
|
|
188
|
-
if isinstance(e, dict):
|
|
189
|
-
# Index by label
|
|
190
|
-
label = e.get("label")
|
|
191
|
-
if isinstance(label, str):
|
|
192
|
-
by_label[label] = e
|
|
193
|
-
# Index by address
|
|
194
|
-
addr = e.get("address")
|
|
195
|
-
if isinstance(addr, str):
|
|
196
|
-
by_addr[addr.lower()] = e
|
|
197
|
-
|
|
198
|
-
# Load main wallet by exact label match only
|
|
199
|
-
if "main_wallet" not in self.strategy_config:
|
|
200
|
-
main_wallet = by_label.get("main")
|
|
201
|
-
if main_wallet:
|
|
202
|
-
self.strategy_config["main_wallet"] = {
|
|
203
|
-
"address": main_wallet["address"]
|
|
204
|
-
}
|
|
176
|
+
by_label, by_addr = self._load_wallets_from_file()
|
|
205
177
|
|
|
206
|
-
|
|
207
|
-
if strategy_name and isinstance(strategy_name, str):
|
|
208
|
-
strategy_wallet = by_label.get(strategy_name)
|
|
209
|
-
if strategy_wallet:
|
|
210
|
-
# Use strategy-specific wallet as vault_wallet
|
|
211
|
-
if "vault_wallet" not in self.strategy_config:
|
|
212
|
-
self.strategy_config["vault_wallet"] = {
|
|
213
|
-
"address": strategy_wallet["address"]
|
|
214
|
-
}
|
|
215
|
-
elif isinstance(self.strategy_config.get("vault_wallet"), dict):
|
|
216
|
-
# Ensure address is set if not already
|
|
217
|
-
if not self.strategy_config["vault_wallet"].get("address"):
|
|
218
|
-
self.strategy_config["vault_wallet"]["address"] = (
|
|
219
|
-
strategy_wallet["address"]
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
# Enrich wallet configs with private keys from wallets.json
|
|
223
|
-
# Only enrich private keys if using local wallet type (or defaulting to local)
|
|
224
|
-
# This ensures custom wallet providers don't get private keys from files
|
|
178
|
+
self._enrich_wallet_addresses(by_label)
|
|
225
179
|
if wallet_type in (None, "local"):
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
wallet_obj = self.strategy_config.get(key)
|
|
229
|
-
if isinstance(wallet_obj, dict):
|
|
230
|
-
addr = (wallet_obj.get("address") or "").lower()
|
|
231
|
-
entry = by_addr.get(addr)
|
|
232
|
-
if entry:
|
|
233
|
-
pk = entry.get("private_key") or entry.get(
|
|
234
|
-
"private_key_hex"
|
|
235
|
-
)
|
|
236
|
-
if (
|
|
237
|
-
pk
|
|
238
|
-
and not wallet_obj.get("private_key")
|
|
239
|
-
and not wallet_obj.get("private_key_hex")
|
|
240
|
-
):
|
|
241
|
-
wallet_obj["private_key_hex"] = pk
|
|
242
|
-
except Exception:
|
|
243
|
-
pass
|
|
244
|
-
except Exception:
|
|
180
|
+
self._enrich_wallet_private_keys(by_addr)
|
|
181
|
+
except Exception as e:
|
|
245
182
|
# Defensive: never allow config construction to fail on enrichment
|
|
246
|
-
|
|
183
|
+
logger.warning(
|
|
184
|
+
f"Failed to enrich strategy config with wallet information: {e}"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _get_wallet_type(self) -> str | None:
|
|
188
|
+
"""
|
|
189
|
+
Determine the wallet type from strategy config.
|
|
190
|
+
|
|
191
|
+
Checks strategy_config, main_wallet, and strategy_wallet for wallet_type.
|
|
192
|
+
Returns the first wallet_type found, or None if not specified.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Wallet type string or None if not specified.
|
|
196
|
+
"""
|
|
197
|
+
wallet_type = self.strategy_config.get("wallet_type")
|
|
198
|
+
if wallet_type:
|
|
199
|
+
return wallet_type
|
|
200
|
+
|
|
201
|
+
main_wallet = self.strategy_config.get("main_wallet")
|
|
202
|
+
if isinstance(main_wallet, dict):
|
|
203
|
+
wallet_type = main_wallet.get("wallet_type")
|
|
204
|
+
if wallet_type:
|
|
205
|
+
return wallet_type
|
|
206
|
+
|
|
207
|
+
strategy_wallet = self.strategy_config.get("strategy_wallet")
|
|
208
|
+
if isinstance(strategy_wallet, dict):
|
|
209
|
+
wallet_type = strategy_wallet.get("wallet_type")
|
|
210
|
+
if wallet_type:
|
|
211
|
+
return wallet_type
|
|
212
|
+
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
def _load_wallets_from_file(
|
|
216
|
+
self,
|
|
217
|
+
) -> tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any]]]:
|
|
218
|
+
"""
|
|
219
|
+
Load wallets from wallets.json file and index by label and address.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Tuple of (by_label, by_addr) dictionaries:
|
|
223
|
+
- by_label: Maps wallet label to wallet entry
|
|
224
|
+
- by_addr: Maps wallet address (lowercase) to wallet entry
|
|
225
|
+
"""
|
|
226
|
+
entries = _read_wallets_file(self.system.wallets_path)
|
|
227
|
+
by_label: dict[str, dict[str, Any]] = {}
|
|
228
|
+
by_addr: dict[str, dict[str, Any]] = {}
|
|
229
|
+
|
|
230
|
+
if entries and isinstance(entries, list):
|
|
231
|
+
for e in entries:
|
|
232
|
+
if isinstance(e, dict):
|
|
233
|
+
# Index by label
|
|
234
|
+
label = e.get("label")
|
|
235
|
+
if isinstance(label, str):
|
|
236
|
+
by_label[label] = e
|
|
237
|
+
# Index by address
|
|
238
|
+
addr = e.get("address")
|
|
239
|
+
if isinstance(addr, str):
|
|
240
|
+
by_addr[addr.lower()] = e
|
|
241
|
+
|
|
242
|
+
return by_label, by_addr
|
|
243
|
+
|
|
244
|
+
def _enrich_wallet_addresses(self, by_label: dict[str, dict[str, Any]]) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Enrich strategy_config with wallet addresses from wallets.json.
|
|
247
|
+
|
|
248
|
+
Loads main_wallet and strategy_wallet addresses by exact label match.
|
|
249
|
+
Only sets addresses if they are not already present in strategy_config.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
by_label: Dictionary mapping wallet labels to wallet entries.
|
|
253
|
+
"""
|
|
254
|
+
# Load main wallet by exact label match only
|
|
255
|
+
if "main_wallet" not in self.strategy_config:
|
|
256
|
+
main_wallet = by_label.get("main")
|
|
257
|
+
if main_wallet:
|
|
258
|
+
self.strategy_config["main_wallet"] = {
|
|
259
|
+
"address": main_wallet["address"]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
# Load strategy wallet by strategy name label match only
|
|
263
|
+
strategy_name = self.strategy_config.get("_strategy_name")
|
|
264
|
+
if strategy_name and isinstance(strategy_name, str):
|
|
265
|
+
strategy_wallet = by_label.get(strategy_name)
|
|
266
|
+
if strategy_wallet:
|
|
267
|
+
# Use strategy-specific wallet as strategy_wallet
|
|
268
|
+
if "strategy_wallet" not in self.strategy_config:
|
|
269
|
+
self.strategy_config["strategy_wallet"] = {
|
|
270
|
+
"address": strategy_wallet["address"]
|
|
271
|
+
}
|
|
272
|
+
elif isinstance(self.strategy_config.get("strategy_wallet"), dict):
|
|
273
|
+
# Ensure address is set if not already
|
|
274
|
+
if not self.strategy_config["strategy_wallet"].get("address"):
|
|
275
|
+
self.strategy_config["strategy_wallet"]["address"] = (
|
|
276
|
+
strategy_wallet["address"]
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def _enrich_wallet_private_keys(self, by_addr: dict[str, dict[str, Any]]) -> None:
|
|
280
|
+
"""
|
|
281
|
+
Enrich wallet configs with private keys from wallets.json.
|
|
282
|
+
|
|
283
|
+
Only enriches private keys if using local wallet type (or defaulting to local).
|
|
284
|
+
This ensures custom wallet providers don't get private keys from files.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
by_addr: Dictionary mapping wallet addresses (lowercase) to wallet entries.
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
for key in ("main_wallet", "strategy_wallet"):
|
|
291
|
+
wallet_obj = self.strategy_config.get(key)
|
|
292
|
+
if isinstance(wallet_obj, dict):
|
|
293
|
+
addr = (wallet_obj.get("address") or "").lower()
|
|
294
|
+
entry = by_addr.get(addr)
|
|
295
|
+
if entry:
|
|
296
|
+
pk = entry.get("private_key") or entry.get("private_key_hex")
|
|
297
|
+
if (
|
|
298
|
+
pk
|
|
299
|
+
and not wallet_obj.get("private_key")
|
|
300
|
+
and not wallet_obj.get("private_key_hex")
|
|
301
|
+
):
|
|
302
|
+
wallet_obj["private_key_hex"] = pk
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning(
|
|
305
|
+
f"Failed to enrich wallet private keys from wallets.json: {e}"
|
|
306
|
+
)
|
|
247
307
|
|
|
248
308
|
@classmethod
|
|
249
309
|
def from_dict(
|
|
250
310
|
cls, data: dict[str, Any], strategy_name: str | None = None
|
|
251
|
-
) -> "
|
|
252
|
-
"""Create
|
|
311
|
+
) -> "StrategyJobConfig":
|
|
312
|
+
"""Create StrategyJobConfig from dictionary
|
|
253
313
|
|
|
254
314
|
Args:
|
|
255
315
|
data: Configuration dictionary
|
|
@@ -287,14 +347,19 @@ class VaultConfig:
|
|
|
287
347
|
|
|
288
348
|
# Add wallet configuration if needed
|
|
289
349
|
# Only use wallets from strategy_config (matched by label) - no fallbacks
|
|
290
|
-
if adapter_name in [
|
|
291
|
-
|
|
350
|
+
if adapter_name in [
|
|
351
|
+
ADAPTER_BALANCE,
|
|
352
|
+
ADAPTER_BRAP,
|
|
353
|
+
ADAPTER_MOONWELL,
|
|
354
|
+
ADAPTER_HYPERLIQUID,
|
|
355
|
+
]:
|
|
356
|
+
strategy_wallet = self.strategy_config.get("strategy_wallet")
|
|
292
357
|
main_wallet = self.strategy_config.get("main_wallet")
|
|
293
|
-
config["
|
|
294
|
-
{"address":
|
|
295
|
-
if
|
|
296
|
-
and isinstance(
|
|
297
|
-
and
|
|
358
|
+
config["strategy_wallet"] = (
|
|
359
|
+
{"address": strategy_wallet["address"]}
|
|
360
|
+
if strategy_wallet
|
|
361
|
+
and isinstance(strategy_wallet, dict)
|
|
362
|
+
and strategy_wallet.get("address")
|
|
298
363
|
else {}
|
|
299
364
|
)
|
|
300
365
|
config["main_wallet"] = (
|
|
@@ -304,13 +369,13 @@ class VaultConfig:
|
|
|
304
369
|
and main_wallet.get("address")
|
|
305
370
|
else {}
|
|
306
371
|
)
|
|
307
|
-
# user_wallet uses
|
|
372
|
+
# user_wallet uses strategy_wallet if available, otherwise main_wallet
|
|
308
373
|
config["user_wallet"] = (
|
|
309
|
-
config.get("
|
|
374
|
+
config.get("strategy_wallet") or config.get("main_wallet") or {}
|
|
310
375
|
)
|
|
311
376
|
|
|
312
377
|
# Add specific settings
|
|
313
|
-
if adapter_name ==
|
|
378
|
+
if adapter_name == ADAPTER_BRAP:
|
|
314
379
|
config["default_slippage"] = self.user.default_slippage
|
|
315
380
|
config["gas_multiplier"] = self.user.gas_multiplier
|
|
316
381
|
|
|
@@ -321,7 +386,7 @@ class VaultConfig:
|
|
|
321
386
|
return config
|
|
322
387
|
|
|
323
388
|
|
|
324
|
-
def load_config_from_env() ->
|
|
389
|
+
def load_config_from_env() -> StrategyJobConfig:
|
|
325
390
|
"""
|
|
326
391
|
Load configuration from environment variables
|
|
327
392
|
This is the simplest way for users to provide configuration
|
|
@@ -331,7 +396,7 @@ def load_config_from_env() -> VaultConfig:
|
|
|
331
396
|
password=os.getenv("WAYFINDER_PASSWORD"),
|
|
332
397
|
refresh_token=os.getenv("WAYFINDER_REFRESH_TOKEN"),
|
|
333
398
|
main_wallet_address=os.getenv("MAIN_WALLET_ADDRESS"),
|
|
334
|
-
|
|
399
|
+
strategy_wallet_address=os.getenv("STRATEGY_WALLET_ADDRESS"),
|
|
335
400
|
default_slippage=float(os.getenv("DEFAULT_SLIPPAGE", "0.005")),
|
|
336
401
|
gas_multiplier=float(os.getenv("GAS_MULTIPLIER", "1.2")),
|
|
337
402
|
)
|
|
@@ -348,13 +413,33 @@ def load_config_from_env() -> VaultConfig:
|
|
|
348
413
|
|
|
349
414
|
# No auto-population - wallets must be explicitly set in environment or matched by label
|
|
350
415
|
|
|
351
|
-
return
|
|
416
|
+
return StrategyJobConfig(user=user_config, system=system_config)
|
|
352
417
|
|
|
353
418
|
|
|
354
419
|
# --- Internal helpers -------------------------------------------------------
|
|
355
420
|
|
|
356
421
|
|
|
357
422
|
def _read_wallets_file(wallets_path: str | None) -> list[dict[str, Any]]:
|
|
423
|
+
"""
|
|
424
|
+
Read wallet entries from a JSON file.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
wallets_path: Path to the wallets.json file. If None or empty, returns empty list.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
List of wallet dictionaries. Each wallet dict should contain:
|
|
431
|
+
- label: Wallet label (str)
|
|
432
|
+
- address: Wallet address (str)
|
|
433
|
+
- private_key or private_key_hex: Private key (str, optional)
|
|
434
|
+
|
|
435
|
+
Returns empty list if file doesn't exist, is invalid JSON, or contains
|
|
436
|
+
non-list data.
|
|
437
|
+
|
|
438
|
+
Note:
|
|
439
|
+
All errors are logged but do not raise exceptions. This allows the
|
|
440
|
+
config system to continue functioning even if wallets.json is missing
|
|
441
|
+
or malformed.
|
|
442
|
+
"""
|
|
358
443
|
if not wallets_path:
|
|
359
444
|
return []
|
|
360
445
|
path = Path(wallets_path)
|
|
@@ -365,5 +450,9 @@ def _read_wallets_file(wallets_path: str | None) -> list[dict[str, Any]]:
|
|
|
365
450
|
if isinstance(data, list):
|
|
366
451
|
return data
|
|
367
452
|
return []
|
|
368
|
-
except
|
|
453
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError) as e:
|
|
454
|
+
logger.warning(f"Failed to read wallets file at {wallets_path}: {e}")
|
|
455
|
+
return []
|
|
456
|
+
except Exception as e:
|
|
457
|
+
logger.warning(f"Unexpected error reading wallets file at {wallets_path}: {e}")
|
|
369
458
|
return []
|
|
@@ -23,3 +23,20 @@ DEFAULT_GAS_ESTIMATE_FALLBACK = 100000
|
|
|
23
23
|
GAS_BUFFER_MULTIPLIER = 1.1 # 10% buffer for native sends
|
|
24
24
|
ONE_GWEI = 1_000_000_000
|
|
25
25
|
DEFAULT_SLIPPAGE = 0.005
|
|
26
|
+
|
|
27
|
+
# Timeout constants (seconds)
|
|
28
|
+
DEFAULT_TRANSACTION_TIMEOUT = 120 # Transaction receipt wait timeout
|
|
29
|
+
DEFAULT_HTTP_TIMEOUT = 30.0 # HTTP client timeout
|
|
30
|
+
|
|
31
|
+
# Adapter type identifiers
|
|
32
|
+
ADAPTER_BALANCE = "BALANCE"
|
|
33
|
+
ADAPTER_BRAP = "BRAP"
|
|
34
|
+
ADAPTER_MOONWELL = "MOONWELL"
|
|
35
|
+
ADAPTER_HYPERLIQUID = "HYPERLIQUID"
|
|
36
|
+
ADAPTER_POOL = "POOL"
|
|
37
|
+
ADAPTER_TOKEN = "TOKEN"
|
|
38
|
+
ADAPTER_LEDGER = "LEDGER"
|
|
39
|
+
ADAPTER_HYPERLEND = "HYPERLEND"
|
|
40
|
+
|
|
41
|
+
# Pagination defaults
|
|
42
|
+
DEFAULT_PAGINATION_LIMIT = 50 # Default limit for paginated API responses
|
|
@@ -5,25 +5,25 @@ from typing import Any
|
|
|
5
5
|
from loguru import logger
|
|
6
6
|
|
|
7
7
|
from wayfinder_paths.core.clients.ClientManager import ClientManager
|
|
8
|
-
from wayfinder_paths.core.config import
|
|
8
|
+
from wayfinder_paths.core.config import StrategyJobConfig
|
|
9
9
|
from wayfinder_paths.core.strategies.Strategy import Strategy
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class
|
|
12
|
+
class StrategyJob:
|
|
13
13
|
def __init__(
|
|
14
14
|
self,
|
|
15
15
|
strategy: Strategy,
|
|
16
|
-
config:
|
|
16
|
+
config: StrategyJobConfig,
|
|
17
17
|
clients: dict[str, Any] | None = None,
|
|
18
18
|
skip_auth: bool = False,
|
|
19
19
|
api_key: str | None = None,
|
|
20
20
|
):
|
|
21
21
|
"""
|
|
22
|
-
Initialize a
|
|
22
|
+
Initialize a StrategyJob.
|
|
23
23
|
|
|
24
24
|
Args:
|
|
25
25
|
strategy: The strategy to execute.
|
|
26
|
-
config:
|
|
26
|
+
config: Strategy job configuration.
|
|
27
27
|
clients: Optional dict of pre-instantiated clients to inject directly.
|
|
28
28
|
skip_auth: If True, skips authentication (for SDK usage).
|
|
29
29
|
api_key: Optional API key for service account authentication.
|
|
@@ -40,7 +40,7 @@ class VaultJob:
|
|
|
40
40
|
def _setup_strategy(self):
|
|
41
41
|
"""Setup the strategy instance"""
|
|
42
42
|
if not self.strategy:
|
|
43
|
-
raise ValueError("No strategy provided to
|
|
43
|
+
raise ValueError("No strategy provided to StrategyJob")
|
|
44
44
|
|
|
45
45
|
self.strategy.log = self.log
|
|
46
46
|
|
|
@@ -63,7 +63,7 @@ class VaultJob:
|
|
|
63
63
|
|
|
64
64
|
async def setup(self):
|
|
65
65
|
"""
|
|
66
|
-
Initialize the
|
|
66
|
+
Initialize the strategy job and strategy.
|
|
67
67
|
|
|
68
68
|
Sets up authentication and initializes the strategy with merged configuration.
|
|
69
69
|
"""
|
|
@@ -103,8 +103,8 @@ class VaultJob:
|
|
|
103
103
|
) from e
|
|
104
104
|
|
|
105
105
|
existing_cfg = dict(getattr(self.strategy, "config", {}) or {})
|
|
106
|
-
|
|
107
|
-
merged_cfg = {**
|
|
106
|
+
strategy_cfg = dict(self.config.strategy_config or {})
|
|
107
|
+
merged_cfg = {**strategy_cfg, **existing_cfg}
|
|
108
108
|
self.strategy.config = merged_cfg
|
|
109
109
|
self.strategy.clients = self.clients
|
|
110
110
|
await self.strategy.setup()
|
|
@@ -160,23 +160,29 @@ class VaultJob:
|
|
|
160
160
|
logger.error(f"Error in continuous execution: {str(e)}")
|
|
161
161
|
await asyncio.sleep(interval)
|
|
162
162
|
|
|
163
|
-
async def message_user(self, msg):
|
|
164
|
-
if msg is not None:
|
|
165
|
-
logger.info(f"notifying user: {msg}")
|
|
166
|
-
await self.clients.chat.send_msg_to_owner(
|
|
167
|
-
msg, "agent", self.clients.chat.session_id
|
|
168
|
-
)
|
|
169
|
-
|
|
170
163
|
async def log(self, msg: str):
|
|
171
164
|
"""Log messages for the job"""
|
|
172
165
|
logger.info(f"Job {self.job_id}: {msg}")
|
|
173
166
|
|
|
174
|
-
async def handle_error(self, error_data):
|
|
167
|
+
async def handle_error(self, error_data: dict[str, Any]) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Handle errors that occur during strategy execution.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
error_data: Dictionary containing error information. Expected keys:
|
|
173
|
+
- error: Error message or exception string
|
|
174
|
+
- action: Strategy action that failed (e.g., "deposit", "update")
|
|
175
|
+
|
|
176
|
+
Note:
|
|
177
|
+
Base implementation is a no-op. Subclasses or external systems
|
|
178
|
+
can override this method to implement custom error handling,
|
|
179
|
+
logging, alerting, or recovery logic.
|
|
180
|
+
"""
|
|
175
181
|
pass
|
|
176
182
|
|
|
177
183
|
async def stop(self):
|
|
178
|
-
"""Stop the
|
|
184
|
+
"""Stop the strategy job and cleanup"""
|
|
179
185
|
if hasattr(self.strategy, "stop"):
|
|
180
186
|
await self.strategy.stop()
|
|
181
187
|
|
|
182
|
-
logger.info(f"
|
|
188
|
+
logger.info(f"Strategy job {self.job_id} stopped")
|
|
@@ -40,7 +40,7 @@ class StrategyManifest(BaseModel):
|
|
|
40
40
|
schema_version: str = Field(default="0.1")
|
|
41
41
|
entrypoint: str = Field(
|
|
42
42
|
...,
|
|
43
|
-
description="Python path to class, e.g.
|
|
43
|
+
description="Python path to class, e.g. strategies.funding_rate_strategy.FundingRateStrategy",
|
|
44
44
|
)
|
|
45
45
|
name: str | None = Field(
|
|
46
46
|
default=None,
|
|
@@ -3,6 +3,8 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from web3 import AsyncWeb3
|
|
5
5
|
|
|
6
|
+
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
class TokenTxn(ABC):
|
|
8
10
|
"""Interface describing high-level EVM transaction builders."""
|
|
@@ -43,7 +45,7 @@ class EvmTxn(ABC):
|
|
|
43
45
|
Abstract base class for wallet providers.
|
|
44
46
|
|
|
45
47
|
This interface abstracts all blockchain interactions needed by adapters so the
|
|
46
|
-
rest of the
|
|
48
|
+
rest of the codebase never touches raw web3 primitives. Implementations
|
|
47
49
|
are responsible for RPC resolution, gas estimation, signing, broadcasting and
|
|
48
50
|
transaction confirmations.
|
|
49
51
|
"""
|
|
@@ -77,7 +79,7 @@ class EvmTxn(ABC):
|
|
|
77
79
|
from_address: str,
|
|
78
80
|
chain_id: int,
|
|
79
81
|
wait_for_receipt: bool = True,
|
|
80
|
-
timeout: int =
|
|
82
|
+
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
81
83
|
) -> tuple[bool, Any]:
|
|
82
84
|
"""
|
|
83
85
|
Approve a spender to spend tokens on behalf of from_address.
|
|
@@ -103,7 +105,7 @@ class EvmTxn(ABC):
|
|
|
103
105
|
transaction: dict[str, Any],
|
|
104
106
|
*,
|
|
105
107
|
wait_for_receipt: bool = True,
|
|
106
|
-
timeout: int =
|
|
108
|
+
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
107
109
|
) -> tuple[bool, Any]:
|
|
108
110
|
"""
|
|
109
111
|
Sign and broadcast a transaction dict.
|
|
@@ -165,7 +167,7 @@ class Web3Service(ABC):
|
|
|
165
167
|
transaction: dict[str, Any],
|
|
166
168
|
*,
|
|
167
169
|
wait_for_receipt: bool = True,
|
|
168
|
-
timeout: int =
|
|
170
|
+
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
169
171
|
) -> tuple[bool, Any]:
|
|
170
172
|
"""Proxy convenience wrapper to underlying wallet provider."""
|
|
171
173
|
return await self.evm_transactions.broadcast_transaction(
|
|
@@ -11,6 +11,7 @@ from wayfinder_paths.core.constants import (
|
|
|
11
11
|
ONE_GWEI,
|
|
12
12
|
ZERO_ADDRESS,
|
|
13
13
|
)
|
|
14
|
+
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
14
15
|
from wayfinder_paths.core.constants.erc20_abi import (
|
|
15
16
|
ERC20_APPROVAL_ABI,
|
|
16
17
|
ERC20_MINIMAL_ABI,
|
|
@@ -142,7 +143,7 @@ class LocalEvmTxn(EvmTxn):
|
|
|
142
143
|
from_address: str,
|
|
143
144
|
chain_id: int,
|
|
144
145
|
wait_for_receipt: bool = True,
|
|
145
|
-
timeout: int =
|
|
146
|
+
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
146
147
|
) -> tuple[bool, Any]:
|
|
147
148
|
"""
|
|
148
149
|
Approve a spender to spend tokens on behalf of from_address.
|
|
@@ -185,7 +186,7 @@ class LocalEvmTxn(EvmTxn):
|
|
|
185
186
|
transaction: dict[str, Any],
|
|
186
187
|
*,
|
|
187
188
|
wait_for_receipt: bool = True,
|
|
188
|
-
timeout: int =
|
|
189
|
+
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
189
190
|
) -> tuple[bool, Any]:
|
|
190
191
|
"""
|
|
191
192
|
Sign and broadcast a transaction dict.
|
wayfinder_paths/core/settings.py
CHANGED
|
@@ -7,7 +7,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
7
7
|
|
|
8
8
|
class CoreSettings(BaseSettings):
|
|
9
9
|
"""
|
|
10
|
-
Core settings for Wayfinder
|
|
10
|
+
Core settings for Wayfinder Paths Engine
|
|
11
11
|
These are minimal settings required by the core engine
|
|
12
12
|
"""
|
|
13
13
|
|
|
@@ -51,7 +51,7 @@ class CoreSettings(BaseSettings):
|
|
|
51
51
|
|
|
52
52
|
# Logging
|
|
53
53
|
LOG_LEVEL: str = Field("INFO", env="LOG_LEVEL")
|
|
54
|
-
LOG_FILE: str = Field("logs/
|
|
54
|
+
LOG_FILE: str = Field("logs/strategy.log", env="LOG_FILE")
|
|
55
55
|
|
|
56
56
|
# Safety
|
|
57
57
|
DRY_RUN: bool = Field(False, env="DRY_RUN")
|