wayfinder-paths 0.1.7__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 (149) hide show
  1. wayfinder_paths/CONFIG_GUIDE.md +399 -0
  2. wayfinder_paths/__init__.py +22 -0
  3. wayfinder_paths/abis/generic/erc20.json +383 -0
  4. wayfinder_paths/adapters/__init__.py +0 -0
  5. wayfinder_paths/adapters/balance_adapter/README.md +94 -0
  6. wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
  7. wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
  8. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  9. wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
  10. wayfinder_paths/adapters/brap_adapter/README.md +249 -0
  11. wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
  12. wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
  13. wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
  15. wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
  16. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
  18. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
  19. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
  20. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  21. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  23. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  24. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  28. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  29. wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
  30. wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
  31. wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
  32. wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
  33. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
  34. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
  35. wayfinder_paths/adapters/pool_adapter/README.md +206 -0
  36. wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
  37. wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
  38. wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
  39. wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
  40. wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
  41. wayfinder_paths/adapters/token_adapter/README.md +101 -0
  42. wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
  43. wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
  44. wayfinder_paths/adapters/token_adapter/examples.json +26 -0
  45. wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
  46. wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
  47. wayfinder_paths/config.example.json +22 -0
  48. wayfinder_paths/conftest.py +31 -0
  49. wayfinder_paths/core/__init__.py +18 -0
  50. wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
  51. wayfinder_paths/core/adapters/__init__.py +5 -0
  52. wayfinder_paths/core/adapters/base.py +5 -0
  53. wayfinder_paths/core/adapters/models.py +46 -0
  54. wayfinder_paths/core/analytics/__init__.py +11 -0
  55. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  56. wayfinder_paths/core/analytics/stats.py +48 -0
  57. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  58. wayfinder_paths/core/clients/AuthClient.py +83 -0
  59. wayfinder_paths/core/clients/BRAPClient.py +109 -0
  60. wayfinder_paths/core/clients/ClientManager.py +210 -0
  61. wayfinder_paths/core/clients/HyperlendClient.py +192 -0
  62. wayfinder_paths/core/clients/LedgerClient.py +443 -0
  63. wayfinder_paths/core/clients/PoolClient.py +128 -0
  64. wayfinder_paths/core/clients/SimulationClient.py +192 -0
  65. wayfinder_paths/core/clients/TokenClient.py +89 -0
  66. wayfinder_paths/core/clients/TransactionClient.py +63 -0
  67. wayfinder_paths/core/clients/WalletClient.py +94 -0
  68. wayfinder_paths/core/clients/WayfinderClient.py +269 -0
  69. wayfinder_paths/core/clients/__init__.py +48 -0
  70. wayfinder_paths/core/clients/protocols.py +392 -0
  71. wayfinder_paths/core/clients/sdk_example.py +110 -0
  72. wayfinder_paths/core/config.py +458 -0
  73. wayfinder_paths/core/constants/__init__.py +26 -0
  74. wayfinder_paths/core/constants/base.py +42 -0
  75. wayfinder_paths/core/constants/erc20_abi.py +118 -0
  76. wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
  77. wayfinder_paths/core/engine/StrategyJob.py +188 -0
  78. wayfinder_paths/core/engine/__init__.py +5 -0
  79. wayfinder_paths/core/engine/manifest.py +97 -0
  80. wayfinder_paths/core/services/__init__.py +0 -0
  81. wayfinder_paths/core/services/base.py +179 -0
  82. wayfinder_paths/core/services/local_evm_txn.py +430 -0
  83. wayfinder_paths/core/services/local_token_txn.py +231 -0
  84. wayfinder_paths/core/services/web3_service.py +45 -0
  85. wayfinder_paths/core/settings.py +61 -0
  86. wayfinder_paths/core/strategies/Strategy.py +280 -0
  87. wayfinder_paths/core/strategies/__init__.py +5 -0
  88. wayfinder_paths/core/strategies/base.py +7 -0
  89. wayfinder_paths/core/strategies/descriptors.py +81 -0
  90. wayfinder_paths/core/utils/__init__.py +1 -0
  91. wayfinder_paths/core/utils/evm_helpers.py +206 -0
  92. wayfinder_paths/core/utils/wallets.py +77 -0
  93. wayfinder_paths/core/wallets/README.md +91 -0
  94. wayfinder_paths/core/wallets/WalletManager.py +56 -0
  95. wayfinder_paths/core/wallets/__init__.py +7 -0
  96. wayfinder_paths/policies/enso.py +17 -0
  97. wayfinder_paths/policies/erc20.py +34 -0
  98. wayfinder_paths/policies/evm.py +21 -0
  99. wayfinder_paths/policies/hyper_evm.py +19 -0
  100. wayfinder_paths/policies/hyperlend.py +12 -0
  101. wayfinder_paths/policies/hyperliquid.py +30 -0
  102. wayfinder_paths/policies/moonwell.py +54 -0
  103. wayfinder_paths/policies/prjx.py +30 -0
  104. wayfinder_paths/policies/util.py +27 -0
  105. wayfinder_paths/run_strategy.py +411 -0
  106. wayfinder_paths/scripts/__init__.py +0 -0
  107. wayfinder_paths/scripts/create_strategy.py +181 -0
  108. wayfinder_paths/scripts/make_wallets.py +169 -0
  109. wayfinder_paths/scripts/run_strategy.py +124 -0
  110. wayfinder_paths/scripts/validate_manifests.py +213 -0
  111. wayfinder_paths/strategies/__init__.py +0 -0
  112. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  113. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  114. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  115. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  116. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  117. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  118. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  119. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  120. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  121. wayfinder_paths/strategies/config.py +85 -0
  122. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
  123. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
  124. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
  125. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
  126. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
  127. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
  128. wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
  129. wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
  130. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
  131. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
  132. wayfinder_paths/templates/adapter/README.md +105 -0
  133. wayfinder_paths/templates/adapter/adapter.py +26 -0
  134. wayfinder_paths/templates/adapter/examples.json +8 -0
  135. wayfinder_paths/templates/adapter/manifest.yaml +6 -0
  136. wayfinder_paths/templates/adapter/test_adapter.py +49 -0
  137. wayfinder_paths/templates/strategy/README.md +153 -0
  138. wayfinder_paths/templates/strategy/examples.json +11 -0
  139. wayfinder_paths/templates/strategy/manifest.yaml +8 -0
  140. wayfinder_paths/templates/strategy/strategy.py +57 -0
  141. wayfinder_paths/templates/strategy/test_strategy.py +197 -0
  142. wayfinder_paths/tests/__init__.py +0 -0
  143. wayfinder_paths/tests/test_smoke_manifest.py +48 -0
  144. wayfinder_paths/tests/test_test_coverage.py +212 -0
  145. wayfinder_paths/tests/test_utils.py +64 -0
  146. wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
  147. wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
  148. wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
  149. wayfinder_paths-0.1.7.dist-info/WHEEL +4 -0
