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.
- wayfinder_paths/CONFIG_GUIDE.md +399 -0
- wayfinder_paths/__init__.py +22 -0
- wayfinder_paths/abis/generic/erc20.json +383 -0
- wayfinder_paths/adapters/__init__.py +0 -0
- wayfinder_paths/adapters/balance_adapter/README.md +94 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +238 -0
- wayfinder_paths/adapters/balance_adapter/examples.json +6 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +59 -0
- wayfinder_paths/adapters/brap_adapter/README.md +249 -0
- wayfinder_paths/adapters/brap_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +726 -0
- wayfinder_paths/adapters/brap_adapter/examples.json +175 -0
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +286 -0
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +305 -0
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +274 -0
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/README.md +145 -0
- wayfinder_paths/adapters/ledger_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/ledger_adapter/adapter.py +289 -0
- wayfinder_paths/adapters/ledger_adapter/examples.json +137 -0
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +11 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +205 -0
- wayfinder_paths/adapters/pool_adapter/README.md +206 -0
- wayfinder_paths/adapters/pool_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pool_adapter/adapter.py +282 -0
- wayfinder_paths/adapters/pool_adapter/examples.json +143 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +220 -0
- wayfinder_paths/adapters/token_adapter/README.md +101 -0
- wayfinder_paths/adapters/token_adapter/__init__.py +3 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +96 -0
- wayfinder_paths/adapters/token_adapter/examples.json +26 -0
- wayfinder_paths/adapters/token_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/test_adapter.py +125 -0
- wayfinder_paths/config.example.json +22 -0
- wayfinder_paths/conftest.py +31 -0
- wayfinder_paths/core/__init__.py +18 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +65 -0
- wayfinder_paths/core/adapters/__init__.py +5 -0
- wayfinder_paths/core/adapters/base.py +5 -0
- wayfinder_paths/core/adapters/models.py +46 -0
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/AuthClient.py +83 -0
- wayfinder_paths/core/clients/BRAPClient.py +109 -0
- wayfinder_paths/core/clients/ClientManager.py +210 -0
- wayfinder_paths/core/clients/HyperlendClient.py +192 -0
- wayfinder_paths/core/clients/LedgerClient.py +443 -0
- wayfinder_paths/core/clients/PoolClient.py +128 -0
- wayfinder_paths/core/clients/SimulationClient.py +192 -0
- wayfinder_paths/core/clients/TokenClient.py +89 -0
- wayfinder_paths/core/clients/TransactionClient.py +63 -0
- wayfinder_paths/core/clients/WalletClient.py +94 -0
- wayfinder_paths/core/clients/WayfinderClient.py +269 -0
- wayfinder_paths/core/clients/__init__.py +48 -0
- wayfinder_paths/core/clients/protocols.py +392 -0
- wayfinder_paths/core/clients/sdk_example.py +110 -0
- wayfinder_paths/core/config.py +458 -0
- wayfinder_paths/core/constants/__init__.py +26 -0
- wayfinder_paths/core/constants/base.py +42 -0
- wayfinder_paths/core/constants/erc20_abi.py +118 -0
- wayfinder_paths/core/constants/hyperlend_abi.py +152 -0
- wayfinder_paths/core/engine/StrategyJob.py +188 -0
- wayfinder_paths/core/engine/__init__.py +5 -0
- wayfinder_paths/core/engine/manifest.py +97 -0
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +179 -0
- wayfinder_paths/core/services/local_evm_txn.py +430 -0
- wayfinder_paths/core/services/local_token_txn.py +231 -0
- wayfinder_paths/core/services/web3_service.py +45 -0
- wayfinder_paths/core/settings.py +61 -0
- wayfinder_paths/core/strategies/Strategy.py +280 -0
- wayfinder_paths/core/strategies/__init__.py +5 -0
- wayfinder_paths/core/strategies/base.py +7 -0
- wayfinder_paths/core/strategies/descriptors.py +81 -0
- wayfinder_paths/core/utils/__init__.py +1 -0
- wayfinder_paths/core/utils/evm_helpers.py +206 -0
- wayfinder_paths/core/utils/wallets.py +77 -0
- wayfinder_paths/core/wallets/README.md +91 -0
- wayfinder_paths/core/wallets/WalletManager.py +56 -0
- wayfinder_paths/core/wallets/__init__.py +7 -0
- wayfinder_paths/policies/enso.py +17 -0
- wayfinder_paths/policies/erc20.py +34 -0
- wayfinder_paths/policies/evm.py +21 -0
- wayfinder_paths/policies/hyper_evm.py +19 -0
- wayfinder_paths/policies/hyperlend.py +12 -0
- wayfinder_paths/policies/hyperliquid.py +30 -0
- wayfinder_paths/policies/moonwell.py +54 -0
- wayfinder_paths/policies/prjx.py +30 -0
- wayfinder_paths/policies/util.py +27 -0
- wayfinder_paths/run_strategy.py +411 -0
- wayfinder_paths/scripts/__init__.py +0 -0
- wayfinder_paths/scripts/create_strategy.py +181 -0
- wayfinder_paths/scripts/make_wallets.py +169 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/scripts/validate_manifests.py +213 -0
- wayfinder_paths/strategies/__init__.py +0 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/config.py +85 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +100 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +8 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +7 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2270 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +352 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +96 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/examples.json +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +17 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1810 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +520 -0
- wayfinder_paths/templates/adapter/README.md +105 -0
- wayfinder_paths/templates/adapter/adapter.py +26 -0
- wayfinder_paths/templates/adapter/examples.json +8 -0
- wayfinder_paths/templates/adapter/manifest.yaml +6 -0
- wayfinder_paths/templates/adapter/test_adapter.py +49 -0
- wayfinder_paths/templates/strategy/README.md +153 -0
- wayfinder_paths/templates/strategy/examples.json +11 -0
- wayfinder_paths/templates/strategy/manifest.yaml +8 -0
- wayfinder_paths/templates/strategy/strategy.py +57 -0
- wayfinder_paths/templates/strategy/test_strategy.py +197 -0
- wayfinder_paths/tests/__init__.py +0 -0
- wayfinder_paths/tests/test_smoke_manifest.py +48 -0
- wayfinder_paths/tests/test_test_coverage.py +212 -0
- wayfinder_paths/tests/test_utils.py +64 -0
- wayfinder_paths-0.1.7.dist-info/LICENSE +21 -0
- wayfinder_paths-0.1.7.dist-info/METADATA +777 -0
- wayfinder_paths-0.1.7.dist-info/RECORD +149 -0
- 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
|