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.

Files changed (46) hide show
  1. wayfinder_paths/adapters/balance_adapter/adapter.py +3 -7
  2. wayfinder_paths/adapters/brap_adapter/adapter.py +10 -13
  3. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +6 -9
  4. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
  5. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
  6. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
  7. wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
  8. wayfinder_paths/adapters/pool_adapter/README.md +4 -19
  9. wayfinder_paths/adapters/pool_adapter/adapter.py +4 -29
  10. wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
  11. wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
  12. wayfinder_paths/core/clients/AuthClient.py +2 -2
  13. wayfinder_paths/core/clients/BRAPClient.py +2 -2
  14. wayfinder_paths/core/clients/HyperlendClient.py +2 -2
  15. wayfinder_paths/core/clients/PoolClient.py +18 -54
  16. wayfinder_paths/core/clients/TokenClient.py +3 -3
  17. wayfinder_paths/core/clients/WalletClient.py +2 -2
  18. wayfinder_paths/core/clients/WayfinderClient.py +9 -10
  19. wayfinder_paths/core/clients/protocols.py +1 -7
  20. wayfinder_paths/core/config.py +60 -224
  21. wayfinder_paths/core/services/local_evm_txn.py +22 -4
  22. wayfinder_paths/core/strategies/Strategy.py +3 -3
  23. wayfinder_paths/core/strategies/descriptors.py +7 -0
  24. wayfinder_paths/core/utils/evm_helpers.py +5 -1
  25. wayfinder_paths/core/utils/wallets.py +12 -19
  26. wayfinder_paths/core/wallets/README.md +1 -1
  27. wayfinder_paths/run_strategy.py +10 -8
  28. wayfinder_paths/scripts/create_strategy.py +5 -5
  29. wayfinder_paths/scripts/make_wallets.py +5 -5
  30. wayfinder_paths/scripts/run_strategy.py +3 -3
  31. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
  32. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +196 -515
  33. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
  34. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
  35. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -0
  36. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
  37. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +8 -7
  38. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +2 -2
  39. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +25 -25
  40. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +28 -9
  41. wayfinder_paths/templates/adapter/README.md +1 -1
  42. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/METADATA +9 -12
  43. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/RECORD +45 -45
  44. wayfinder_paths/core/settings.py +0 -61
  45. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/LICENSE +0 -0
  46. {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.13.dist-info}/WHEEL +0 -0
@@ -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
- # Wallet configuration
34
- main_wallet_address: str | None = None # User's main wallet address
35
- strategy_wallet_address: str | None = None # Dedicated strategy wallet address
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
- # Execution settings
82
- update_interval: int = 60 # Default update interval in seconds
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 wallets.json: {e}"
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
- """Create StrategyJobConfig from dictionary
303
-
304
- Args:
305
- data: Configuration dictionary
306
- strategy_name: Optional strategy name for per-strategy wallet lookup
307
- """
308
- user_cfg = UserConfig.from_dict(data.get("user", {}))
309
- sys_cfg = SystemConfig.from_dict(data.get("system", {}))
310
- # No auto-population - wallets must be explicitly set in config or matched by label
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
- # --- Internal helpers -------------------------------------------------------
380
-
381
-
382
- def _read_wallets_file(wallets_path: str | None) -> list[dict[str, Any]]:
383
- """
384
- Read wallet entries from a JSON file.
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
- Returns:
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
- data = json.loads(path.read_text())
410
- if isinstance(data, list):
411
- return data
412
- return []
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"Unexpected error reading wallets file at {wallets_path}: {e}")
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 wallets.json.
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 wallets.json
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
- w3 = AsyncWeb3(AsyncHTTPProvider(rpc_url))
60
- return w3
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 (Django-compatible)."""
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 Django-compatible keys (portfolio_value, net_deposit, strategy_status).
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. In Django, this also snapshots.
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 wallets.json
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
- """Generate a new random wallet.
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
- # Ensure stable ordering by address for readability
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
- file_path.write_text(json.dumps(sorted_wallets, indent=2))
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 = "wallets.json"
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 = "wallets.json"
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 wallets.json, while `WalletManager` resolves which provider to use at runtime.
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
 
@@ -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
- logger.debug(
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
- (config.user.main_wallet_address or "none"),
145
- (config.user.strategy_wallet_address or "none"),
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
- logger.debug(
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