wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.15__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/adapters/balance_adapter/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +73 -32
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +144 -78
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
- wayfinder_paths/adapters/pool_adapter/README.md +9 -10
- wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +9 -7
- wayfinder_paths/core/clients/BRAPClient.py +102 -61
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +125 -64
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +122 -48
- wayfinder_paths/core/clients/TokenClient.py +91 -36
- wayfinder_paths/core/clients/WalletClient.py +26 -56
- wayfinder_paths/core/clients/WayfinderClient.py +28 -160
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -46
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/constants/erc20_abi.py +0 -11
- wayfinder_paths/core/engine/StrategyJob.py +10 -56
- wayfinder_paths/core/services/base.py +1 -0
- wayfinder_paths/core/services/local_evm_txn.py +25 -9
- wayfinder_paths/core/services/local_token_txn.py +2 -6
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +16 -4
- wayfinder_paths/core/utils/evm_helpers.py +2 -9
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +13 -19
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
- wayfinder_paths/abis/generic/erc20.json +0 -383
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -7,83 +7,138 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from typing import NotRequired, Required, TypedDict
|
|
9
9
|
|
|
10
|
-
from wayfinder_paths.core.clients.AuthClient import AuthClient
|
|
11
10
|
from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
12
11
|
from wayfinder_paths.core.config import get_api_base_url
|
|
13
12
|
|
|
14
13
|
|
|
14
|
+
class TokenLinks(TypedDict):
|
|
15
|
+
"""Token links structure"""
|
|
16
|
+
|
|
17
|
+
github: NotRequired[list[str]]
|
|
18
|
+
reddit: NotRequired[str]
|
|
19
|
+
discord: NotRequired[str]
|
|
20
|
+
twitter: NotRequired[str]
|
|
21
|
+
homepage: NotRequired[list[str]]
|
|
22
|
+
telegram: NotRequired[str]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ChainAddress(TypedDict):
|
|
26
|
+
"""Chain address structure"""
|
|
27
|
+
|
|
28
|
+
address: Required[str]
|
|
29
|
+
token_id: Required[str]
|
|
30
|
+
is_contract: NotRequired[bool]
|
|
31
|
+
chain_id: NotRequired[int]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ChainInfo(TypedDict):
|
|
35
|
+
"""Chain information structure"""
|
|
36
|
+
|
|
37
|
+
id: Required[int]
|
|
38
|
+
name: Required[str]
|
|
39
|
+
code: Required[str]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TokenMetadata(TypedDict):
|
|
43
|
+
"""Token metadata structure"""
|
|
44
|
+
|
|
45
|
+
query_processed: NotRequired[str]
|
|
46
|
+
query_type: NotRequired[str]
|
|
47
|
+
has_addresses: NotRequired[bool]
|
|
48
|
+
address_count: NotRequired[int]
|
|
49
|
+
has_price_data: NotRequired[bool]
|
|
50
|
+
|
|
51
|
+
|
|
15
52
|
class TokenDetails(TypedDict):
|
|
16
53
|
"""Token details response structure"""
|
|
17
54
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
symbol: Required[str]
|
|
55
|
+
asset_id: NotRequired[str]
|
|
56
|
+
token_ids: NotRequired[list[str]]
|
|
21
57
|
name: Required[str]
|
|
58
|
+
symbol: Required[str]
|
|
22
59
|
decimals: Required[int]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
60
|
+
description: NotRequired[str]
|
|
61
|
+
links: NotRequired[TokenLinks]
|
|
62
|
+
categories: NotRequired[list[str]]
|
|
63
|
+
current_price: NotRequired[float]
|
|
64
|
+
market_cap: NotRequired[float]
|
|
65
|
+
total_volume_usd_24h: NotRequired[float]
|
|
66
|
+
price_change_24h: NotRequired[float]
|
|
67
|
+
price_change_7d: NotRequired[float]
|
|
68
|
+
price_change_30d: NotRequired[float]
|
|
69
|
+
price_change_1y: NotRequired[float]
|
|
70
|
+
addresses: NotRequired[dict[str, str]]
|
|
71
|
+
chain_addresses: NotRequired[dict[str, ChainAddress]]
|
|
72
|
+
chain_ids: NotRequired[dict[str, int]]
|
|
73
|
+
id: NotRequired[int]
|
|
74
|
+
token_id: Required[str]
|
|
75
|
+
address: Required[str]
|
|
76
|
+
chain: NotRequired[ChainInfo]
|
|
77
|
+
query: NotRequired[str]
|
|
78
|
+
query_type: NotRequired[str]
|
|
79
|
+
metadata: NotRequired[TokenMetadata]
|
|
27
80
|
image_url: NotRequired[str | None]
|
|
28
|
-
coingecko_id: NotRequired[str | None]
|
|
29
81
|
|
|
30
82
|
|
|
31
83
|
class GasToken(TypedDict):
|
|
32
84
|
"""Gas token response structure"""
|
|
33
85
|
|
|
34
86
|
id: Required[str]
|
|
35
|
-
|
|
36
|
-
|
|
87
|
+
coingecko_id: NotRequired[str]
|
|
88
|
+
token_id: Required[str]
|
|
37
89
|
name: Required[str]
|
|
90
|
+
symbol: Required[str]
|
|
91
|
+
address: Required[str]
|
|
38
92
|
decimals: Required[int]
|
|
39
|
-
|
|
40
|
-
chain_code: Required[str]
|
|
41
|
-
price_usd: NotRequired[float]
|
|
93
|
+
chain: NotRequired[ChainInfo]
|
|
42
94
|
|
|
43
95
|
|
|
44
96
|
class TokenClient(WayfinderClient):
|
|
45
97
|
"""Adapter for token-related operations"""
|
|
46
98
|
|
|
47
|
-
def __init__(self
|
|
48
|
-
super().__init__(
|
|
49
|
-
self.api_base_url = f"{
|
|
50
|
-
self._auth_client: AuthClient | None = AuthClient(api_key=api_key)
|
|
51
|
-
|
|
52
|
-
# ============== Public (No-Auth) Endpoints ==============
|
|
99
|
+
def __init__(self):
|
|
100
|
+
super().__init__()
|
|
101
|
+
self.api_base_url = f"{get_api_base_url()}/v1/blockchain/tokens"
|
|
53
102
|
|
|
54
103
|
async def get_token_details(
|
|
55
|
-
self,
|
|
104
|
+
self, query: str, market_data: bool = False, chain_id: int | None = None
|
|
56
105
|
) -> TokenDetails:
|
|
57
106
|
"""
|
|
58
|
-
Get token data including price from the token-details endpoint
|
|
107
|
+
Get token data including price from the token-details endpoint.
|
|
59
108
|
|
|
60
109
|
Args:
|
|
61
|
-
|
|
110
|
+
query: Token identifier, address, or symbol to query
|
|
111
|
+
market_data: Whether to include market data (default: True)
|
|
112
|
+
chain_id: Optional chain ID
|
|
62
113
|
|
|
63
114
|
Returns:
|
|
64
115
|
Full token data including price information
|
|
65
116
|
"""
|
|
66
|
-
url = f"{
|
|
117
|
+
url = f"{self.api_base_url}/detail/"
|
|
67
118
|
params = {
|
|
68
|
-
"query":
|
|
69
|
-
"
|
|
70
|
-
"force_refresh": str(force_refresh),
|
|
119
|
+
"query": query,
|
|
120
|
+
"market_data": market_data,
|
|
71
121
|
}
|
|
72
|
-
|
|
73
|
-
|
|
122
|
+
if chain_id is not None:
|
|
123
|
+
params["chain_id"] = chain_id
|
|
124
|
+
response = await self._authed_request("GET", url, params=params)
|
|
74
125
|
response.raise_for_status()
|
|
75
126
|
data = response.json()
|
|
76
127
|
return data.get("data", data)
|
|
77
128
|
|
|
78
|
-
async def get_gas_token(self,
|
|
129
|
+
async def get_gas_token(self, query: str) -> GasToken:
|
|
79
130
|
"""
|
|
80
|
-
Fetch the native gas token for a given chain code
|
|
81
|
-
|
|
131
|
+
Fetch the native gas token for a given chain code or query.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
query: Chain code or query string
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Gas token information including chain details
|
|
82
138
|
"""
|
|
83
|
-
url = f"{
|
|
84
|
-
params = {"
|
|
85
|
-
|
|
86
|
-
response = await self._request("GET", url, params=params, headers={})
|
|
139
|
+
url = f"{self.api_base_url}/gas/"
|
|
140
|
+
params = {"query": query}
|
|
141
|
+
response = await self._authed_request("GET", url, params=params)
|
|
87
142
|
response.raise_for_status()
|
|
88
143
|
data = response.json()
|
|
89
144
|
return data.get("data", data)
|
|
@@ -7,80 +7,50 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from typing import NotRequired, Required, TypedDict
|
|
9
9
|
|
|
10
|
-
from wayfinder_paths.core.clients.AuthClient import AuthClient
|
|
11
10
|
from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
12
11
|
from wayfinder_paths.core.config import get_api_base_url
|
|
13
12
|
|
|
14
13
|
|
|
15
|
-
class
|
|
16
|
-
"""
|
|
14
|
+
class AddressBalance(TypedDict):
|
|
15
|
+
"""Balance response structure for address/query lookups."""
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
wallet_address: Required[str]
|
|
20
|
-
balance: Required[str]
|
|
21
|
-
balance_human: NotRequired[float | None]
|
|
22
|
-
usd_value: NotRequired[float | None]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class PoolBalance(TypedDict):
|
|
26
|
-
"""Pool balance response structure"""
|
|
27
|
-
|
|
28
|
-
pool_address: Required[str]
|
|
29
|
-
chain_id: Required[int]
|
|
30
|
-
user_address: Required[str]
|
|
31
|
-
balance: Required[str]
|
|
17
|
+
balance: Required[int]
|
|
32
18
|
balance_human: NotRequired[float | None]
|
|
33
19
|
usd_value: NotRequired[float | None]
|
|
20
|
+
address: NotRequired[str]
|
|
21
|
+
token_id: NotRequired[str | None]
|
|
22
|
+
wallet_address: NotRequired[str]
|
|
23
|
+
chain_id: NotRequired[int]
|
|
34
24
|
|
|
35
25
|
|
|
36
26
|
class WalletClient(WayfinderClient):
|
|
37
|
-
def __init__(self
|
|
38
|
-
super().__init__(
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__()
|
|
39
29
|
self.api_base_url = get_api_base_url()
|
|
40
|
-
self._auth_client = AuthClient(api_key=api_key)
|
|
41
30
|
|
|
42
|
-
async def
|
|
31
|
+
async def get_token_balance_for_address(
|
|
43
32
|
self,
|
|
44
33
|
*,
|
|
45
|
-
token_id: str,
|
|
46
34
|
wallet_address: str,
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
query: str,
|
|
36
|
+
chain_id: int | None = None,
|
|
37
|
+
) -> AddressBalance:
|
|
49
38
|
"""
|
|
50
|
-
Fetch a
|
|
39
|
+
Fetch a balance for a wallet address + chain + query.
|
|
51
40
|
|
|
52
|
-
|
|
41
|
+
Args:
|
|
42
|
+
wallet_address: Wallet address
|
|
43
|
+
query: Token address, pool address, or equivalent identifier
|
|
44
|
+
chain_id: Chain ID (required)
|
|
53
45
|
"""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"token_id": token_id,
|
|
57
|
-
"wallet_address": wallet_address,
|
|
58
|
-
"human_readable": human_readable,
|
|
59
|
-
}
|
|
60
|
-
response = await self._authed_request("POST", url, json=payload)
|
|
61
|
-
data = response.json()
|
|
62
|
-
return data.get("data", data)
|
|
46
|
+
if chain_id is None:
|
|
47
|
+
raise ValueError("chain_id is required")
|
|
63
48
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
pool_address: str,
|
|
68
|
-
chain_id: int,
|
|
69
|
-
user_address: str,
|
|
70
|
-
human_readable: bool = True,
|
|
71
|
-
) -> PoolBalance:
|
|
72
|
-
"""
|
|
73
|
-
Fetch a wallet's LP/share balance for a given pool address and chain.
|
|
74
|
-
|
|
75
|
-
Mirrors POST /api/v1/public/balances/pool/
|
|
76
|
-
"""
|
|
77
|
-
url = f"{self.api_base_url}/public/balances/pool/"
|
|
78
|
-
payload = {
|
|
79
|
-
"pool_address": pool_address,
|
|
49
|
+
url = f"{self.api_base_url}/v1/blockchain/balances/address/"
|
|
50
|
+
params = {
|
|
51
|
+
"wallet_address": wallet_address,
|
|
80
52
|
"chain_id": chain_id,
|
|
81
|
-
"
|
|
82
|
-
"human_readable": human_readable,
|
|
53
|
+
"query": query,
|
|
83
54
|
}
|
|
84
|
-
response = await self._authed_request("
|
|
85
|
-
|
|
86
|
-
return data.get("data", data)
|
|
55
|
+
response = await self._authed_request("GET", url, params=params)
|
|
56
|
+
return response.json()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import os
|
|
3
2
|
import time
|
|
4
3
|
from typing import Any
|
|
5
4
|
|
|
@@ -11,13 +10,11 @@ from wayfinder_paths.core.constants.base import DEFAULT_HTTP_TIMEOUT
|
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class WayfinderClient:
|
|
14
|
-
def __init__(self
|
|
13
|
+
def __init__(self):
|
|
15
14
|
"""
|
|
16
15
|
Initialize WayfinderClient.
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
api_key: Optional API key for service account authentication.
|
|
20
|
-
If provided, uses API key auth. Otherwise falls back to config.json.
|
|
17
|
+
API key is loaded from system.api_key in config.json.
|
|
21
18
|
"""
|
|
22
19
|
self.api_base_url = f"{get_api_base_url()}/"
|
|
23
20
|
timeout = httpx.Timeout(DEFAULT_HTTP_TIMEOUT)
|
|
@@ -27,153 +24,51 @@ class WayfinderClient:
|
|
|
27
24
|
"Content-Type": "application/json",
|
|
28
25
|
}
|
|
29
26
|
|
|
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 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
27
|
def clear_auth(self) -> None:
|
|
49
|
-
"""Clear
|
|
50
|
-
self.headers.pop("
|
|
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"{get_api_base_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
|
|
28
|
+
"""Clear X-API-KEY header."""
|
|
29
|
+
self.headers.pop("X-API-KEY", None)
|
|
83
30
|
|
|
84
31
|
def _load_config_credentials(self) -> dict[str, str | None]:
|
|
85
32
|
"""
|
|
86
|
-
Load
|
|
33
|
+
Load API key from config.json.
|
|
87
34
|
Expected shape:
|
|
88
35
|
{
|
|
89
|
-
"user": { "username": ..., "password": ..., "refresh_token": ..., "api_key": ... },
|
|
90
36
|
"system": { "api_key": ... }
|
|
91
37
|
}
|
|
92
38
|
"""
|
|
93
39
|
try:
|
|
94
40
|
with open("config.json") as f:
|
|
95
41
|
cfg = json.load(f)
|
|
96
|
-
user = cfg.get("user", {}) if isinstance(cfg, dict) else {}
|
|
97
42
|
system = cfg.get("system", {}) if isinstance(cfg, dict) else {}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"password": user.get("password"),
|
|
101
|
-
"refresh_token": user.get("refresh_token"),
|
|
102
|
-
"api_key": user.get("api_key") or system.get("api_key"),
|
|
103
|
-
}
|
|
43
|
+
api_key = system.get("api_key")
|
|
44
|
+
return {"api_key": api_key}
|
|
104
45
|
except (FileNotFoundError, json.JSONDecodeError, OSError) as e:
|
|
105
46
|
logger.debug(f"Could not load config file at config.json: {e}")
|
|
106
|
-
return {
|
|
107
|
-
"username": None,
|
|
108
|
-
"password": None,
|
|
109
|
-
"refresh_token": None,
|
|
110
|
-
"api_key": None,
|
|
111
|
-
}
|
|
47
|
+
return {"api_key": None}
|
|
112
48
|
except Exception as e:
|
|
113
49
|
logger.warning(f"Unexpected error loading config file at config.json: {e}")
|
|
114
|
-
return {
|
|
115
|
-
"username": None,
|
|
116
|
-
"password": None,
|
|
117
|
-
"refresh_token": None,
|
|
118
|
-
"api_key": None,
|
|
119
|
-
}
|
|
50
|
+
return {"api_key": None}
|
|
120
51
|
|
|
121
|
-
|
|
52
|
+
def _ensure_api_key(self) -> bool:
|
|
122
53
|
"""
|
|
123
|
-
Ensure
|
|
124
|
-
Raises PermissionError if no
|
|
54
|
+
Ensure X-API-KEY header is set from system.api_key in config.json.
|
|
55
|
+
Raises PermissionError if no API key found.
|
|
125
56
|
"""
|
|
126
|
-
if self.headers.get("
|
|
57
|
+
if self.headers.get("X-API-KEY"):
|
|
127
58
|
return True
|
|
128
59
|
|
|
129
|
-
|
|
130
|
-
api_key =
|
|
131
|
-
if not api_key:
|
|
132
|
-
creds = self._load_config_credentials()
|
|
133
|
-
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
60
|
+
creds = self._load_config_credentials()
|
|
61
|
+
api_key = creds.get("api_key")
|
|
134
62
|
|
|
135
63
|
if api_key:
|
|
136
64
|
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
137
65
|
if not api_key:
|
|
138
66
|
raise ValueError("API key cannot be empty")
|
|
139
|
-
self.headers["
|
|
67
|
+
self.headers["X-API-KEY"] = api_key
|
|
140
68
|
return True
|
|
141
69
|
|
|
142
|
-
# Fall back to OAuth token-based auth
|
|
143
|
-
creds = self._load_config_credentials()
|
|
144
|
-
refresh = creds.get("refresh_token")
|
|
145
|
-
|
|
146
|
-
if refresh:
|
|
147
|
-
self._refresh_token = refresh
|
|
148
|
-
refreshed = await self._refresh_access_token()
|
|
149
|
-
if refreshed:
|
|
150
|
-
return True
|
|
151
|
-
|
|
152
|
-
username = creds.get("username")
|
|
153
|
-
password = creds.get("password")
|
|
154
|
-
|
|
155
|
-
if username and password:
|
|
156
|
-
try:
|
|
157
|
-
url = f"{get_api_base_url()}/auth/token/"
|
|
158
|
-
payload = {"username": username, "password": password}
|
|
159
|
-
response = await self.client.post(
|
|
160
|
-
url,
|
|
161
|
-
json=payload,
|
|
162
|
-
headers={"Content-Type": "application/json"},
|
|
163
|
-
)
|
|
164
|
-
if response.status_code == 200:
|
|
165
|
-
data = response.json()
|
|
166
|
-
access = data.get("access") or data.get("access_token")
|
|
167
|
-
refresh = data.get("refresh") or data.get("refresh_token")
|
|
168
|
-
self.set_tokens(access, refresh)
|
|
169
|
-
return bool(access)
|
|
170
|
-
except Exception as e:
|
|
171
|
-
logger.debug(f"Failed to authenticate with username/password: {e}")
|
|
172
|
-
pass
|
|
173
|
-
|
|
174
70
|
raise PermissionError(
|
|
175
|
-
"Not authenticated: provide api_key
|
|
176
|
-
"or username+password/refresh_token in config.json for personal access"
|
|
71
|
+
"Not authenticated: provide api_key in system.api_key in config.json"
|
|
177
72
|
)
|
|
178
73
|
|
|
179
74
|
async def _request(
|
|
@@ -182,49 +77,32 @@ class WayfinderClient:
|
|
|
182
77
|
url: str,
|
|
183
78
|
*,
|
|
184
79
|
headers: dict[str, str] | None = None,
|
|
185
|
-
retry_on_401: bool =
|
|
80
|
+
retry_on_401: bool = False,
|
|
186
81
|
**kwargs: Any,
|
|
187
82
|
) -> httpx.Response:
|
|
188
83
|
"""
|
|
189
|
-
Wrapper around httpx that injects
|
|
190
|
-
Ensures API key
|
|
84
|
+
Wrapper around httpx that injects X-API-KEY header.
|
|
85
|
+
Ensures API key is set in headers when available (for authentication and rate limiting).
|
|
191
86
|
"""
|
|
192
87
|
logger.debug(f"Making {method} request to {url}")
|
|
193
88
|
start_time = time.time()
|
|
194
89
|
|
|
195
|
-
# Ensure API key
|
|
90
|
+
# Ensure API key is set in headers if available and not already set
|
|
196
91
|
# This ensures API keys are passed to all endpoints (including public ones) for rate limiting
|
|
197
|
-
if not self.headers.get("
|
|
198
|
-
|
|
199
|
-
api_key =
|
|
200
|
-
if not api_key:
|
|
201
|
-
creds = self._load_config_credentials()
|
|
202
|
-
api_key = creds.get("api_key") or os.getenv("WAYFINDER_API_KEY")
|
|
92
|
+
if not self.headers.get("X-API-KEY"):
|
|
93
|
+
creds = self._load_config_credentials()
|
|
94
|
+
api_key = creds.get("api_key")
|
|
203
95
|
|
|
204
96
|
if api_key:
|
|
205
97
|
api_key = api_key.strip() if isinstance(api_key, str) else api_key
|
|
206
98
|
if api_key:
|
|
207
|
-
self.headers["
|
|
99
|
+
self.headers["X-API-KEY"] = api_key
|
|
208
100
|
|
|
209
101
|
merged_headers = dict(self.headers)
|
|
210
102
|
if headers:
|
|
211
103
|
merged_headers.update(headers)
|
|
212
104
|
resp = await self.client.request(method, url, headers=merged_headers, **kwargs)
|
|
213
105
|
|
|
214
|
-
if resp.status_code == 401 and retry_on_401 and self._refresh_token:
|
|
215
|
-
logger.info("Received 401, attempting token refresh and retry")
|
|
216
|
-
refreshed = await self._refresh_access_token()
|
|
217
|
-
if refreshed:
|
|
218
|
-
merged_headers = dict(self.headers)
|
|
219
|
-
if headers:
|
|
220
|
-
merged_headers.update(headers)
|
|
221
|
-
resp = await self.client.request(
|
|
222
|
-
method, url, headers=merged_headers, **kwargs
|
|
223
|
-
)
|
|
224
|
-
logger.info("Retry after token refresh successful")
|
|
225
|
-
else:
|
|
226
|
-
logger.error("Token refresh failed, request will fail")
|
|
227
|
-
|
|
228
106
|
elapsed = time.time() - start_time
|
|
229
107
|
if resp.status_code >= 400:
|
|
230
108
|
logger.warning(
|
|
@@ -247,17 +125,7 @@ class WayfinderClient:
|
|
|
247
125
|
**kwargs: Any,
|
|
248
126
|
) -> httpx.Response:
|
|
249
127
|
"""
|
|
250
|
-
Ensure
|
|
251
|
-
Retries once on 401 by re-acquiring tokens.
|
|
128
|
+
Ensure X-API-KEY header is set (from system.api_key in config.json) and perform the request.
|
|
252
129
|
"""
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
raise PermissionError("Not authenticated: set env tokens or credentials")
|
|
256
|
-
try:
|
|
257
|
-
return await self._request(method, url, headers=headers, **kwargs)
|
|
258
|
-
except httpx.HTTPStatusError as e:
|
|
259
|
-
if e.response is not None and e.response.status_code == 401:
|
|
260
|
-
# Retry after attempting re-acquire/refresh
|
|
261
|
-
await self._ensure_bearer_token()
|
|
262
|
-
return await self._request(method, url, headers=headers, **kwargs)
|
|
263
|
-
raise
|
|
130
|
+
self._ensure_api_key()
|
|
131
|
+
return await self._request(method, url, headers=headers, **kwargs)
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
Core client modules for API communication
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from wayfinder_paths.core.clients.AuthClient import AuthClient
|
|
6
5
|
from wayfinder_paths.core.clients.BRAPClient import BRAPClient
|
|
7
6
|
from wayfinder_paths.core.clients.ClientManager import ClientManager
|
|
8
7
|
from wayfinder_paths.core.clients.HyperlendClient import HyperlendClient
|
|
@@ -23,7 +22,6 @@ from wayfinder_paths.core.clients.WayfinderClient import WayfinderClient
|
|
|
23
22
|
__all__ = [
|
|
24
23
|
"WayfinderClient",
|
|
25
24
|
"ClientManager",
|
|
26
|
-
"AuthClient",
|
|
27
25
|
"TokenClient",
|
|
28
26
|
"WalletClient",
|
|
29
27
|
"LedgerClient",
|