wayfinder-paths 0.1.10__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 (49) 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/base.py +0 -55
  22. wayfinder_paths/core/services/local_evm_txn.py +37 -134
  23. wayfinder_paths/core/strategies/Strategy.py +3 -3
  24. wayfinder_paths/core/strategies/descriptors.py +7 -0
  25. wayfinder_paths/core/utils/evm_helpers.py +5 -28
  26. wayfinder_paths/core/utils/wallets.py +12 -19
  27. wayfinder_paths/core/wallets/README.md +1 -1
  28. wayfinder_paths/run_strategy.py +10 -8
  29. wayfinder_paths/scripts/create_strategy.py +5 -5
  30. wayfinder_paths/scripts/make_wallets.py +5 -5
  31. wayfinder_paths/scripts/run_strategy.py +3 -3
  32. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
  33. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +196 -515
  34. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
  35. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
  36. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +1 -0
  37. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
  38. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +8 -7
  39. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +2 -2
  40. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +25 -25
  41. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +28 -9
  42. wayfinder_paths/templates/adapter/README.md +1 -1
  43. {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/METADATA +15 -18
  44. {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/RECORD +46 -48
  45. wayfinder_paths/CONFIG_GUIDE.md +0 -390
  46. wayfinder_paths/config.example.json +0 -22
  47. wayfinder_paths/core/settings.py +0 -61
  48. {wayfinder_paths-0.1.10.dist-info → wayfinder_paths-0.1.13.dist-info}/LICENSE +0 -0
  49. {wayfinder_paths-0.1.10.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 []
@@ -50,61 +50,6 @@ class EvmTxn(ABC):
50
50
  transaction confirmations.
51
51
  """
52
52
 
53
- @abstractmethod
54
- async def get_balance(
55
- self,
56
- address: str,
57
- token_address: str | None,
58
- chain_id: int,
59
- block_identifier: int | str | None = None,
60
- ) -> tuple[bool, Any]:
61
- """
62
- Get balance for an address at a specific block.
63
-
64
- Args:
65
- address: Address to query balance for
66
- token_address: ERC20 token address, or None for native token
67
- chain_id: Chain ID
68
- block_identifier: Block to query at. Can be:
69
- - int: specific block number (for pinning to tx block)
70
- - "safe": OP Stack safe block (data posted to L1)
71
- - "finalized": fully finalized block
72
- - None/"latest": current head (default, but avoid after txs)
73
-
74
- Returns:
75
- Tuple of (success, balance_integer_or_error_message)
76
- """
77
- pass
78
-
79
- @abstractmethod
80
- async def approve_token(
81
- self,
82
- token_address: str,
83
- spender: str,
84
- amount: int,
85
- from_address: str,
86
- chain_id: int,
87
- wait_for_receipt: bool = True,
88
- timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
89
- ) -> tuple[bool, Any]:
90
- """
91
- Approve a spender to spend tokens on behalf of from_address.
92
-
93
- Args:
94
- token_address: ERC20 token contract address
95
- spender: Address being approved to spend tokens
96
- amount: Amount to approve (in token units, not human-readable)
97
- from_address: Address approving the tokens
98
- chain_id: Chain ID
99
- wait_for_receipt: Whether to wait for the transaction receipt
100
- timeout: Receipt timeout in seconds
101
-
102
- Returns:
103
- Tuple of (success, transaction_result_dict_or_error_message)
104
- Transaction result should include 'tx_hash' and optionally 'receipt'
105
- """
106
- pass
107
-
108
53
  @abstractmethod
109
54
  async def broadcast_transaction(
110
55
  self,