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,269 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from loguru import logger
|
|
8
|
+
|
|
9
|
+
from wayfinder_paths.core.constants.base import DEFAULT_HTTP_TIMEOUT
|
|
10
|
+
from wayfinder_paths.core.settings import settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WayfinderClient:
|
|
14
|
+
def __init__(self, api_key: str | None = None):
|
|
15
|
+
"""
|
|
16
|
+
Initialize WayfinderClient.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
api_key: Optional API key for service account authentication.
|
|
20
|
+
If provided, uses API key auth. Otherwise falls back to config.json.
|
|
21
|
+
"""
|
|
22
|
+
self.api_base_url = f"{settings.WAYFINDER_API_URL}/"
|
|
23
|
+
timeout = httpx.Timeout(DEFAULT_HTTP_TIMEOUT)
|
|
24
|
+
self.client = httpx.AsyncClient(timeout=timeout)
|
|
25
|
+
|
|
26
|
+
self.headers = {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
self._api_key: str | None = api_key
|
|
31
|
+
self._access_token: str | None = None
|
|
32
|
+
self._refresh_token: str | None = None
|
|
33
|
+
|
|
34
|
+
def set_bearer_token(self, token: str) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Set runtime OAuth/JWT Bearer token for Django-backed Wayfinder services.
|
|
37
|
+
"""
|
|
38
|
+
self.headers["Authorization"] = f"Bearer {token}"
|
|
39
|
+
self._access_token = token
|
|
40
|
+
|
|
41
|
+
def set_tokens(self, access: str | None, refresh: str | None) -> None:
|
|
42
|
+
"""Set both access and refresh tokens and configure Authorization header."""
|
|
43
|
+
if access:
|
|
44
|
+
self.set_bearer_token(access)
|
|
45
|
+
if refresh:
|
|
46
|
+
self._refresh_token = refresh
|
|
47
|
+
|
|
48
|
+
def clear_auth(self) -> None:
|
|
49
|
+
"""Clear Authorization headers (useful for logout or auth mode switch)."""
|
|
50
|
+
self.headers.pop("Authorization", None)
|
|
51
|
+
|
|
52
|
+
async def _refresh_access_token(self) -> bool:
|
|
53
|
+
"""Attempt to refresh access token using stored refresh token."""
|
|
54
|
+
if not self._refresh_token:
|
|
55
|
+
logger.debug("No refresh token available")
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
logger.info("Attempting to refresh access token")
|
|
59
|
+
start_time = time.time()
|
|
60
|
+
url = f"{settings.WAYFINDER_API_URL}/auth/token/refresh/"
|
|
61
|
+
payload = {"refresh": self._refresh_token}
|
|
62
|
+
response = await self.client.post(
|
|
63
|
+
url, json=payload, headers={"Content-Type": "application/json"}
|
|
64
|
+
)
|
|
65
|
+
if response.status_code != 200:
|
|
66
|
+
logger.warning(
|
|
67
|
+
f"Token refresh failed with status {response.status_code}"
|
|
68
|
+
)
|
|
69
|
+
return False
|
|
70
|
+
data = response.json()
|
|
71
|
+
new_access = data.get("access") or data.get("access_token")
|
|
72
|
+
if not new_access:
|
|
73
|
+
logger.warning("No access token in refresh response")
|
|
74
|
+
return False
|
|
75
|
+
self.set_bearer_token(new_access)
|
|
76
|
+
elapsed = time.time() - start_time
|
|
77
|
+
logger.info(f"Access token refreshed successfully in {elapsed:.2f}s")
|
|
78
|
+
return True
|
|
79
|
+
except Exception as e:
|
|
80
|
+
elapsed = time.time() - start_time
|
|
81
|
+
logger.error(f"Token refresh failed after {elapsed:.2f}s: {e}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def _load_config_credentials(self) -> dict[str, str | None]:
|
|
85
|
+
"""
|
|
86
|
+
Load credentials from config.json. Path can be overridden via WAYFINDER_CONFIG_PATH.
|
|
87
|
+
Expected shape:
|
|
88
|
+
{
|
|
89
|
+
"user": { "username": ..., "password": ..., "refresh_token": ..., "api_key": ... },
|
|
90
|
+
"system": { "api_key": ... }
|
|
91
|
+
}
|
|
92
|
+
"""
|
|
93
|
+
path = os.getenv("WAYFINDER_CONFIG_PATH", "config.json")
|
|
94
|
+
try:
|
|
95
|
+
with open(path) as f:
|
|
96
|
+
cfg = json.load(f)
|
|
97
|
+
user = cfg.get("user", {}) if isinstance(cfg, dict) else {}
|
|
98
|
+
system = cfg.get("system", {}) if isinstance(cfg, dict) else {}
|
|
99
|
+
return {
|
|
100
|
+
"username": user.get("username"),
|
|
101
|
+
"password": user.get("password"),
|
|
102
|
+
"refresh_token": user.get("refresh_token"),
|
|
103
|
+
"api_key": user.get("api_key") or system.get("api_key"),
|
|
104
|
+
}
|
|
105
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError) as e:
|
|
106
|
+
logger.debug(f"Could not load config file at {path}: {e}")
|
|
107
|
+
return {
|
|
108
|
+
"username": None,
|
|
109
|
+
"password": None,
|
|
110
|
+
"refresh_token": None,
|
|
111
|
+
"api_key": None,
|
|
112
|
+
}
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.warning(f"Unexpected error loading config file at {path}: {e}")
|
|
115
|
+
return {
|
|
116
|
+
"username": None,
|
|
117
|
+
"password": None,
|
|
118
|
+
"refresh_token": None,
|
|
119
|
+
"api_key": None,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async def _ensure_bearer_token(self) -> bool:
|
|
123
|
+
"""
|
|
124
|
+
Ensure Authorization header is set. Priority: existing header > constructor api_key > config.json api_key > env api_key > config.json tokens > env tokens > username/password.
|
|
125
|
+
Raises PermissionError if no credentials found.
|
|
126
|
+
"""
|
|
127
|
+
if self.headers.get("Authorization"):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
# Check for API key: constructor > config.json > environment
|
|
131
|
+
api_key = self._api_key
|
|
132
|
+
if not api_key:
|
|
133
|
+
creds = self._load_config_credentials()
|
|
134
|
+
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
135
|
+
|
|
136
|
+
if api_key:
|
|
137
|
+
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
138
|
+
if not api_key:
|
|
139
|
+
raise ValueError("API key cannot be empty")
|
|
140
|
+
self.headers["Authorization"] = f"Bearer {api_key}"
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
# Fall back to OAuth token-based auth
|
|
144
|
+
creds = self._load_config_credentials()
|
|
145
|
+
access = os.getenv("WAYFINDER_ACCESS_TOKEN")
|
|
146
|
+
refresh = creds.get("refresh_token") or os.getenv("WAYFINDER_REFRESH_TOKEN")
|
|
147
|
+
|
|
148
|
+
if access:
|
|
149
|
+
self.set_tokens(access, refresh)
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
if refresh:
|
|
153
|
+
self._refresh_token = refresh
|
|
154
|
+
refreshed = await self._refresh_access_token()
|
|
155
|
+
if refreshed:
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
username = creds.get("username") or os.getenv("WAYFINDER_USERNAME")
|
|
159
|
+
password = creds.get("password") or os.getenv("WAYFINDER_PASSWORD")
|
|
160
|
+
|
|
161
|
+
if username and password:
|
|
162
|
+
try:
|
|
163
|
+
url = f"{settings.WAYFINDER_API_URL}/auth/token/"
|
|
164
|
+
payload = {"username": username, "password": password}
|
|
165
|
+
response = await self.client.post(
|
|
166
|
+
url,
|
|
167
|
+
json=payload,
|
|
168
|
+
headers={"Content-Type": "application/json"},
|
|
169
|
+
)
|
|
170
|
+
if response.status_code == 200:
|
|
171
|
+
data = response.json()
|
|
172
|
+
access = data.get("access") or data.get("access_token")
|
|
173
|
+
refresh = data.get("refresh") or data.get("refresh_token")
|
|
174
|
+
self.set_tokens(access, refresh)
|
|
175
|
+
return bool(access)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.debug(f"Failed to authenticate with username/password: {e}")
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
raise PermissionError(
|
|
181
|
+
"Not authenticated: provide api_key (via constructor, config.json, or WAYFINDER_API_KEY env var) for service account auth, "
|
|
182
|
+
"or username+password/refresh_token in config.json for personal access"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def _request(
|
|
186
|
+
self,
|
|
187
|
+
method: str,
|
|
188
|
+
url: str,
|
|
189
|
+
*,
|
|
190
|
+
headers: dict[str, str] | None = None,
|
|
191
|
+
retry_on_401: bool = True,
|
|
192
|
+
**kwargs: Any,
|
|
193
|
+
) -> httpx.Response:
|
|
194
|
+
"""
|
|
195
|
+
Wrapper around httpx that injects headers and auto-refreshes tokens on 401 once.
|
|
196
|
+
Ensures API key or bearer token is set in headers when available (for service account auth and rate limits).
|
|
197
|
+
"""
|
|
198
|
+
logger.debug(f"Making {method} request to {url}")
|
|
199
|
+
start_time = time.time()
|
|
200
|
+
|
|
201
|
+
# Ensure API key or bearer token is set in headers if available and not already set
|
|
202
|
+
# This ensures API keys are passed to all endpoints (including public ones) for rate limiting
|
|
203
|
+
if not self.headers.get("Authorization"):
|
|
204
|
+
# Try to get API key from constructor, config, or env
|
|
205
|
+
api_key = self._api_key
|
|
206
|
+
if not api_key:
|
|
207
|
+
creds = self._load_config_credentials()
|
|
208
|
+
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
209
|
+
|
|
210
|
+
if api_key:
|
|
211
|
+
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
212
|
+
if api_key:
|
|
213
|
+
self.headers["Authorization"] = f"Bearer {api_key}"
|
|
214
|
+
|
|
215
|
+
merged_headers = dict(self.headers)
|
|
216
|
+
if headers:
|
|
217
|
+
merged_headers.update(headers)
|
|
218
|
+
resp = await self.client.request(method, url, headers=merged_headers, **kwargs)
|
|
219
|
+
|
|
220
|
+
if resp.status_code == 401 and retry_on_401 and self._refresh_token:
|
|
221
|
+
logger.info("Received 401, attempting token refresh and retry")
|
|
222
|
+
refreshed = await self._refresh_access_token()
|
|
223
|
+
if refreshed:
|
|
224
|
+
merged_headers = dict(self.headers)
|
|
225
|
+
if headers:
|
|
226
|
+
merged_headers.update(headers)
|
|
227
|
+
resp = await self.client.request(
|
|
228
|
+
method, url, headers=merged_headers, **kwargs
|
|
229
|
+
)
|
|
230
|
+
logger.info("Retry after token refresh successful")
|
|
231
|
+
else:
|
|
232
|
+
logger.error("Token refresh failed, request will fail")
|
|
233
|
+
|
|
234
|
+
elapsed = time.time() - start_time
|
|
235
|
+
if resp.status_code >= 400:
|
|
236
|
+
logger.warning(
|
|
237
|
+
f"HTTP {resp.status_code} response for {method} {url} after {elapsed:.2f}s"
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
logger.debug(
|
|
241
|
+
f"HTTP {resp.status_code} response for {method} {url} after {elapsed:.2f}s"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
resp.raise_for_status()
|
|
245
|
+
return resp
|
|
246
|
+
|
|
247
|
+
async def _authed_request(
|
|
248
|
+
self,
|
|
249
|
+
method: str,
|
|
250
|
+
url: str,
|
|
251
|
+
*,
|
|
252
|
+
headers: dict[str, str] | None = None,
|
|
253
|
+
**kwargs: Any,
|
|
254
|
+
) -> httpx.Response:
|
|
255
|
+
"""
|
|
256
|
+
Ensure Authorization (via env/config creds) and perform the request.
|
|
257
|
+
Retries once on 401 by re-acquiring tokens.
|
|
258
|
+
"""
|
|
259
|
+
ok = await self._ensure_bearer_token()
|
|
260
|
+
if not ok:
|
|
261
|
+
raise PermissionError("Not authenticated: set env tokens or credentials")
|
|
262
|
+
try:
|
|
263
|
+
return await self._request(method, url, headers=headers, **kwargs)
|
|
264
|
+
except httpx.HTTPStatusError as e:
|
|
265
|
+
if e.response is not None and e.response.status_code == 401:
|
|
266
|
+
# Retry after attempting re-acquire/refresh
|
|
267
|
+
await self._ensure_bearer_token()
|
|
268
|
+
return await self._request(method, url, headers=headers, **kwargs)
|
|
269
|
+
raise
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core client modules for API communication
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from wayfinder_paths.core.clients.AuthClient import AuthClient
|
|
6
|
+
from wayfinder_paths.core.clients.BRAPClient import BRAPClient
|
|
7
|
+
from wayfinder_paths.core.clients.ClientManager import ClientManager
|
|
8
|
+
from wayfinder_paths.core.clients.HyperlendClient import HyperlendClient
|
|
9
|
+
from wayfinder_paths.core.clients.LedgerClient import LedgerClient
|
|
10
|
+
from wayfinder_paths.core.clients.PoolClient import PoolClient
|
|
11
|
+
from wayfinder_paths.core.clients.protocols import (
|
|
12
|
+
BRAPClientProtocol,
|
|
13
|
+
HyperlendClientProtocol,
|
|
14
|
+
LedgerClientProtocol,
|
|
15
|
+
PoolClientProtocol,
|
|
16
|
+
SimulationClientProtocol,
|
|
17
|
+
TokenClientProtocol,
|
|
18
|
+
TransactionClientProtocol,
|
|
19
|
+
WalletClientProtocol,
|
|
20
|
+
)
|
|
21
|
+
from wayfinder_paths.core.clients.SimulationClient import SimulationClient
|
|
22
|
+
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
23
|
+
from wayfinder_paths.core.clients.TransactionClient import TransactionClient
|
|
24
|
+
from wayfinder_paths.core.clients.WalletClient import WalletClient
|
|
25
|
+
from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"WayfinderClient",
|
|
29
|
+
"ClientManager",
|
|
30
|
+
"AuthClient",
|
|
31
|
+
"TokenClient",
|
|
32
|
+
"WalletClient",
|
|
33
|
+
"TransactionClient",
|
|
34
|
+
"LedgerClient",
|
|
35
|
+
"PoolClient",
|
|
36
|
+
"BRAPClient",
|
|
37
|
+
"SimulationClient",
|
|
38
|
+
"HyperlendClient",
|
|
39
|
+
# Protocols for SDK usage
|
|
40
|
+
"TokenClientProtocol",
|
|
41
|
+
"HyperlendClientProtocol",
|
|
42
|
+
"LedgerClientProtocol",
|
|
43
|
+
"WalletClientProtocol",
|
|
44
|
+
"TransactionClientProtocol",
|
|
45
|
+
"PoolClientProtocol",
|
|
46
|
+
"BRAPClientProtocol",
|
|
47
|
+
"SimulationClientProtocol",
|
|
48
|
+
]
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Protocol definitions for API clients.
|
|
3
|
+
|
|
4
|
+
These protocols define the interface that all client implementations must satisfy.
|
|
5
|
+
When used as an SDK, users can provide custom implementations that match these protocols.
|
|
6
|
+
|
|
7
|
+
Note: AuthClient is excluded as SDK users handle their own authentication.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Protocol
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from wayfinder_paths.core.clients.BRAPClient import BRAPQuote
|
|
16
|
+
from wayfinder_paths.core.clients.HyperlendClient import (
|
|
17
|
+
AssetsView,
|
|
18
|
+
LendRateHistory,
|
|
19
|
+
MarketEntry,
|
|
20
|
+
StableMarket,
|
|
21
|
+
)
|
|
22
|
+
from wayfinder_paths.core.clients.LedgerClient import (
|
|
23
|
+
StrategyTransactionList,
|
|
24
|
+
TransactionRecord,
|
|
25
|
+
)
|
|
26
|
+
from wayfinder_paths.core.clients.PoolClient import (
|
|
27
|
+
LlamaMatch,
|
|
28
|
+
LlamaReport,
|
|
29
|
+
PoolList,
|
|
30
|
+
)
|
|
31
|
+
from wayfinder_paths.core.clients.SimulationClient import SimulationResult
|
|
32
|
+
from wayfinder_paths.core.clients.TokenClient import (
|
|
33
|
+
GasToken,
|
|
34
|
+
TokenDetails,
|
|
35
|
+
)
|
|
36
|
+
from wayfinder_paths.core.clients.TransactionClient import TransactionPayload
|
|
37
|
+
from wayfinder_paths.core.clients.WalletClient import (
|
|
38
|
+
PoolBalance,
|
|
39
|
+
TokenBalance,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TokenClientProtocol(Protocol):
|
|
44
|
+
"""Protocol for token-related operations"""
|
|
45
|
+
|
|
46
|
+
async def get_token_details(
|
|
47
|
+
self, token_id: str, force_refresh: bool = False
|
|
48
|
+
) -> TokenDetails:
|
|
49
|
+
"""Get token data including price from the token-details endpoint"""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
async def get_gas_token(self, chain_code: str) -> GasToken:
|
|
53
|
+
"""Fetch the native gas token for a given chain code"""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HyperlendClientProtocol(Protocol):
|
|
58
|
+
"""Protocol for Hyperlend-related operations"""
|
|
59
|
+
|
|
60
|
+
async def get_stable_markets(
|
|
61
|
+
self,
|
|
62
|
+
*,
|
|
63
|
+
chain_id: int,
|
|
64
|
+
required_underlying_tokens: float | None = None,
|
|
65
|
+
buffer_bps: int | None = None,
|
|
66
|
+
min_buffer_tokens: float | None = None,
|
|
67
|
+
is_stable_symbol: bool | None = None,
|
|
68
|
+
) -> list[StableMarket]:
|
|
69
|
+
"""Fetch stable markets from Hyperlend"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
async def get_assets_view(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
chain_id: int,
|
|
76
|
+
user_address: str,
|
|
77
|
+
) -> AssetsView:
|
|
78
|
+
"""Fetch assets view for a user address from Hyperlend"""
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
async def get_market_entry(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
chain_id: int,
|
|
85
|
+
token_address: str,
|
|
86
|
+
) -> MarketEntry:
|
|
87
|
+
"""Fetch market entry from Hyperlend"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
async def get_lend_rate_history(
|
|
91
|
+
self,
|
|
92
|
+
*,
|
|
93
|
+
chain_id: int,
|
|
94
|
+
token_address: str,
|
|
95
|
+
lookback_hours: int,
|
|
96
|
+
) -> LendRateHistory:
|
|
97
|
+
"""Fetch lend rate history from Hyperlend"""
|
|
98
|
+
...
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class LedgerClientProtocol(Protocol):
|
|
102
|
+
"""Protocol for strategy transaction history and bookkeeping operations"""
|
|
103
|
+
|
|
104
|
+
async def get_strategy_transactions(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
wallet_address: str,
|
|
108
|
+
limit: int = 50,
|
|
109
|
+
offset: int = 0,
|
|
110
|
+
) -> StrategyTransactionList:
|
|
111
|
+
"""Fetch a paginated list of transactions for a given strategy wallet"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
async def get_strategy_net_deposit(self, *, wallet_address: str) -> float:
|
|
115
|
+
"""Fetch the net deposit (deposits - withdrawals) for a strategy"""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
async def get_strategy_latest_transactions(
|
|
119
|
+
self, *, wallet_address: str
|
|
120
|
+
) -> StrategyTransactionList:
|
|
121
|
+
"""Fetch the latest transactions for a strategy"""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
async def add_strategy_deposit(
|
|
125
|
+
self,
|
|
126
|
+
*,
|
|
127
|
+
wallet_address: str,
|
|
128
|
+
chain_id: int,
|
|
129
|
+
token_address: str,
|
|
130
|
+
token_amount: str | float,
|
|
131
|
+
usd_value: str | float,
|
|
132
|
+
data: dict[str, Any] | None = None,
|
|
133
|
+
strategy_name: str | None = None,
|
|
134
|
+
) -> TransactionRecord:
|
|
135
|
+
"""Record a deposit for a strategy"""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
async def add_strategy_withdraw(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
wallet_address: str,
|
|
142
|
+
chain_id: int,
|
|
143
|
+
token_address: str,
|
|
144
|
+
token_amount: str | float,
|
|
145
|
+
usd_value: str | float,
|
|
146
|
+
data: dict[str, Any] | None = None,
|
|
147
|
+
strategy_name: str | None = None,
|
|
148
|
+
) -> TransactionRecord:
|
|
149
|
+
"""Record a withdrawal for a strategy"""
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
async def add_strategy_operation(
|
|
153
|
+
self,
|
|
154
|
+
*,
|
|
155
|
+
wallet_address: str,
|
|
156
|
+
operation_data: dict[str, Any],
|
|
157
|
+
usd_value: str | float,
|
|
158
|
+
strategy_name: str | None = None,
|
|
159
|
+
) -> TransactionRecord:
|
|
160
|
+
"""Record a strategy operation (e.g., swaps, rebalances)"""
|
|
161
|
+
...
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class WalletClientProtocol(Protocol):
|
|
165
|
+
"""Protocol for wallet-related operations"""
|
|
166
|
+
|
|
167
|
+
async def get_token_balance_for_wallet(
|
|
168
|
+
self,
|
|
169
|
+
*,
|
|
170
|
+
token_id: str,
|
|
171
|
+
wallet_address: str,
|
|
172
|
+
human_readable: bool = True,
|
|
173
|
+
) -> TokenBalance:
|
|
174
|
+
"""Fetch a single token balance for an explicit wallet address"""
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
async def get_pool_balance_for_wallet(
|
|
178
|
+
self,
|
|
179
|
+
*,
|
|
180
|
+
pool_address: str,
|
|
181
|
+
chain_id: int,
|
|
182
|
+
user_address: str,
|
|
183
|
+
human_readable: bool = True,
|
|
184
|
+
) -> PoolBalance:
|
|
185
|
+
"""Fetch a wallet's LP/share balance for a given pool address and chain"""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TransactionClientProtocol(Protocol):
|
|
190
|
+
"""Protocol for transaction operations"""
|
|
191
|
+
|
|
192
|
+
async def build_send(
|
|
193
|
+
self,
|
|
194
|
+
from_address: str,
|
|
195
|
+
to_address: str,
|
|
196
|
+
token_address: str,
|
|
197
|
+
amount: float,
|
|
198
|
+
chain_id: int,
|
|
199
|
+
) -> TransactionPayload:
|
|
200
|
+
"""Build a send transaction payload for EVM tokens/native transfers"""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class PoolClientProtocol(Protocol):
|
|
205
|
+
"""Protocol for pool-related read operations"""
|
|
206
|
+
|
|
207
|
+
async def get_pools_by_ids(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
pool_ids: str,
|
|
211
|
+
merge_external: bool | None = None,
|
|
212
|
+
) -> PoolList:
|
|
213
|
+
"""Fetch pools by comma-separated pool ids"""
|
|
214
|
+
...
|
|
215
|
+
|
|
216
|
+
async def get_all_pools(self, *, merge_external: bool | None = None) -> PoolList:
|
|
217
|
+
"""Fetch all pools"""
|
|
218
|
+
...
|
|
219
|
+
|
|
220
|
+
async def get_llama_matches(self) -> dict[str, LlamaMatch]:
|
|
221
|
+
"""Fetch Llama matches for pools"""
|
|
222
|
+
...
|
|
223
|
+
|
|
224
|
+
async def get_llama_reports(self, *, identifiers: str) -> dict[str, LlamaReport]:
|
|
225
|
+
"""Fetch Llama reports using identifiers"""
|
|
226
|
+
...
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class BRAPClientProtocol(Protocol):
|
|
230
|
+
"""Protocol for BRAP (Bridge/Router/Adapter Protocol) quote operations"""
|
|
231
|
+
|
|
232
|
+
async def get_quote(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
from_token_address: str,
|
|
236
|
+
to_token_address: str,
|
|
237
|
+
from_chain_id: int,
|
|
238
|
+
to_chain_id: int,
|
|
239
|
+
from_address: str,
|
|
240
|
+
to_address: str,
|
|
241
|
+
amount1: str,
|
|
242
|
+
slippage: float | None = None,
|
|
243
|
+
wayfinder_fee: float | None = None,
|
|
244
|
+
) -> BRAPQuote:
|
|
245
|
+
"""Get a quote for a bridge/swap operation"""
|
|
246
|
+
...
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class SimulationClientProtocol(Protocol):
|
|
250
|
+
"""Protocol for blockchain transaction simulations"""
|
|
251
|
+
|
|
252
|
+
async def simulate_send(
|
|
253
|
+
self,
|
|
254
|
+
from_address: str,
|
|
255
|
+
to_address: str,
|
|
256
|
+
token_address: str,
|
|
257
|
+
amount: str,
|
|
258
|
+
chain_id: int,
|
|
259
|
+
initial_balances: dict[str, str],
|
|
260
|
+
) -> SimulationResult:
|
|
261
|
+
"""Simulate sending native ETH or ERC20 tokens"""
|
|
262
|
+
...
|
|
263
|
+
|
|
264
|
+
async def simulate_approve(
|
|
265
|
+
self,
|
|
266
|
+
from_address: str,
|
|
267
|
+
to_address: str,
|
|
268
|
+
token_address: str,
|
|
269
|
+
amount: str,
|
|
270
|
+
chain_id: int,
|
|
271
|
+
initial_balances: dict[str, str],
|
|
272
|
+
clear_approval_first: bool = False,
|
|
273
|
+
) -> SimulationResult:
|
|
274
|
+
"""Simulate ERC20 token approval"""
|
|
275
|
+
...
|
|
276
|
+
|
|
277
|
+
async def simulate_swap(
|
|
278
|
+
self,
|
|
279
|
+
from_token_address: str,
|
|
280
|
+
to_token_address: str,
|
|
281
|
+
from_chain_id: int,
|
|
282
|
+
to_chain_id: int,
|
|
283
|
+
amount: str,
|
|
284
|
+
from_address: str,
|
|
285
|
+
slippage: float,
|
|
286
|
+
initial_balances: dict[str, str],
|
|
287
|
+
) -> SimulationResult:
|
|
288
|
+
"""Simulate token swap operation"""
|
|
289
|
+
...
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class HyperliquidExecutorProtocol(Protocol):
|
|
293
|
+
"""Protocol for Hyperliquid order execution operations."""
|
|
294
|
+
|
|
295
|
+
async def place_market_order(
|
|
296
|
+
self,
|
|
297
|
+
*,
|
|
298
|
+
asset_id: int,
|
|
299
|
+
is_buy: bool,
|
|
300
|
+
slippage: float,
|
|
301
|
+
size: float,
|
|
302
|
+
address: str,
|
|
303
|
+
reduce_only: bool = False,
|
|
304
|
+
cloid: Any = None,
|
|
305
|
+
builder: dict[str, Any] | None = None,
|
|
306
|
+
) -> dict[str, Any]:
|
|
307
|
+
"""Place a market order."""
|
|
308
|
+
...
|
|
309
|
+
|
|
310
|
+
async def cancel_order(
|
|
311
|
+
self,
|
|
312
|
+
*,
|
|
313
|
+
asset_id: int,
|
|
314
|
+
order_id: int,
|
|
315
|
+
address: str,
|
|
316
|
+
) -> dict[str, Any]:
|
|
317
|
+
"""Cancel an open order."""
|
|
318
|
+
...
|
|
319
|
+
|
|
320
|
+
async def update_leverage(
|
|
321
|
+
self,
|
|
322
|
+
*,
|
|
323
|
+
asset_id: int,
|
|
324
|
+
leverage: int,
|
|
325
|
+
is_cross: bool,
|
|
326
|
+
address: str,
|
|
327
|
+
) -> dict[str, Any]:
|
|
328
|
+
"""Update leverage for an asset."""
|
|
329
|
+
...
|
|
330
|
+
|
|
331
|
+
async def transfer_spot_to_perp(
|
|
332
|
+
self,
|
|
333
|
+
*,
|
|
334
|
+
amount: float,
|
|
335
|
+
address: str,
|
|
336
|
+
) -> dict[str, Any]:
|
|
337
|
+
"""Transfer USDC from spot to perp balance."""
|
|
338
|
+
...
|
|
339
|
+
|
|
340
|
+
async def transfer_perp_to_spot(
|
|
341
|
+
self,
|
|
342
|
+
*,
|
|
343
|
+
amount: float,
|
|
344
|
+
address: str,
|
|
345
|
+
) -> dict[str, Any]:
|
|
346
|
+
"""Transfer USDC from perp to spot balance."""
|
|
347
|
+
...
|
|
348
|
+
|
|
349
|
+
async def place_stop_loss(
|
|
350
|
+
self,
|
|
351
|
+
*,
|
|
352
|
+
asset_id: int,
|
|
353
|
+
is_buy: bool,
|
|
354
|
+
trigger_price: float,
|
|
355
|
+
size: float,
|
|
356
|
+
address: str,
|
|
357
|
+
) -> dict[str, Any]:
|
|
358
|
+
"""Place a stop-loss order."""
|
|
359
|
+
...
|
|
360
|
+
|
|
361
|
+
async def place_limit_order(
|
|
362
|
+
self,
|
|
363
|
+
*,
|
|
364
|
+
asset_id: int,
|
|
365
|
+
is_buy: bool,
|
|
366
|
+
price: float,
|
|
367
|
+
size: float,
|
|
368
|
+
address: str,
|
|
369
|
+
reduce_only: bool = False,
|
|
370
|
+
builder: dict[str, Any] | None = None,
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
"""Place a limit order."""
|
|
373
|
+
...
|
|
374
|
+
|
|
375
|
+
async def withdraw(
|
|
376
|
+
self,
|
|
377
|
+
*,
|
|
378
|
+
amount: float,
|
|
379
|
+
address: str,
|
|
380
|
+
) -> dict[str, Any]:
|
|
381
|
+
"""Withdraw USDC from Hyperliquid to Arbitrum."""
|
|
382
|
+
...
|
|
383
|
+
|
|
384
|
+
async def approve_builder_fee(
|
|
385
|
+
self,
|
|
386
|
+
*,
|
|
387
|
+
builder: str,
|
|
388
|
+
max_fee_rate: str,
|
|
389
|
+
address: str,
|
|
390
|
+
) -> dict[str, Any]:
|
|
391
|
+
"""Approve a builder fee for the user."""
|
|
392
|
+
...
|