wayfinder-paths 0.1.8__py3-none-any.whl → 0.1.10__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 +6 -15
- wayfinder_paths/adapters/balance_adapter/README.md +1 -2
- wayfinder_paths/adapters/balance_adapter/adapter.py +4 -4
- wayfinder_paths/adapters/brap_adapter/README.md +1 -1
- wayfinder_paths/adapters/brap_adapter/adapter.py +139 -74
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -7
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +0 -54
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
- wayfinder_paths/adapters/ledger_adapter/README.md +1 -1
- wayfinder_paths/adapters/moonwell_adapter/README.md +174 -0
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +1226 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +635 -0
- wayfinder_paths/adapters/pool_adapter/README.md +1 -77
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -122
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -57
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -86
- wayfinder_paths/adapters/token_adapter/README.md +1 -1
- wayfinder_paths/core/clients/ClientManager.py +1 -22
- wayfinder_paths/core/clients/WalletClient.py +0 -8
- wayfinder_paths/core/clients/WayfinderClient.py +7 -12
- wayfinder_paths/core/clients/__init__.py +0 -8
- wayfinder_paths/core/clients/protocols.py +0 -60
- wayfinder_paths/core/config.py +5 -45
- wayfinder_paths/core/constants/__init__.py +0 -2
- wayfinder_paths/core/constants/base.py +6 -2
- wayfinder_paths/core/constants/moonwell_abi.py +411 -0
- wayfinder_paths/core/services/base.py +7 -1
- wayfinder_paths/core/services/local_evm_txn.py +223 -222
- wayfinder_paths/core/services/local_token_txn.py +103 -92
- wayfinder_paths/core/services/web3_service.py +0 -2
- wayfinder_paths/core/settings.py +8 -8
- wayfinder_paths/core/strategies/Strategy.py +1 -5
- wayfinder_paths/core/strategies/descriptors.py +1 -1
- wayfinder_paths/core/utils/evm_helpers.py +7 -12
- wayfinder_paths/core/wallets/README.md +3 -6
- wayfinder_paths/run_strategy.py +62 -105
- wayfinder_paths/scripts/create_strategy.py +2 -27
- wayfinder_paths/scripts/make_wallets.py +1 -25
- wayfinder_paths/scripts/run_strategy.py +37 -9
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -3
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +87 -138
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +96 -58
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -17
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +4 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -29
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +53 -14
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +108 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/examples.json +11 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2975 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +886 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +2 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -4
- wayfinder_paths/templates/adapter/README.md +5 -21
- wayfinder_paths/templates/adapter/adapter.py +1 -2
- wayfinder_paths/templates/adapter/test_adapter.py +1 -1
- wayfinder_paths/templates/strategy/README.md +4 -21
- wayfinder_paths/templates/strategy/test_strategy.py +0 -4
- wayfinder_paths/tests/test_smoke_manifest.py +17 -2
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/METADATA +64 -201
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/RECORD +64 -71
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +0 -8
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +0 -11
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +0 -10
- wayfinder_paths/adapters/token_adapter/manifest.yaml +0 -6
- wayfinder_paths/core/clients/SimulationClient.py +0 -192
- wayfinder_paths/core/clients/TransactionClient.py +0 -63
- wayfinder_paths/core/engine/manifest.py +0 -97
- wayfinder_paths/scripts/validate_manifests.py +0 -213
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +0 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/manifest.yaml +0 -7
- wayfinder_paths/strategies/stablecoin_yield_strategy/manifest.yaml +0 -17
- wayfinder_paths/templates/adapter/manifest.yaml +0 -6
- wayfinder_paths/templates/strategy/manifest.yaml +0 -8
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.10.dist-info}/WHEEL +0 -0
wayfinder_paths/CONFIG_GUIDE.md
CHANGED
|
@@ -52,7 +52,6 @@ cp wayfinder_paths/config.example.json config.json
|
|
|
52
52
|
**Option 1: Service Account (API Key) - Recommended for Production**
|
|
53
53
|
- `user.api_key` - Your Wayfinder API key for service account authentication (loaded directly by clients from config.json)
|
|
54
54
|
- OR `system.api_key` - System-level API key (alternative location, loaded directly by clients)
|
|
55
|
-
- OR set `WAYFINDER_API_KEY` environment variable
|
|
56
55
|
- OR pass `api_key` parameter to strategy constructor
|
|
57
56
|
|
|
58
57
|
**Note:** API keys in `config.json` are loaded directly by `WayfinderClient` via `_load_config_credentials()`, not through the `UserConfig` or `SystemConfig` dataclasses. This is intentional to allow flexible credential loading.
|
|
@@ -139,7 +138,7 @@ just create-strategy "My Strategy Name"
|
|
|
139
138
|
# This automatically:
|
|
140
139
|
# - Creates the strategy directory
|
|
141
140
|
# - Generates a wallet with label matching the directory name
|
|
142
|
-
# - Updates the
|
|
141
|
+
# - Updates the strategy files with the correct names
|
|
143
142
|
```
|
|
144
143
|
|
|
145
144
|
### Wallet Structure
|
|
@@ -229,7 +228,6 @@ The strategy system supports multiple wallet types through a wallet abstraction
|
|
|
229
228
|
|
|
230
229
|
By default, adapters use `LocalWalletProvider` which resolves private keys from:
|
|
231
230
|
- `wallets.json` (matched by address)
|
|
232
|
-
- Environment variables (`PRIVATE_KEY`, `PRIVATE_KEY_STRATEGY`)
|
|
233
231
|
- Wallet config in `config.json`
|
|
234
232
|
|
|
235
233
|
No code changes are required - existing strategies continue to work.
|
|
@@ -270,7 +268,7 @@ Wayfinder Paths supports **dual authentication** for different use cases:
|
|
|
270
268
|
|
|
271
269
|
**Best for:** Backend services, automated systems, and production deployments with higher rate limits.
|
|
272
270
|
|
|
273
|
-
API keys provide service account authentication and are automatically discovered by all clients. You can provide an API key in
|
|
271
|
+
API keys provide service account authentication and are automatically discovered by all clients. You can provide an API key in two ways:
|
|
274
272
|
|
|
275
273
|
#### Option A: Strategy Constructor (Programmatic)
|
|
276
274
|
```python
|
|
@@ -278,16 +276,11 @@ from wayfinder_paths.strategies.stablecoin_yield_strategy.strategy import Stable
|
|
|
278
276
|
|
|
279
277
|
strategy = StablecoinYieldStrategy(
|
|
280
278
|
config={...},
|
|
281
|
-
api_key="sk_live_abc123..." #
|
|
279
|
+
api_key="sk_live_abc123..." # API key is auto-discovered by all clients
|
|
282
280
|
)
|
|
283
281
|
```
|
|
284
282
|
|
|
285
|
-
#### Option B:
|
|
286
|
-
```bash
|
|
287
|
-
export WAYFINDER_API_KEY="sk_live_abc123..."
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
#### Option C: config.json
|
|
283
|
+
#### Option B: config.json
|
|
291
284
|
```json
|
|
292
285
|
{
|
|
293
286
|
"user": {
|
|
@@ -299,14 +292,12 @@ export WAYFINDER_API_KEY="sk_live_abc123..."
|
|
|
299
292
|
}
|
|
300
293
|
```
|
|
301
294
|
|
|
302
|
-
**Priority Order:** Constructor parameter > `config.json` (user.api_key or system.api_key)
|
|
295
|
+
**Priority Order:** Constructor parameter > `config.json` (user.api_key or system.api_key)
|
|
303
296
|
|
|
304
297
|
**How It Works:**
|
|
305
|
-
- When a strategy receives an `api_key`, it sets `os.environ["WAYFINDER_API_KEY"]`
|
|
306
298
|
- All clients created by adapters automatically discover the API key from:
|
|
307
299
|
- Constructor parameter (if passed)
|
|
308
300
|
- `config.json` (via `WayfinderClient._load_config_credentials()` which reads `user.api_key` or `system.api_key`)
|
|
309
|
-
- `WAYFINDER_API_KEY` environment variable
|
|
310
301
|
- API keys are included in **all** API requests (including public endpoints) for rate limiting
|
|
311
302
|
- No need to pass API keys explicitly to adapters or clients—they auto-discover it
|
|
312
303
|
- **Note:** API keys in `config.json` are loaded directly by clients, not stored in the `UserConfig` or `SystemConfig` dataclasses
|
|
@@ -379,7 +370,7 @@ To use custom RPC endpoints, update the `strategy.rpc_urls` section in `config.j
|
|
|
379
370
|
**Issue:** "Authentication failed"
|
|
380
371
|
- **If using API key:**
|
|
381
372
|
- Verify API key is correct and not expired
|
|
382
|
-
- Check that API key is set in one of: constructor parameter
|
|
373
|
+
- Check that API key is set in one of: constructor parameter or `config.json` (user.api_key or system.api_key)
|
|
383
374
|
- Ensure API key has proper permissions for the operations you're performing
|
|
384
375
|
- **If using OAuth:**
|
|
385
376
|
- Check that `username` and `password` are correct in `config.json`
|
|
@@ -3,12 +3,11 @@
|
|
|
3
3
|
Adapter that exposes wallet, token, and pool balances backed by `WalletClient`/`TokenClient` and now orchestrates transfers between the configured main/strategy wallets (with ledger bookkeeping).
|
|
4
4
|
|
|
5
5
|
- Entrypoint: `adapters.balance_adapter.adapter.BalanceAdapter`
|
|
6
|
-
- Manifest: `manifest.yaml`
|
|
7
6
|
- Tests: `test_adapter.py`
|
|
8
7
|
|
|
9
8
|
## Capabilities
|
|
10
9
|
|
|
11
|
-
The adapter
|
|
10
|
+
The adapter provides both wallet read and wallet transfer capabilities. Transfers are executed by leveraging the shared `DefaultWeb3Service.token_transactions` helper, but ledger recording + wallet selection now live inside the adapter.
|
|
12
11
|
|
|
13
12
|
## Construction
|
|
14
13
|
|
|
@@ -170,7 +170,7 @@ class BalanceAdapter(BaseAdapter):
|
|
|
170
170
|
token_amount=str(amount),
|
|
171
171
|
usd_value=usd_value,
|
|
172
172
|
data={
|
|
173
|
-
"token_id": token_info.get("
|
|
173
|
+
"token_id": token_info.get("token_id"),
|
|
174
174
|
"amount": str(amount),
|
|
175
175
|
"usd_value": usd_value,
|
|
176
176
|
},
|
|
@@ -180,7 +180,7 @@ class BalanceAdapter(BaseAdapter):
|
|
|
180
180
|
self.logger.warning(
|
|
181
181
|
"Ledger entry failed",
|
|
182
182
|
wallet=wallet_address,
|
|
183
|
-
token_id=token_info.get("
|
|
183
|
+
token_id=token_info.get("token_id"),
|
|
184
184
|
amount=amount,
|
|
185
185
|
error=response,
|
|
186
186
|
)
|
|
@@ -188,13 +188,13 @@ class BalanceAdapter(BaseAdapter):
|
|
|
188
188
|
self.logger.warning(
|
|
189
189
|
f"Ledger entry raised: {exc}",
|
|
190
190
|
wallet=wallet_address,
|
|
191
|
-
token_id=token_info.get("
|
|
191
|
+
token_id=token_info.get("token_id"),
|
|
192
192
|
)
|
|
193
193
|
|
|
194
194
|
async def _token_amount_usd(
|
|
195
195
|
self, token_info: dict[str, Any], amount: float
|
|
196
196
|
) -> float:
|
|
197
|
-
token_id = token_info.get("
|
|
197
|
+
token_id = token_info.get("token_id")
|
|
198
198
|
if not token_id:
|
|
199
199
|
return 0.0
|
|
200
200
|
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
@@ -17,7 +17,7 @@ The adapter uses the BRAPClient which automatically handles authentication and A
|
|
|
17
17
|
|
|
18
18
|
The BRAPClient will automatically:
|
|
19
19
|
- Use the WAYFINDER_API_URL from settings
|
|
20
|
-
- Handle authentication via
|
|
20
|
+
- Handle authentication via config.json
|
|
21
21
|
- Manage token refresh and retry logic
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
@@ -10,12 +10,8 @@ from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
|
10
10
|
from wayfinder_paths.core.adapters.models import SWAP
|
|
11
11
|
from wayfinder_paths.core.clients.BRAPClient import BRAPClient, BRAPQuote
|
|
12
12
|
from wayfinder_paths.core.clients.LedgerClient import TransactionRecord
|
|
13
|
-
from wayfinder_paths.core.clients.SimulationClient import (
|
|
14
|
-
SimulationClient,
|
|
15
|
-
SimulationResult,
|
|
16
|
-
)
|
|
17
13
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
18
|
-
from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE
|
|
14
|
+
from wayfinder_paths.core.constants import DEFAULT_SLIPPAGE, ZERO_ADDRESS
|
|
19
15
|
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
20
16
|
from wayfinder_paths.core.services.base import Web3Service
|
|
21
17
|
from wayfinder_paths.core.settings import settings
|
|
@@ -45,7 +41,6 @@ class BRAPAdapter(BaseAdapter):
|
|
|
45
41
|
config: dict[str, Any] | None = None,
|
|
46
42
|
*,
|
|
47
43
|
web3_service: Web3Service,
|
|
48
|
-
simulation: bool = False,
|
|
49
44
|
):
|
|
50
45
|
super().__init__("brap_adapter", config)
|
|
51
46
|
self.brap_client = BRAPClient()
|
|
@@ -55,8 +50,6 @@ class BRAPAdapter(BaseAdapter):
|
|
|
55
50
|
self.web3_service = web3_service
|
|
56
51
|
self.wallet_provider = web3_service.evm_transactions
|
|
57
52
|
self.token_transactions = web3_service.token_transactions
|
|
58
|
-
self.simulation = simulation
|
|
59
|
-
self.simulation_client = SimulationClient() if simulation else None
|
|
60
53
|
|
|
61
54
|
async def get_swap_quote(
|
|
62
55
|
self,
|
|
@@ -115,6 +108,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
115
108
|
amount: str,
|
|
116
109
|
slippage: float | None = None,
|
|
117
110
|
wayfinder_fee: float | None = None,
|
|
111
|
+
preferred_providers: list[str] | None = None,
|
|
118
112
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
119
113
|
"""
|
|
120
114
|
Get the best available quote for a swap operation.
|
|
@@ -129,6 +123,8 @@ class BRAPAdapter(BaseAdapter):
|
|
|
129
123
|
amount: Amount to swap (in smallest units)
|
|
130
124
|
slippage: Maximum slippage tolerance (optional)
|
|
131
125
|
wayfinder_fee: Wayfinder fee (optional)
|
|
126
|
+
preferred_providers: List of provider names in order of preference (optional).
|
|
127
|
+
If provided, selects first matching provider instead of "best".
|
|
132
128
|
|
|
133
129
|
Returns:
|
|
134
130
|
Tuple of (success, data) where data is best quote or error message
|
|
@@ -146,8 +142,27 @@ class BRAPAdapter(BaseAdapter):
|
|
|
146
142
|
wayfinder_fee=wayfinder_fee,
|
|
147
143
|
)
|
|
148
144
|
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
quotes_container = data.get("quotes", {})
|
|
146
|
+
# BRAP returns quotes in "all_quotes" not "quotes"
|
|
147
|
+
all_quotes = quotes_container.get("all_quotes", []) or quotes_container.get(
|
|
148
|
+
"quotes", []
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# If preferred providers specified, select by provider preference
|
|
152
|
+
if preferred_providers and all_quotes:
|
|
153
|
+
selected_quote = self._select_quote_by_provider(
|
|
154
|
+
all_quotes, preferred_providers
|
|
155
|
+
)
|
|
156
|
+
if selected_quote:
|
|
157
|
+
return (True, selected_quote)
|
|
158
|
+
# Fall through to best_quote if no preferred provider found
|
|
159
|
+
|
|
160
|
+
# Extract best quote from response - check nested quotes first, then top level
|
|
161
|
+
best_quote = (
|
|
162
|
+
quotes_container.get("best_quote")
|
|
163
|
+
or data.get("best_quote")
|
|
164
|
+
or data.get("best_route")
|
|
165
|
+
)
|
|
151
166
|
|
|
152
167
|
if not best_quote:
|
|
153
168
|
return (False, "No quotes available")
|
|
@@ -157,6 +172,58 @@ class BRAPAdapter(BaseAdapter):
|
|
|
157
172
|
self.logger.error(f"Error getting best quote: {e}")
|
|
158
173
|
return (False, str(e))
|
|
159
174
|
|
|
175
|
+
def _select_quote_by_provider(
|
|
176
|
+
self,
|
|
177
|
+
quotes: list[dict[str, Any]],
|
|
178
|
+
preferred_providers: list[str],
|
|
179
|
+
) -> dict[str, Any] | None:
|
|
180
|
+
"""
|
|
181
|
+
Select a quote from the list based on provider preference order.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
quotes: List of quote objects from BRAP API
|
|
185
|
+
preferred_providers: List of provider names in order of preference (case-insensitive)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The first matching quote, or None if no preferred provider found
|
|
189
|
+
"""
|
|
190
|
+
# Normalize preferred providers to lowercase for case-insensitive matching
|
|
191
|
+
preferred_lower = [p.lower() for p in preferred_providers]
|
|
192
|
+
|
|
193
|
+
# Build a map of provider name -> quotes
|
|
194
|
+
provider_quotes: dict[str, list[dict[str, Any]]] = {}
|
|
195
|
+
for quote in quotes:
|
|
196
|
+
# Provider name might be in different fields depending on BRAP response structure
|
|
197
|
+
provider = (
|
|
198
|
+
quote.get("provider")
|
|
199
|
+
or quote.get("provider_name")
|
|
200
|
+
or quote.get("source")
|
|
201
|
+
or quote.get("protocol")
|
|
202
|
+
or ""
|
|
203
|
+
).lower()
|
|
204
|
+
if provider:
|
|
205
|
+
if provider not in provider_quotes:
|
|
206
|
+
provider_quotes[provider] = []
|
|
207
|
+
provider_quotes[provider].append(quote)
|
|
208
|
+
|
|
209
|
+
# Select first matching provider in preference order
|
|
210
|
+
for pref in preferred_lower:
|
|
211
|
+
if pref in provider_quotes:
|
|
212
|
+
# Return the quote with highest output for this provider
|
|
213
|
+
provider_list = provider_quotes[pref]
|
|
214
|
+
best_for_provider = max(
|
|
215
|
+
provider_list, key=lambda q: int(q.get("output_amount", 0) or 0)
|
|
216
|
+
)
|
|
217
|
+
self.logger.info(f"Selected quote from preferred provider: {pref}")
|
|
218
|
+
return best_for_provider
|
|
219
|
+
|
|
220
|
+
# Log available providers for debugging
|
|
221
|
+
available = list(provider_quotes.keys())
|
|
222
|
+
self.logger.warning(
|
|
223
|
+
f"No preferred provider found. Wanted: {preferred_providers}, Available: {available}"
|
|
224
|
+
)
|
|
225
|
+
return None
|
|
226
|
+
|
|
160
227
|
async def calculate_swap_fees(
|
|
161
228
|
self,
|
|
162
229
|
from_token_address: str,
|
|
@@ -255,7 +322,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
255
322
|
)
|
|
256
323
|
|
|
257
324
|
quotes = data.get("quotes", {})
|
|
258
|
-
all_quotes = quotes.get("quotes", [])
|
|
325
|
+
all_quotes = quotes.get("all_quotes", []) or quotes.get("quotes", [])
|
|
259
326
|
best_quote = quotes.get("best_quote")
|
|
260
327
|
|
|
261
328
|
if not all_quotes:
|
|
@@ -298,9 +365,20 @@ class BRAPAdapter(BaseAdapter):
|
|
|
298
365
|
amount: str,
|
|
299
366
|
slippage: float = DEFAULT_SLIPPAGE,
|
|
300
367
|
strategy_name: str | None = None,
|
|
368
|
+
preferred_providers: list[str] | None = None,
|
|
301
369
|
) -> tuple[bool, Any]:
|
|
302
370
|
"""
|
|
303
371
|
Execute a swap by looking up token metadata via token IDs.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
from_token_id: Source token ID
|
|
375
|
+
to_token_id: Destination token ID
|
|
376
|
+
from_address: Wallet address
|
|
377
|
+
amount: Amount to swap (in smallest units)
|
|
378
|
+
slippage: Maximum slippage tolerance
|
|
379
|
+
strategy_name: Strategy name for ledger recording
|
|
380
|
+
preferred_providers: List of provider names in order of preference (e.g., ["enso", "aerodrome"]).
|
|
381
|
+
If provided, selects first matching provider instead of "best".
|
|
304
382
|
"""
|
|
305
383
|
from_token = await self.token_client.get_token_details(from_token_id)
|
|
306
384
|
if not from_token:
|
|
@@ -318,6 +396,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
318
396
|
to_address=from_address,
|
|
319
397
|
amount=amount,
|
|
320
398
|
slippage=slippage,
|
|
399
|
+
preferred_providers=preferred_providers,
|
|
321
400
|
)
|
|
322
401
|
if not success:
|
|
323
402
|
return (False, best_quote)
|
|
@@ -356,7 +435,14 @@ class BRAPAdapter(BaseAdapter):
|
|
|
356
435
|
or quote.get("inputAmount")
|
|
357
436
|
or transaction.get("value")
|
|
358
437
|
)
|
|
359
|
-
|
|
438
|
+
token_address = from_token.get("address")
|
|
439
|
+
token_address_l = str(token_address or "").lower()
|
|
440
|
+
is_native = token_address_l in {
|
|
441
|
+
"",
|
|
442
|
+
ZERO_ADDRESS.lower(),
|
|
443
|
+
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
|
|
444
|
+
}
|
|
445
|
+
if token_address and (not is_native) and spender and approve_amount:
|
|
360
446
|
approve_success, approve_response = await self._handle_token_approval(
|
|
361
447
|
chain=chain,
|
|
362
448
|
token_address=from_token.get("address"),
|
|
@@ -367,26 +453,39 @@ class BRAPAdapter(BaseAdapter):
|
|
|
367
453
|
if not approve_success:
|
|
368
454
|
return (False, approve_response)
|
|
369
455
|
|
|
370
|
-
if self.simulation:
|
|
371
|
-
simulation = await self._simulate_swap(
|
|
372
|
-
from_token, to_token, from_address, chain_id, quote
|
|
373
|
-
)
|
|
374
|
-
return (True, {"quote": quote, "simulation": simulation})
|
|
375
|
-
|
|
376
456
|
broadcast_success, broadcast_response = await self._broadcast_transaction(
|
|
377
457
|
transaction
|
|
378
458
|
)
|
|
459
|
+
self.logger.info(
|
|
460
|
+
f"Swap broadcast result: success={broadcast_success}, "
|
|
461
|
+
f"response={broadcast_response}"
|
|
462
|
+
)
|
|
379
463
|
if not broadcast_success:
|
|
380
464
|
return (False, broadcast_response)
|
|
381
465
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
466
|
+
# Record the swap operation in ledger - but don't let ledger errors fail the swap
|
|
467
|
+
# since the on-chain transaction already succeeded
|
|
468
|
+
try:
|
|
469
|
+
ledger_record = await self._record_swap_operation(
|
|
470
|
+
from_token=from_token,
|
|
471
|
+
to_token=to_token,
|
|
472
|
+
wallet_address=from_address,
|
|
473
|
+
quote=quote,
|
|
474
|
+
broadcast_response=broadcast_response,
|
|
475
|
+
strategy_name=strategy_name,
|
|
476
|
+
)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
self.logger.warning(
|
|
479
|
+
f"Ledger recording failed (swap succeeded on-chain): {e}"
|
|
480
|
+
)
|
|
481
|
+
# Return the quote with output amount so caller can proceed
|
|
482
|
+
ledger_record = {
|
|
483
|
+
"to_amount": quote.get("output_amount"),
|
|
484
|
+
"from_amount": quote.get("input_amount"),
|
|
485
|
+
"tx_hash": broadcast_response.get("tx_hash")
|
|
486
|
+
if isinstance(broadcast_response, dict)
|
|
487
|
+
else None,
|
|
488
|
+
}
|
|
390
489
|
return (True, ledger_record)
|
|
391
490
|
|
|
392
491
|
async def get_bridge_quote(
|
|
@@ -594,10 +693,10 @@ class BRAPAdapter(BaseAdapter):
|
|
|
594
693
|
)
|
|
595
694
|
if not build_success:
|
|
596
695
|
return False, approve_tx
|
|
597
|
-
return await self._broadcast_transaction(approve_tx)
|
|
696
|
+
return await self._broadcast_transaction(approve_tx, confirmations=2)
|
|
598
697
|
|
|
599
698
|
async def _broadcast_transaction(
|
|
600
|
-
self, transaction: dict[str, Any]
|
|
699
|
+
self, transaction: dict[str, Any], confirmations: int = 0
|
|
601
700
|
) -> tuple[bool, Any]:
|
|
602
701
|
if getattr(settings, "DRY_RUN", False):
|
|
603
702
|
return True, {"dry_run": True, "transaction": transaction}
|
|
@@ -605,6 +704,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
605
704
|
transaction,
|
|
606
705
|
wait_for_receipt=True,
|
|
607
706
|
timeout=DEFAULT_TRANSACTION_TIMEOUT,
|
|
707
|
+
confirmations=confirmations,
|
|
608
708
|
)
|
|
609
709
|
|
|
610
710
|
async def _record_swap_operation(
|
|
@@ -631,16 +731,19 @@ class BRAPAdapter(BaseAdapter):
|
|
|
631
731
|
response = broadcast_response if isinstance(broadcast_response, dict) else {}
|
|
632
732
|
operation_data = SWAP(
|
|
633
733
|
adapter=self.adapter_type,
|
|
634
|
-
from_token_id=from_token.get("id"),
|
|
635
|
-
to_token_id=to_token.get("id"),
|
|
636
|
-
from_amount=quote.get("input_amount"),
|
|
637
|
-
to_amount=quote.get("output_amount"),
|
|
734
|
+
from_token_id=str(from_token.get("id")),
|
|
735
|
+
to_token_id=str(to_token.get("id")),
|
|
736
|
+
from_amount=str(quote.get("input_amount")),
|
|
737
|
+
to_amount=str(quote.get("output_amount")),
|
|
638
738
|
from_amount_usd=from_amount_usd or 0,
|
|
639
739
|
to_amount_usd=to_amount_usd or 0,
|
|
640
|
-
transaction_hash=response.get("
|
|
641
|
-
|
|
740
|
+
transaction_hash=response.get("tx_hash")
|
|
741
|
+
or response.get("transaction_hash"),
|
|
742
|
+
transaction_chain_id=from_token.get("chain_id")
|
|
743
|
+
or (from_token.get("chain") or {}).get("id"),
|
|
642
744
|
transaction_status=response.get("transaction_status"),
|
|
643
|
-
|
|
745
|
+
# Don't pass raw receipt - it contains HexBytes that can't be JSON serialized
|
|
746
|
+
transaction_receipt=None,
|
|
644
747
|
)
|
|
645
748
|
|
|
646
749
|
try:
|
|
@@ -666,7 +769,7 @@ class BRAPAdapter(BaseAdapter):
|
|
|
666
769
|
if raw_amount is None:
|
|
667
770
|
return None
|
|
668
771
|
success, price_data = await self.token_adapter.get_token_price(
|
|
669
|
-
token_info.get("
|
|
772
|
+
token_info.get("token_id")
|
|
670
773
|
)
|
|
671
774
|
if not success or not price_data:
|
|
672
775
|
return None
|
|
@@ -677,44 +780,6 @@ class BRAPAdapter(BaseAdapter):
|
|
|
677
780
|
/ 10 ** int(decimals)
|
|
678
781
|
)
|
|
679
782
|
|
|
680
|
-
async def _simulate_swap(
|
|
681
|
-
self,
|
|
682
|
-
from_token: dict[str, Any],
|
|
683
|
-
to_token: dict[str, Any],
|
|
684
|
-
from_address: str,
|
|
685
|
-
chain_id: int,
|
|
686
|
-
quote: dict[str, Any],
|
|
687
|
-
) -> SimulationResult:
|
|
688
|
-
client = await self._get_simulation_client()
|
|
689
|
-
initial_balances = {"native": "5000000000000000000"}
|
|
690
|
-
if from_token.get("address"):
|
|
691
|
-
initial_balances[from_token.get("address")] = "1000000000000000000000000"
|
|
692
|
-
|
|
693
|
-
slippage = quote.get("slippage") or quote.get("slippage_percent")
|
|
694
|
-
if isinstance(slippage, str):
|
|
695
|
-
try:
|
|
696
|
-
slippage = float(slippage)
|
|
697
|
-
except ValueError:
|
|
698
|
-
slippage = DEFAULT_SLIPPAGE
|
|
699
|
-
slippage = slippage or DEFAULT_SLIPPAGE
|
|
700
|
-
|
|
701
|
-
amount = quote.get("input_amount") or quote.get("inputAmount") or "0"
|
|
702
|
-
return await client.simulate_swap(
|
|
703
|
-
from_token_address=from_token.get("address"),
|
|
704
|
-
to_token_address=to_token.get("address"),
|
|
705
|
-
from_chain_id=chain_id,
|
|
706
|
-
to_chain_id=chain_id,
|
|
707
|
-
amount=str(amount),
|
|
708
|
-
from_address=from_address,
|
|
709
|
-
slippage=float(slippage),
|
|
710
|
-
initial_balances=initial_balances,
|
|
711
|
-
)
|
|
712
|
-
|
|
713
|
-
async def _get_simulation_client(self) -> SimulationClient:
|
|
714
|
-
if not self.simulation_client:
|
|
715
|
-
self.simulation_client = SimulationClient()
|
|
716
|
-
return self.simulation_client
|
|
717
|
-
|
|
718
783
|
def _chain_id(self, chain: Any) -> int:
|
|
719
784
|
if isinstance(chain, dict):
|
|
720
785
|
chain_id = chain.get("id") or chain.get("chain_id")
|
|
@@ -12,7 +12,6 @@ from wayfinder_paths.core.clients.HyperlendClient import (
|
|
|
12
12
|
MarketEntry,
|
|
13
13
|
StableMarket,
|
|
14
14
|
)
|
|
15
|
-
from wayfinder_paths.core.clients.SimulationClient import SimulationClient
|
|
16
15
|
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
17
16
|
from wayfinder_paths.core.constants.hyperlend_abi import (
|
|
18
17
|
POOL_ABI,
|
|
@@ -37,16 +36,12 @@ class HyperlendAdapter(BaseAdapter):
|
|
|
37
36
|
self,
|
|
38
37
|
config: dict[str, Any],
|
|
39
38
|
web3_service: Web3Service,
|
|
40
|
-
simulation: bool = False,
|
|
41
39
|
) -> None:
|
|
42
40
|
super().__init__("hyperlend_adapter", config)
|
|
43
41
|
cfg = config or {}
|
|
44
42
|
adapter_cfg = cfg.get("hyperlend_adapter") or {}
|
|
45
43
|
|
|
46
44
|
self.hyperlend_client = HyperlendClient()
|
|
47
|
-
self.simulation = simulation
|
|
48
|
-
self.simulation_client = SimulationClient() if simulation else None
|
|
49
|
-
|
|
50
45
|
self.web3 = web3_service
|
|
51
46
|
self.token_txn_service = web3_service.token_transactions
|
|
52
47
|
|
|
@@ -237,8 +232,6 @@ class HyperlendAdapter(BaseAdapter):
|
|
|
237
232
|
return await self._broadcast_transaction(approve_tx)
|
|
238
233
|
|
|
239
234
|
async def _execute(self, tx: dict[str, Any]) -> tuple[bool, Any]:
|
|
240
|
-
if self.simulation:
|
|
241
|
-
return True, {"simulation": tx}
|
|
242
235
|
return await self.web3.broadcast_transaction(
|
|
243
236
|
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
244
237
|
)
|
|
@@ -74,7 +74,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
74
74
|
self,
|
|
75
75
|
config: dict[str, Any] | None = None,
|
|
76
76
|
*,
|
|
77
|
-
simulation: bool = False,
|
|
78
77
|
executor: HyperliquidExecutor | None = None,
|
|
79
78
|
) -> None:
|
|
80
79
|
super().__init__("hyperliquid_adapter", config)
|
|
@@ -85,7 +84,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
85
84
|
"Install with: poetry add hyperliquid"
|
|
86
85
|
)
|
|
87
86
|
|
|
88
|
-
self.simulation = simulation
|
|
89
87
|
self._cache = SimpleCache()
|
|
90
88
|
self._executor = executor
|
|
91
89
|
|
|
@@ -481,13 +479,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
481
479
|
Returns:
|
|
482
480
|
(success, response_data or error_message)
|
|
483
481
|
"""
|
|
484
|
-
if self.simulation:
|
|
485
|
-
self.logger.info(
|
|
486
|
-
f"[SIMULATION] place_market_order: asset={asset_id}, "
|
|
487
|
-
f"is_buy={is_buy}, size={size}, address={address}"
|
|
488
|
-
)
|
|
489
|
-
return True, {"simulation": True, "status": "ok"}
|
|
490
|
-
|
|
491
482
|
if not self._executor:
|
|
492
483
|
raise NotImplementedError(
|
|
493
484
|
"No Hyperliquid executor configured. "
|
|
@@ -525,12 +516,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
525
516
|
Returns:
|
|
526
517
|
(success, response_data or error_message)
|
|
527
518
|
"""
|
|
528
|
-
if self.simulation:
|
|
529
|
-
self.logger.info(
|
|
530
|
-
f"[SIMULATION] cancel_order: asset={asset_id}, oid={order_id}"
|
|
531
|
-
)
|
|
532
|
-
return True, {"simulation": True, "status": "ok"}
|
|
533
|
-
|
|
534
519
|
if not self._executor:
|
|
535
520
|
raise NotImplementedError(
|
|
536
521
|
"No Hyperliquid executor configured. "
|
|
@@ -565,12 +550,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
565
550
|
Returns:
|
|
566
551
|
(success, response_data or error_message)
|
|
567
552
|
"""
|
|
568
|
-
if self.simulation:
|
|
569
|
-
self.logger.info(
|
|
570
|
-
f"[SIMULATION] update_leverage: asset={asset_id}, leverage={leverage}"
|
|
571
|
-
)
|
|
572
|
-
return True, {"simulation": True, "status": "ok"}
|
|
573
|
-
|
|
574
553
|
if not self._executor:
|
|
575
554
|
raise NotImplementedError("No Hyperliquid executor configured.")
|
|
576
555
|
|
|
@@ -590,10 +569,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
590
569
|
address: str,
|
|
591
570
|
) -> tuple[bool, dict[str, Any]]:
|
|
592
571
|
"""Transfer USDC from spot to perp balance."""
|
|
593
|
-
if self.simulation:
|
|
594
|
-
self.logger.info(f"[SIMULATION] transfer_spot_to_perp: {amount} USDC")
|
|
595
|
-
return True, {"simulation": True, "status": "ok"}
|
|
596
|
-
|
|
597
572
|
if not self._executor:
|
|
598
573
|
raise NotImplementedError("No Hyperliquid executor configured.")
|
|
599
574
|
|
|
@@ -611,10 +586,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
611
586
|
address: str,
|
|
612
587
|
) -> tuple[bool, dict[str, Any]]:
|
|
613
588
|
"""Transfer USDC from perp to spot balance."""
|
|
614
|
-
if self.simulation:
|
|
615
|
-
self.logger.info(f"[SIMULATION] transfer_perp_to_spot: {amount} USDC")
|
|
616
|
-
return True, {"simulation": True, "status": "ok"}
|
|
617
|
-
|
|
618
589
|
if not self._executor:
|
|
619
590
|
raise NotImplementedError("No Hyperliquid executor configured.")
|
|
620
591
|
|
|
@@ -647,13 +618,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
647
618
|
Returns:
|
|
648
619
|
(success, response_data or error_message)
|
|
649
620
|
"""
|
|
650
|
-
if self.simulation:
|
|
651
|
-
self.logger.info(
|
|
652
|
-
f"[SIMULATION] place_stop_loss: asset={asset_id}, "
|
|
653
|
-
f"trigger={trigger_price}, size={size}"
|
|
654
|
-
)
|
|
655
|
-
return True, {"simulation": True, "status": "ok"}
|
|
656
|
-
|
|
657
621
|
if not self._executor:
|
|
658
622
|
raise NotImplementedError("No Hyperliquid executor configured.")
|
|
659
623
|
|
|
@@ -758,10 +722,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
758
722
|
|
|
759
723
|
Note: This is an L1 withdrawal handled by the Hyperliquid executor (signing required).
|
|
760
724
|
"""
|
|
761
|
-
if self.simulation:
|
|
762
|
-
self.logger.info(f"[SIMULATION] withdraw: {amount} USDC")
|
|
763
|
-
return True, {"simulation": True, "status": "ok"}
|
|
764
|
-
|
|
765
725
|
if not self._executor:
|
|
766
726
|
raise NotImplementedError("No Hyperliquid executor configured.")
|
|
767
727
|
|
|
@@ -856,13 +816,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
856
816
|
Returns:
|
|
857
817
|
(success, response_data or error_message)
|
|
858
818
|
"""
|
|
859
|
-
if self.simulation:
|
|
860
|
-
self.logger.info(
|
|
861
|
-
f"[SIMULATION] approve_builder_fee: builder={builder}, "
|
|
862
|
-
f"rate={max_fee_rate}, address={address}"
|
|
863
|
-
)
|
|
864
|
-
return True, {"simulation": True, "status": "ok"}
|
|
865
|
-
|
|
866
819
|
if not self._executor:
|
|
867
820
|
raise NotImplementedError("No Hyperliquid executor configured.")
|
|
868
821
|
|
|
@@ -903,13 +856,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
903
856
|
Returns:
|
|
904
857
|
(success, response_data or error_message)
|
|
905
858
|
"""
|
|
906
|
-
if self.simulation:
|
|
907
|
-
self.logger.info(
|
|
908
|
-
f"[SIMULATION] place_limit_order: asset={asset_id}, "
|
|
909
|
-
f"is_buy={is_buy}, price={price}, size={size}"
|
|
910
|
-
)
|
|
911
|
-
return True, {"simulation": True, "status": "ok"}
|
|
912
|
-
|
|
913
859
|
if not self._executor:
|
|
914
860
|
raise NotImplementedError("No Hyperliquid executor configured.")
|
|
915
861
|
|
|
@@ -17,7 +17,7 @@ from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdap
|
|
|
17
17
|
@pytest.fixture
|
|
18
18
|
def live_adapter():
|
|
19
19
|
"""Create adapter connected to real Hyperliquid API."""
|
|
20
|
-
return HyperliquidAdapter(config={}
|
|
20
|
+
return HyperliquidAdapter(config={})
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
class TestSpotAssetIDs:
|
|
@@ -17,7 +17,7 @@ The adapter uses the LedgerClient which automatically handles authentication and
|
|
|
17
17
|
|
|
18
18
|
The LedgerClient will automatically:
|
|
19
19
|
- Use the WAYFINDER_API_URL from settings
|
|
20
|
-
- Handle authentication via
|
|
20
|
+
- Handle authentication via config.json
|
|
21
21
|
- Manage token refresh and retry logic
|
|
22
22
|
|
|
23
23
|
## Usage
|