@@ -0,0 +1,110 @@
1
+ """
2
+ SDK Usage Examples
3
+
4
+ Demonstrates how to use the SDK with custom client implementations.
5
+ Use cases: mocks for testing, caching layers, alternative endpoints, rate limiting.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from wayfinder_paths.core.clients.ClientManager import ClientManager
11
+ from wayfinder_paths.core.clients.TokenClient import TokenClient
12
+
13
+
14
+ class CachedTokenClient:
15
+ """Token client with in-memory caching"""
16
+
17
+ def __init__(self):
18
+ self._cache: dict[str, dict[str, Any]] = {}
19
+ self._default_client = TokenClient()
20
+
21
+ async def get_token_details(
22
+ self, token_id: str, force_refresh: bool = False
23
+ ) -> dict[str, Any]:
24
+ cache_key = f"token_{token_id}"
25
+ if not force_refresh and cache_key in self._cache:
26
+ return self._cache[cache_key]
27
+ data = await self._default_client.get_token_details(token_id, force_refresh)
28
+ self._cache[cache_key] = data
29
+ return data
30
+
31
+ async def get_gas_token(self, chain_code: str) -> dict[str, Any]:
32
+ return await self._default_client.get_gas_token(chain_code)
33
+
34
+
35
+ class MockHyperlendClient:
36
+ """Mock client for testing"""
37
+
38
+ async def get_stable_markets(
39
+ self,
40
+ *,
41
+ chain_id: int,
42
+ required_underlying_tokens: float | None = None,
43
+ buffer_bps: int | None = None,
44
+ min_buffer_tokens: float | None = None,
45
+ is_stable_symbol: bool | None = None,
46
+ ) -> dict[str, Any]:
47
+ return {
48
+ "markets": [
49
+ {
50
+ "chain_id": chain_id,
51
+ "token_address": "0xMockToken",
52
+ "symbol": "USDC",
53
+ "lend_rate": 0.05,
54
+ "available_liquidity": 1000000.0,
55
+ }
56
+ ]
57
+ }
58
+
59
+ async def get_assets_view(
60
+ self,
61
+ *,
62
+ chain_id: int,
63
+ user_address: str,
64
+ ) -> dict[str, Any]:
65
+ return {
66
+ "user_address": user_address,
67
+ "chain_id": chain_id,
68
+ "assets": [],
69
+ }
70
+
71
+ async def get_market_entry(
72
+ self,
73
+ *,
74
+ chain_id: int,
75
+ token_address: str,
76
+ ) -> dict[str, Any]:
77
+ return {
78
+ "chain_id": chain_id,
79
+ "token_address": token_address,
80
+ "market_data": {},
81
+ }
82
+
83
+ async def get_lend_rate_history(
84
+ self,
85
+ *,
86
+ chain_id: int,
87
+ token_address: str,
88
+ lookback_hours: int,
89
+ ) -> dict[str, Any]:
90
+ return {
91
+ "chain_id": chain_id,
92
+ "token_address": token_address,
93
+ "rates": [],
94
+ }
95
+
96
+
97
+ async def example_sdk_usage():
98
+ """Direct client injection - inject only what you customize"""
99
+
100
+ custom_token_client = CachedTokenClient()
101
+ custom_hyperlend_client = MockHyperlendClient()
102
+
103
+ ClientManager(
104
+ clients={
105
+ "token": custom_token_client,
106
+ "hyperlend": custom_hyperlend_client,
107
+ },
108
+ skip_auth=True,
109
+ )
110
+ pass
@@ -0,0 +1,458 @@
1
+ """
2
+ Core Configuration System
3
+ Separates user-provided configuration from system configuration
4
+ """
5
+
6
+ import json
7
+ import os
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Any
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
+
21
+
22
+ @dataclass
23
+ class UserConfig:
24
+ """
25
+ User-provided configuration
26
+ These are values that users MUST provide to run strategies
27
+ """
28
+
29
+ # Credential-based auth (JWT)
30
+ username: str | None = None
31
+ password: str | None = None
32
+ refresh_token: str | None = None
33
+
34
+ # Wallet configuration
35
+ main_wallet_address: str | None = None # User's main wallet address
36
+ strategy_wallet_address: str | None = None # Dedicated strategy wallet address
37
+
38
+ # Optional user preferences
39
+ default_slippage: float = 0.005 # Default slippage tolerance (0.5%)
40
+ gas_multiplier: float = 1.2 # Gas limit multiplier for safety
41
+
42
+ @classmethod
43
+ def from_dict(cls, data: dict[str, Any]) -> "UserConfig":
44
+ """Create UserConfig from dictionary"""
45
+ return cls(
46
+ username=data.get("username"),
47
+ password=data.get("password"),
48
+ refresh_token=data.get("refresh_token"),
49
+ main_wallet_address=data.get("main_wallet_address"),
50
+ strategy_wallet_address=data.get("strategy_wallet_address"),
51
+ default_slippage=data.get("default_slippage", 0.005),
52
+ gas_multiplier=data.get("gas_multiplier", 1.2),
53
+ )
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ """Convert to dictionary"""
57
+ return {
58
+ "username": self.username,
59
+ "password": self.password,
60
+ "refresh_token": self.refresh_token,
61
+ "main_wallet_address": self.main_wallet_address,
62
+ "strategy_wallet_address": self.strategy_wallet_address,
63
+ "default_slippage": self.default_slippage,
64
+ "gas_multiplier": self.gas_multiplier,
65
+ }
66
+
67
+
68
+ @dataclass
69
+ class SystemConfig:
70
+ """
71
+ System-level configuration
72
+ These are values managed by the Wayfinder system
73
+ """
74
+
75
+ # API endpoints (populated from environment or defaults)
76
+ api_base_url: str = field(
77
+ default_factory=lambda: os.getenv(
78
+ "WAYFINDER_API_URL", "https://api.wayfinder.ai"
79
+ )
80
+ )
81
+
82
+ # Job configuration
83
+ job_id: str | None = None
84
+ job_type: str = "strategy"
85
+
86
+ # Execution settings
87
+ update_interval: int = 60 # Default update interval in seconds
88
+ max_retries: int = 3 # Maximum retries for failed operations
89
+ retry_delay: int = 5 # Delay between retries in seconds
90
+
91
+ # System paths
92
+ log_path: str | None = None
93
+ data_path: str | None = None
94
+
95
+ # Local wallets.json path used to auto-populate wallet addresses when not provided
96
+ wallets_path: str | None = "wallets.json"
97
+
98
+ # Optional wallet_id for policy rendering
99
+ wallet_id: str | None = None
100
+
101
+ @classmethod
102
+ def from_dict(cls, data: dict[str, Any]) -> "SystemConfig":
103
+ """Create SystemConfig from dictionary"""
104
+ return cls(
105
+ api_base_url=data.get(
106
+ "api_base_url",
107
+ os.getenv("WAYFINDER_API_URL", "https://api.wayfinder.ai"),
108
+ ),
109
+ job_id=data.get("job_id"),
110
+ job_type=data.get("job_type", "strategy"),
111
+ update_interval=data.get("update_interval", 60),
112
+ max_retries=data.get("max_retries", 3),
113
+ retry_delay=data.get("retry_delay", 5),
114
+ log_path=data.get("log_path"),
115
+ data_path=data.get("data_path"),
116
+ wallets_path=data.get(
117
+ "wallets_path", os.getenv("WALLETS_PATH", "wallets.json")
118
+ ),
119
+ wallet_id=data.get("wallet_id") or os.getenv("WALLET_ID"),
120
+ )
121
+
122
+ def to_dict(self) -> dict[str, Any]:
123
+ """Convert to dictionary"""
124
+ return {
125
+ "api_base_url": self.api_base_url,
126
+ "job_id": self.job_id,
127
+ "job_type": self.job_type,
128
+ "update_interval": self.update_interval,
129
+ "max_retries": self.max_retries,
130
+ "retry_delay": self.retry_delay,
131
+ "log_path": self.log_path,
132
+ "data_path": self.data_path,
133
+ "wallets_path": self.wallets_path,
134
+ "wallet_id": self.wallet_id,
135
+ }
136
+
137
+
138
+ @dataclass
139
+ class StrategyJobConfig:
140
+ """
141
+ Complete configuration for a strategy job
142
+ Combines user and system configurations
143
+ """
144
+
145
+ user: UserConfig
146
+ system: SystemConfig
147
+ strategy_config: dict[str, Any] = field(
148
+ default_factory=dict
149
+ ) # Strategy-specific configuration
150
+
151
+ def __post_init__(self) -> None:
152
+ """
153
+ Enrich strategy_config with wallet addresses and private keys from wallets.json.
154
+
155
+ This method automatically loads wallet information from wallets.json to populate
156
+ main_wallet and strategy_wallet addresses in strategy_config. Only uses wallets
157
+ with exact label matches (no fallbacks).
158
+
159
+ Wallet enrichment is conditional and can be skipped:
160
+ - Skipped if wallet_type is explicitly set to a non-"local" value
161
+ - Only performed if wallet_type is None, "local", or not specified
162
+ - Allows custom wallet providers (Privy/Turnkey) to opt out of file-based enrichment
163
+
164
+ Note:
165
+ This method never raises exceptions - all errors are logged but do not
166
+ prevent config construction failures.
167
+ """
168
+ try:
169
+ if not isinstance(self.strategy_config, dict):
170
+ self.strategy_config = {}
171
+
172
+ wallet_type = self._get_wallet_type()
173
+ if wallet_type and wallet_type != "local":
174
+ return
175
+
176
+ by_label, by_addr = self._load_wallets_from_file()
177
+
178
+ self._enrich_wallet_addresses(by_label)
179
+ if wallet_type in (None, "local"):
180
+ self._enrich_wallet_private_keys(by_addr)
181
+ except Exception as e:
182
+ # Defensive: never allow config construction to fail on enrichment
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
+ )
307
+
308
+ @classmethod
309
+ def from_dict(
310
+ cls, data: dict[str, Any], strategy_name: str | None = None
311
+ ) -> "StrategyJobConfig":
312
+ """Create StrategyJobConfig from dictionary
313
+
314
+ Args:
315
+ data: Configuration dictionary
316
+ strategy_name: Optional strategy name for per-strategy wallet lookup
317
+ """
318
+ user_cfg = UserConfig.from_dict(data.get("user", {}))
319
+ sys_cfg = SystemConfig.from_dict(data.get("system", {}))
320
+ # No auto-population - wallets must be explicitly set in config or matched by label
321
+ strategy_config = data.get("strategy", {})
322
+ # Store strategy name in config for wallet lookup
323
+ if strategy_name:
324
+ strategy_config["_strategy_name"] = strategy_name
325
+ return cls(
326
+ user=user_cfg,
327
+ system=sys_cfg,
328
+ strategy_config=strategy_config,
329
+ )
330
+
331
+ def to_dict(self) -> dict[str, Any]:
332
+ """Convert to dictionary"""
333
+ return {
334
+ "user": self.user.to_dict(),
335
+ "system": self.system.to_dict(),
336
+ "strategy": self.strategy_config,
337
+ }
338
+
339
+ def get_adapter_config(self, adapter_name: str) -> dict[str, Any]:
340
+ """
341
+ Get configuration for a specific adapter
342
+ Combines relevant user and system settings
343
+ """
344
+ config = {
345
+ "api_base_url": self.system.api_base_url,
346
+ }
347
+
348
+ # Add wallet configuration if needed
349
+ # Only use wallets from strategy_config (matched by label) - no fallbacks
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")
357
+ main_wallet = self.strategy_config.get("main_wallet")
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")
363
+ else {}
364
+ )
365
+ config["main_wallet"] = (
366
+ {"address": main_wallet["address"]}
367
+ if main_wallet
368
+ and isinstance(main_wallet, dict)
369
+ and main_wallet.get("address")
370
+ else {}
371
+ )
372
+ # user_wallet uses strategy_wallet if available, otherwise main_wallet
373
+ config["user_wallet"] = (
374
+ config.get("strategy_wallet") or config.get("main_wallet") or {}
375
+ )
376
+
377
+ # Add specific settings
378
+ if adapter_name == ADAPTER_BRAP:
379
+ config["default_slippage"] = self.user.default_slippage
380
+ config["gas_multiplier"] = self.user.gas_multiplier
381
+
382
+ # Add any strategy-specific adapter config
383
+ if adapter_name in self.strategy_config.get("adapters", {}):
384
+ config.update(self.strategy_config["adapters"][adapter_name])
385
+
386
+ return config
387
+
388
+
389
+ def load_config_from_env() -> StrategyJobConfig:
390
+ """
391
+ Load configuration from environment variables
392
+ This is the simplest way for users to provide configuration
393
+ """
394
+ user_config = UserConfig(
395
+ username=os.getenv("WAYFINDER_USERNAME"),
396
+ password=os.getenv("WAYFINDER_PASSWORD"),
397
+ refresh_token=os.getenv("WAYFINDER_REFRESH_TOKEN"),
398
+ main_wallet_address=os.getenv("MAIN_WALLET_ADDRESS"),
399
+ strategy_wallet_address=os.getenv("STRATEGY_WALLET_ADDRESS"),
400
+ default_slippage=float(os.getenv("DEFAULT_SLIPPAGE", "0.005")),
401
+ gas_multiplier=float(os.getenv("GAS_MULTIPLIER", "1.2")),
402
+ )
403
+
404
+ system_config = SystemConfig(
405
+ api_base_url=os.getenv("WAYFINDER_API_URL", "https://api.wayfinder.ai"),
406
+ job_id=os.getenv("JOB_ID"),
407
+ update_interval=int(os.getenv("UPDATE_INTERVAL", "60")),
408
+ max_retries=int(os.getenv("MAX_RETRIES", "3")),
409
+ retry_delay=int(os.getenv("RETRY_DELAY", "5")),
410
+ wallets_path=os.getenv("WALLETS_PATH", "wallets.json"),
411
+ wallet_id=os.getenv("WALLET_ID"),
412
+ )
413
+
414
+ # No auto-population - wallets must be explicitly set in environment or matched by label
415
+
416
+ return StrategyJobConfig(user=user_config, system=system_config)
417
+
418
+
419
+ # --- Internal helpers -------------------------------------------------------
420
+
421
+
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
+ """
443
+ if not wallets_path:
444
+ return []
445
+ path = Path(wallets_path)
446
+ if not path.exists():
447
+ return []
448
+ try:
449
+ data = json.loads(path.read_text())
450
+ if isinstance(data, list):
451
+ return data
452
+ return []
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}")
458
+ return []
@@ -0,0 +1,26 @@
1
+ """Constants package for wayfinder-paths.
2
+
3
+ This package contains all constants used across the system, organized by category:
4
+ - base: Fundamental constants (addresses, chain mappings, gas defaults)
5
+ - erc20_abi: ERC20 token ABI definitions for smart contract interactions
6
+ """
7
+
8
+ from .base import (
9
+ CHAIN_CODE_TO_ID,
10
+ DEFAULT_GAS_ESTIMATE_FALLBACK,
11
+ DEFAULT_NATIVE_GAS_UNITS,
12
+ DEFAULT_SLIPPAGE,
13
+ GAS_BUFFER_MULTIPLIER,
14
+ ONE_GWEI,
15
+ ZERO_ADDRESS,
16
+ )
17
+
18
+ __all__ = [
19
+ "ZERO_ADDRESS",
20
+ "CHAIN_CODE_TO_ID",
21
+ "DEFAULT_NATIVE_GAS_UNITS",
22
+ "DEFAULT_GAS_ESTIMATE_FALLBACK",
23
+ "GAS_BUFFER_MULTIPLIER",
24
+ "ONE_GWEI",
25
+ "DEFAULT_SLIPPAGE",
26
+ ]
@@ -0,0 +1,42 @@
1
+ """Base constants for adapters and strategies.
2
+
3
+ This module contains fundamental constants used across the wayfinder-paths system,
4
+ including address constants, chain mappings, and gas-related defaults.
5
+ """
6
+
7
+ # Address constants
8
+ ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
9
+
10
+ # Chain code to EVM chain id mapping
11
+ CHAIN_CODE_TO_ID = {
12
+ "base": 8453,
13
+ "arbitrum": 42161,
14
+ "arbitrum-one": 42161,
15
+ "ethereum": 1,
16
+ "mainnet": 1,
17
+ "hyperevm": 999,
18
+ }
19
+
20
+ # Gas/defaults
21
+ DEFAULT_NATIVE_GAS_UNITS = 21000
22
+ DEFAULT_GAS_ESTIMATE_FALLBACK = 100000
23
+ GAS_BUFFER_MULTIPLIER = 1.1 # 10% buffer for native sends
24
+ ONE_GWEI = 1_000_000_000
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