wayfinder-paths 0.1.11__py3-none-any.whl → 0.1.14__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 +36 -39
- 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 +87 -75
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +121 -59
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +22 -23
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +114 -60
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +44 -5
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +104 -0
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +0 -3
- wayfinder_paths/adapters/pool_adapter/README.md +11 -27
- wayfinder_paths/adapters/pool_adapter/adapter.py +11 -37
- wayfinder_paths/adapters/pool_adapter/examples.json +6 -7
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +8 -8
- 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 +5 -3
- wayfinder_paths/core/clients/BRAPClient.py +103 -62
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +127 -66
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +126 -88
- wayfinder_paths/core/clients/TokenClient.py +92 -37
- wayfinder_paths/core/clients/WalletClient.py +28 -58
- wayfinder_paths/core/clients/WayfinderClient.py +33 -166
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -52
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/config.py +60 -224
- wayfinder_paths/core/engine/StrategyJob.py +7 -55
- wayfinder_paths/core/services/local_evm_txn.py +28 -10
- wayfinder_paths/core/services/local_token_txn.py +1 -1
- wayfinder_paths/core/strategies/Strategy.py +3 -5
- wayfinder_paths/core/strategies/descriptors.py +7 -0
- wayfinder_paths/core/utils/evm_helpers.py +7 -3
- wayfinder_paths/core/utils/wallets.py +12 -19
- wayfinder_paths/core/wallets/README.md +1 -1
- wayfinder_paths/run_strategy.py +8 -17
- wayfinder_paths/scripts/create_strategy.py +5 -5
- wayfinder_paths/scripts/make_wallets.py +5 -5
- wayfinder_paths/scripts/run_strategy.py +3 -3
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +206 -526
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +228 -11
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +41 -25
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +1 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +10 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +12 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +3 -3
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +110 -78
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +44 -21
- 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.11.dist-info → wayfinder_paths-0.1.14.dist-info}/METADATA +21 -59
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/RECORD +64 -65
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- wayfinder_paths/core/settings.py +0 -61
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.11.dist-info → wayfinder_paths-0.1.14.dist-info}/WHEEL +0 -0
|
@@ -34,31 +34,31 @@ class TestHyperlendAdapter:
|
|
|
34
34
|
async def test_get_stable_markets_success(self, adapter, mock_hyperlend_client):
|
|
35
35
|
"""Test successful stable markets retrieval"""
|
|
36
36
|
mock_response = {
|
|
37
|
-
"markets":
|
|
38
|
-
{
|
|
39
|
-
"chain_id": 999,
|
|
40
|
-
"underlying_token": "0x1234...",
|
|
37
|
+
"markets": {
|
|
38
|
+
"0x1234...": {
|
|
41
39
|
"symbol": "USDT",
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
40
|
+
"symbol_canonical": "usdt",
|
|
41
|
+
"display_symbol": "USDT",
|
|
42
|
+
"reserve": {},
|
|
43
|
+
"decimals": 6,
|
|
44
|
+
"headroom": 1000000000000,
|
|
45
|
+
"supply_cap": 5000000000000,
|
|
46
46
|
},
|
|
47
|
-
{
|
|
48
|
-
"chain_id": 999,
|
|
49
|
-
"underlying_token": "0x5678...",
|
|
47
|
+
"0x5678...": {
|
|
50
48
|
"symbol": "USDC",
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
49
|
+
"symbol_canonical": "usdc",
|
|
50
|
+
"display_symbol": "USDC",
|
|
51
|
+
"reserve": {},
|
|
52
|
+
"decimals": 6,
|
|
53
|
+
"headroom": 2000000000000,
|
|
54
|
+
"supply_cap": 10000000000000,
|
|
55
55
|
},
|
|
56
|
-
|
|
56
|
+
},
|
|
57
|
+
"notes": [],
|
|
57
58
|
}
|
|
58
59
|
mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
|
|
59
60
|
|
|
60
61
|
success, data = await adapter.get_stable_markets(
|
|
61
|
-
chain_id=999,
|
|
62
62
|
required_underlying_tokens=1000.0,
|
|
63
63
|
buffer_bps=100,
|
|
64
64
|
min_buffer_tokens=100.0,
|
|
@@ -67,11 +67,9 @@ class TestHyperlendAdapter:
|
|
|
67
67
|
assert success is True
|
|
68
68
|
assert data == mock_response
|
|
69
69
|
mock_hyperlend_client.get_stable_markets.assert_called_once_with(
|
|
70
|
-
chain_id=999,
|
|
71
70
|
required_underlying_tokens=1000.0,
|
|
72
71
|
buffer_bps=100,
|
|
73
72
|
min_buffer_tokens=100.0,
|
|
74
|
-
is_stable_symbol=None,
|
|
75
73
|
)
|
|
76
74
|
|
|
77
75
|
@pytest.mark.asyncio
|
|
@@ -80,27 +78,29 @@ class TestHyperlendAdapter:
|
|
|
80
78
|
):
|
|
81
79
|
"""Test stable markets retrieval with only required chain_id"""
|
|
82
80
|
mock_response = {
|
|
83
|
-
"markets":
|
|
84
|
-
{
|
|
85
|
-
"chain_id": 999,
|
|
86
|
-
"underlying_token": "0x1234...",
|
|
81
|
+
"markets": {
|
|
82
|
+
"0x1234...": {
|
|
87
83
|
"symbol": "USDT",
|
|
88
|
-
"
|
|
84
|
+
"symbol_canonical": "usdt",
|
|
85
|
+
"display_symbol": "USDT",
|
|
86
|
+
"reserve": {},
|
|
87
|
+
"decimals": 6,
|
|
88
|
+
"headroom": 1000000000000,
|
|
89
|
+
"supply_cap": 5000000000000,
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
+
},
|
|
92
|
+
"notes": [],
|
|
91
93
|
}
|
|
92
94
|
mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
|
|
93
95
|
|
|
94
|
-
success, data = await adapter.get_stable_markets(
|
|
96
|
+
success, data = await adapter.get_stable_markets()
|
|
95
97
|
|
|
96
98
|
assert success is True
|
|
97
99
|
assert data == mock_response
|
|
98
100
|
mock_hyperlend_client.get_stable_markets.assert_called_once_with(
|
|
99
|
-
chain_id=999,
|
|
100
101
|
required_underlying_tokens=None,
|
|
101
102
|
buffer_bps=None,
|
|
102
103
|
min_buffer_tokens=None,
|
|
103
|
-
is_stable_symbol=None,
|
|
104
104
|
)
|
|
105
105
|
|
|
106
106
|
@pytest.mark.asyncio
|
|
@@ -108,21 +108,19 @@ class TestHyperlendAdapter:
|
|
|
108
108
|
self, adapter, mock_hyperlend_client
|
|
109
109
|
):
|
|
110
110
|
"""Test stable markets retrieval with partial optional parameters"""
|
|
111
|
-
mock_response = {"markets": []}
|
|
111
|
+
mock_response = {"markets": {}, "notes": []}
|
|
112
112
|
mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
|
|
113
113
|
|
|
114
114
|
success, data = await adapter.get_stable_markets(
|
|
115
|
-
|
|
115
|
+
required_underlying_tokens=500.0
|
|
116
116
|
)
|
|
117
117
|
|
|
118
118
|
assert success is True
|
|
119
119
|
assert data == mock_response
|
|
120
120
|
mock_hyperlend_client.get_stable_markets.assert_called_once_with(
|
|
121
|
-
chain_id=999,
|
|
122
121
|
required_underlying_tokens=500.0,
|
|
123
122
|
buffer_bps=None,
|
|
124
123
|
min_buffer_tokens=None,
|
|
125
|
-
is_stable_symbol=None,
|
|
126
124
|
)
|
|
127
125
|
|
|
128
126
|
@pytest.mark.asyncio
|
|
@@ -132,7 +130,7 @@ class TestHyperlendAdapter:
|
|
|
132
130
|
side_effect=Exception("API Error: Connection timeout")
|
|
133
131
|
)
|
|
134
132
|
|
|
135
|
-
success, data = await adapter.get_stable_markets(
|
|
133
|
+
success, data = await adapter.get_stable_markets()
|
|
136
134
|
|
|
137
135
|
assert success is False
|
|
138
136
|
assert "API Error: Connection timeout" in data
|
|
@@ -144,7 +142,7 @@ class TestHyperlendAdapter:
|
|
|
144
142
|
side_effect=Exception("HTTP 404 Not Found")
|
|
145
143
|
)
|
|
146
144
|
|
|
147
|
-
success, data = await adapter.get_stable_markets(
|
|
145
|
+
success, data = await adapter.get_stable_markets()
|
|
148
146
|
|
|
149
147
|
assert success is False
|
|
150
148
|
assert "404" in data or "Not Found" in data
|
|
@@ -154,14 +152,14 @@ class TestHyperlendAdapter:
|
|
|
154
152
|
self, adapter, mock_hyperlend_client
|
|
155
153
|
):
|
|
156
154
|
"""Test stable markets retrieval with empty response"""
|
|
157
|
-
mock_response = {"markets": []}
|
|
155
|
+
mock_response = {"markets": {}, "notes": []}
|
|
158
156
|
mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
|
|
159
157
|
|
|
160
|
-
success, data = await adapter.get_stable_markets(
|
|
158
|
+
success, data = await adapter.get_stable_markets()
|
|
161
159
|
|
|
162
160
|
assert success is True
|
|
163
161
|
assert data == mock_response
|
|
164
|
-
assert len(data.get("markets",
|
|
162
|
+
assert len(data.get("markets", {})) == 0
|
|
165
163
|
|
|
166
164
|
def test_adapter_type(self, adapter):
|
|
167
165
|
"""Test adapter has adapter_type"""
|
|
@@ -186,59 +184,95 @@ class TestHyperlendAdapter:
|
|
|
186
184
|
async def test_get_stable_markets_with_is_stable_symbol(
|
|
187
185
|
self, adapter, mock_hyperlend_client
|
|
188
186
|
):
|
|
189
|
-
"""Test stable markets retrieval with is_stable_symbol parameter"""
|
|
187
|
+
"""Test stable markets retrieval with is_stable_symbol parameter (ignored by API)"""
|
|
190
188
|
mock_response = {
|
|
191
|
-
"markets":
|
|
192
|
-
{
|
|
193
|
-
"chain_id": 999,
|
|
194
|
-
"underlying_token": "0x1234...",
|
|
189
|
+
"markets": {
|
|
190
|
+
"0x1234...": {
|
|
195
191
|
"symbol": "USDT",
|
|
196
|
-
"
|
|
192
|
+
"symbol_canonical": "usdt",
|
|
193
|
+
"display_symbol": "USDT",
|
|
194
|
+
"reserve": {},
|
|
195
|
+
"decimals": 6,
|
|
196
|
+
"headroom": 1000000000000,
|
|
197
|
+
"supply_cap": 5000000000000,
|
|
197
198
|
}
|
|
198
|
-
|
|
199
|
+
},
|
|
200
|
+
"notes": [],
|
|
199
201
|
}
|
|
200
202
|
mock_hyperlend_client.get_stable_markets = AsyncMock(return_value=mock_response)
|
|
201
203
|
|
|
202
|
-
success, data = await adapter.get_stable_markets(
|
|
203
|
-
chain_id=999, is_stable_symbol=True
|
|
204
|
-
)
|
|
204
|
+
success, data = await adapter.get_stable_markets()
|
|
205
205
|
|
|
206
206
|
assert success is True
|
|
207
207
|
assert data == mock_response
|
|
208
208
|
mock_hyperlend_client.get_stable_markets.assert_called_once_with(
|
|
209
|
-
chain_id=999,
|
|
210
209
|
required_underlying_tokens=None,
|
|
211
210
|
buffer_bps=None,
|
|
212
211
|
min_buffer_tokens=None,
|
|
213
|
-
is_stable_symbol=True,
|
|
214
212
|
)
|
|
215
213
|
|
|
216
214
|
@pytest.mark.asyncio
|
|
217
215
|
async def test_get_assets_view_success(self, adapter, mock_hyperlend_client):
|
|
218
216
|
"""Test successful assets view retrieval"""
|
|
219
217
|
mock_response = {
|
|
218
|
+
"block_number": 12345,
|
|
219
|
+
"user": "0x0c737cB5934afCb5B01965141F865F795B324080",
|
|
220
|
+
"native_balance_wei": 0,
|
|
221
|
+
"native_balance": 0.0,
|
|
220
222
|
"assets": [
|
|
221
223
|
{
|
|
222
|
-
"
|
|
224
|
+
"underlying": "0x1234...",
|
|
223
225
|
"symbol": "USDT",
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
226
|
+
"symbol_canonical": "usdt",
|
|
227
|
+
"symbol_display": "USDT",
|
|
228
|
+
"decimals": 6,
|
|
229
|
+
"a_token": "0x...",
|
|
230
|
+
"variable_debt_token": "0x...",
|
|
231
|
+
"usage_as_collateral_enabled": True,
|
|
232
|
+
"borrowing_enabled": True,
|
|
233
|
+
"is_active": True,
|
|
234
|
+
"is_frozen": False,
|
|
235
|
+
"is_paused": False,
|
|
236
|
+
"is_siloed_borrowing": False,
|
|
237
|
+
"is_stablecoin": True,
|
|
238
|
+
"underlying_wallet_balance": 1000.0,
|
|
239
|
+
"underlying_wallet_balance_wei": 1000000000,
|
|
240
|
+
"price_usd": 1.0,
|
|
241
|
+
"supply": 500.0,
|
|
242
|
+
"variable_borrow": 0.0,
|
|
243
|
+
"supply_usd": 500.0,
|
|
244
|
+
"variable_borrow_usd": 0.0,
|
|
245
|
+
"supply_apr": 0.05,
|
|
246
|
+
"supply_apy": 0.05,
|
|
247
|
+
"variable_borrow_apr": 0.07,
|
|
248
|
+
"variable_borrow_apy": 0.07,
|
|
227
249
|
}
|
|
228
250
|
],
|
|
229
|
-
"
|
|
251
|
+
"account_data": {
|
|
252
|
+
"total_collateral_base": 500,
|
|
253
|
+
"total_debt_base": 0,
|
|
254
|
+
"available_borrows_base": 400,
|
|
255
|
+
"current_liquidation_threshold": 8000,
|
|
256
|
+
"ltv": 7500,
|
|
257
|
+
"health_factor_wad": 115792089237316195423570985008687907853269984665640564039457584007913129639935,
|
|
258
|
+
"health_factor": 1.157920892373162e59,
|
|
259
|
+
},
|
|
260
|
+
"base_currency_info": {
|
|
261
|
+
"marketReferenceCurrencyUnit": 100000000,
|
|
262
|
+
"marketReferenceCurrencyPriceInUsd": 100000000,
|
|
263
|
+
"networkBaseTokenPriceInUsd": 0,
|
|
264
|
+
"networkBaseTokenPriceDecimals": 8,
|
|
265
|
+
},
|
|
230
266
|
}
|
|
231
267
|
mock_hyperlend_client.get_assets_view = AsyncMock(return_value=mock_response)
|
|
232
268
|
|
|
233
269
|
success, data = await adapter.get_assets_view(
|
|
234
|
-
chain_id=999,
|
|
235
270
|
user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
|
|
236
271
|
)
|
|
237
272
|
|
|
238
273
|
assert success is True
|
|
239
274
|
assert data == mock_response
|
|
240
275
|
mock_hyperlend_client.get_assets_view.assert_called_once_with(
|
|
241
|
-
chain_id=999,
|
|
242
276
|
user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
|
|
243
277
|
)
|
|
244
278
|
|
|
@@ -250,7 +284,6 @@ class TestHyperlendAdapter:
|
|
|
250
284
|
)
|
|
251
285
|
|
|
252
286
|
success, data = await adapter.get_assets_view(
|
|
253
|
-
chain_id=999,
|
|
254
287
|
user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
|
|
255
288
|
)
|
|
256
289
|
|
|
@@ -260,15 +293,36 @@ class TestHyperlendAdapter:
|
|
|
260
293
|
@pytest.mark.asyncio
|
|
261
294
|
async def test_get_assets_view_empty_response(self, adapter, mock_hyperlend_client):
|
|
262
295
|
"""Test assets view retrieval with empty response"""
|
|
263
|
-
mock_response = {
|
|
296
|
+
mock_response = {
|
|
297
|
+
"block_number": 12345,
|
|
298
|
+
"user": "0x0c737cB5934afCb5B01965141F865F795B324080",
|
|
299
|
+
"native_balance_wei": 0,
|
|
300
|
+
"native_balance": 0.0,
|
|
301
|
+
"assets": [],
|
|
302
|
+
"account_data": {
|
|
303
|
+
"total_collateral_base": 0,
|
|
304
|
+
"total_debt_base": 0,
|
|
305
|
+
"available_borrows_base": 0,
|
|
306
|
+
"current_liquidation_threshold": 0,
|
|
307
|
+
"ltv": 0,
|
|
308
|
+
"health_factor_wad": 0,
|
|
309
|
+
"health_factor": 0.0,
|
|
310
|
+
},
|
|
311
|
+
"base_currency_info": {
|
|
312
|
+
"marketReferenceCurrencyUnit": 100000000,
|
|
313
|
+
"marketReferenceCurrencyPriceInUsd": 100000000,
|
|
314
|
+
"networkBaseTokenPriceInUsd": 0,
|
|
315
|
+
"networkBaseTokenPriceDecimals": 8,
|
|
316
|
+
},
|
|
317
|
+
}
|
|
264
318
|
mock_hyperlend_client.get_assets_view = AsyncMock(return_value=mock_response)
|
|
265
319
|
|
|
266
320
|
success, data = await adapter.get_assets_view(
|
|
267
|
-
chain_id=999,
|
|
268
321
|
user_address="0x0c737cB5934afCb5B01965141F865F795B324080",
|
|
269
322
|
)
|
|
270
323
|
|
|
271
324
|
assert success is True
|
|
272
325
|
assert data == mock_response
|
|
273
326
|
assert len(data.get("assets", [])) == 0
|
|
274
|
-
|
|
327
|
+
# New API uses account_data; total_value may not be present
|
|
328
|
+
assert data.get("account_data", {}).get("total_collateral_base") == 0
|
|
@@ -10,6 +10,7 @@ the protocol (for example, by delegating signing to a hosted signer).
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import uuid
|
|
13
|
+
from collections.abc import Mapping
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
15
16
|
from loguru import logger
|
|
@@ -86,11 +87,49 @@ class LocalHyperliquidExecutor:
|
|
|
86
87
|
)
|
|
87
88
|
self.info = Info(base_url, skip_ws=True)
|
|
88
89
|
self.exchange = Exchange(self._wallet, base_url)
|
|
90
|
+
self._asset_id_to_coin: dict[int, str] | None = None
|
|
89
91
|
|
|
90
92
|
logger.info(
|
|
91
93
|
f"LocalHyperliquidExecutor initialized for address: {self._wallet.address}"
|
|
92
94
|
)
|
|
93
95
|
|
|
96
|
+
def _get_perp_coin(self, asset_id: int) -> str | None:
|
|
97
|
+
"""
|
|
98
|
+
Resolve a perp coin name from a perp asset_id.
|
|
99
|
+
|
|
100
|
+
Note: newer versions of the hyperliquid SDK expose `coin_to_asset` but not
|
|
101
|
+
`asset_to_coin`, so we build the reverse mapping when needed.
|
|
102
|
+
"""
|
|
103
|
+
if self._asset_id_to_coin is None:
|
|
104
|
+
mapping: dict[int, str] = {}
|
|
105
|
+
|
|
106
|
+
asset_to_coin = getattr(self.info, "asset_to_coin", None)
|
|
107
|
+
if isinstance(asset_to_coin, Mapping):
|
|
108
|
+
for k, v in asset_to_coin.items():
|
|
109
|
+
try:
|
|
110
|
+
asset_int = int(k)
|
|
111
|
+
except (TypeError, ValueError):
|
|
112
|
+
continue
|
|
113
|
+
if v:
|
|
114
|
+
mapping[asset_int] = str(v)
|
|
115
|
+
|
|
116
|
+
coin_to_asset = getattr(self.info, "coin_to_asset", None)
|
|
117
|
+
try:
|
|
118
|
+
coin_to_asset_dict = dict(coin_to_asset) if coin_to_asset else {}
|
|
119
|
+
except Exception: # noqa: BLE001
|
|
120
|
+
coin_to_asset_dict = {}
|
|
121
|
+
for coin, aid in coin_to_asset_dict.items():
|
|
122
|
+
try:
|
|
123
|
+
asset_int = int(aid)
|
|
124
|
+
except (TypeError, ValueError):
|
|
125
|
+
continue
|
|
126
|
+
if coin and asset_int not in mapping:
|
|
127
|
+
mapping[asset_int] = str(coin)
|
|
128
|
+
|
|
129
|
+
self._asset_id_to_coin = mapping
|
|
130
|
+
|
|
131
|
+
return self._asset_id_to_coin.get(asset_id) if self._asset_id_to_coin else None
|
|
132
|
+
|
|
94
133
|
def _resolve_private_key(self, config: dict[str, Any]) -> str | None:
|
|
95
134
|
"""Extract private key from config."""
|
|
96
135
|
# Try strategy_wallet first
|
|
@@ -172,7 +211,7 @@ class LocalHyperliquidExecutor:
|
|
|
172
211
|
)
|
|
173
212
|
else:
|
|
174
213
|
# Perp market order
|
|
175
|
-
coin = self.
|
|
214
|
+
coin = self._get_perp_coin(asset_id)
|
|
176
215
|
if not coin:
|
|
177
216
|
return {
|
|
178
217
|
"status": "err",
|
|
@@ -231,7 +270,7 @@ class LocalHyperliquidExecutor:
|
|
|
231
270
|
spot_index = asset_id - 10000
|
|
232
271
|
coin = f"@{spot_index}"
|
|
233
272
|
else:
|
|
234
|
-
coin = self.
|
|
273
|
+
coin = self._get_perp_coin(asset_id)
|
|
235
274
|
if not coin:
|
|
236
275
|
return {
|
|
237
276
|
"status": "err",
|
|
@@ -268,7 +307,7 @@ class LocalHyperliquidExecutor:
|
|
|
268
307
|
}
|
|
269
308
|
|
|
270
309
|
try:
|
|
271
|
-
coin = self.
|
|
310
|
+
coin = self._get_perp_coin(asset_id)
|
|
272
311
|
if not coin:
|
|
273
312
|
return {
|
|
274
313
|
"status": "err",
|
|
@@ -366,7 +405,7 @@ class LocalHyperliquidExecutor:
|
|
|
366
405
|
}
|
|
367
406
|
|
|
368
407
|
try:
|
|
369
|
-
coin = self.
|
|
408
|
+
coin = self._get_perp_coin(asset_id)
|
|
370
409
|
if not coin:
|
|
371
410
|
return {
|
|
372
411
|
"status": "err",
|
|
@@ -439,7 +478,7 @@ class LocalHyperliquidExecutor:
|
|
|
439
478
|
spot_index = asset_id - 10000
|
|
440
479
|
coin = f"@{spot_index}"
|
|
441
480
|
else:
|
|
442
|
-
coin = self.
|
|
481
|
+
coin = self._get_perp_coin(asset_id)
|
|
443
482
|
if not coin:
|
|
444
483
|
return {
|
|
445
484
|
"status": "err",
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Tests for LocalHyperliquidExecutor."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestLocalHyperliquidExecutor:
|
|
10
|
+
"""Tests for LocalHyperliquidExecutor."""
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def mock_info_without_asset_to_coin(self):
|
|
14
|
+
"""Mock Info that only exposes coin_to_asset (no asset_to_coin)."""
|
|
15
|
+
return SimpleNamespace(coin_to_asset={"HYPE": 7})
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_exchange(self):
|
|
19
|
+
"""Mock Exchange client."""
|
|
20
|
+
mock = MagicMock()
|
|
21
|
+
mock.update_leverage.return_value = {"status": "ok"}
|
|
22
|
+
mock.market_open.return_value = {"status": "ok"}
|
|
23
|
+
return mock
|
|
24
|
+
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_update_leverage_works_without_asset_to_coin(
|
|
27
|
+
self, mock_info_without_asset_to_coin, mock_exchange
|
|
28
|
+
):
|
|
29
|
+
"""update_leverage should map asset_id->coin via coin_to_asset inversion."""
|
|
30
|
+
dummy_wallet = SimpleNamespace(address="0xabc")
|
|
31
|
+
|
|
32
|
+
with patch(
|
|
33
|
+
"wayfinder_paths.adapters.hyperliquid_adapter.executor.Account"
|
|
34
|
+
) as mock_account:
|
|
35
|
+
mock_account.from_key.return_value = dummy_wallet
|
|
36
|
+
with patch(
|
|
37
|
+
"wayfinder_paths.adapters.hyperliquid_adapter.executor.Info",
|
|
38
|
+
return_value=mock_info_without_asset_to_coin,
|
|
39
|
+
):
|
|
40
|
+
with patch(
|
|
41
|
+
"wayfinder_paths.adapters.hyperliquid_adapter.executor.Exchange",
|
|
42
|
+
return_value=mock_exchange,
|
|
43
|
+
):
|
|
44
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.executor import (
|
|
45
|
+
LocalHyperliquidExecutor,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
executor = LocalHyperliquidExecutor(
|
|
49
|
+
config={"strategy_wallet": {"private_key": "0x" + "11" * 32}}
|
|
50
|
+
)
|
|
51
|
+
resp = await executor.update_leverage(
|
|
52
|
+
asset_id=7,
|
|
53
|
+
leverage=1,
|
|
54
|
+
is_cross=True,
|
|
55
|
+
address="0xabc",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert resp.get("status") == "ok"
|
|
59
|
+
mock_exchange.update_leverage.assert_called_once_with(
|
|
60
|
+
leverage=1,
|
|
61
|
+
name="HYPE",
|
|
62
|
+
is_cross=True,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_place_market_order_perp_works_without_asset_to_coin(
|
|
67
|
+
self, mock_info_without_asset_to_coin, mock_exchange
|
|
68
|
+
):
|
|
69
|
+
"""place_market_order should map asset_id->coin via coin_to_asset inversion."""
|
|
70
|
+
dummy_wallet = SimpleNamespace(address="0xabc")
|
|
71
|
+
|
|
72
|
+
with patch(
|
|
73
|
+
"wayfinder_paths.adapters.hyperliquid_adapter.executor.Account"
|
|
74
|
+
) as mock_account:
|
|
75
|
+
mock_account.from_key.return_value = dummy_wallet
|
|
76
|
+
with patch(
|
|
77
|
+
"wayfinder_paths.adapters.hyperliquid_adapter.executor.Info",
|
|
78
|
+
return_value=mock_info_without_asset_to_coin,
|
|
79
|
+
):
|
|
80
|
+
with patch(
|
|
81
|
+
"wayfinder_paths.adapters.hyperliquid_adapter.executor.Exchange",
|
|
82
|
+
return_value=mock_exchange,
|
|
83
|
+
):
|
|
84
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.executor import (
|
|
85
|
+
LocalHyperliquidExecutor,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
executor = LocalHyperliquidExecutor(
|
|
89
|
+
config={"strategy_wallet": {"private_key": "0x" + "11" * 32}}
|
|
90
|
+
)
|
|
91
|
+
resp = await executor.place_market_order(
|
|
92
|
+
asset_id=7,
|
|
93
|
+
is_buy=True,
|
|
94
|
+
slippage=0.01,
|
|
95
|
+
size=1.0,
|
|
96
|
+
address="0xabc",
|
|
97
|
+
reduce_only=False,
|
|
98
|
+
cloid=MagicMock(), # Avoid Cloid conversion
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
assert resp.get("status") == "ok"
|
|
102
|
+
mock_exchange.market_open.assert_called_once()
|
|
103
|
+
_, kwargs = mock_exchange.market_open.call_args
|
|
104
|
+
assert kwargs["name"] == "HYPE"
|
|
@@ -23,7 +23,6 @@ from wayfinder_paths.core.constants.moonwell_abi import (
|
|
|
23
23
|
WETH_ABI,
|
|
24
24
|
)
|
|
25
25
|
from wayfinder_paths.core.services.base import Web3Service
|
|
26
|
-
from wayfinder_paths.core.settings import settings
|
|
27
26
|
|
|
28
27
|
# Moonwell Base chain addresses
|
|
29
28
|
MOONWELL_DEFAULTS = {
|
|
@@ -1149,8 +1148,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1149
1148
|
|
|
1150
1149
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1151
1150
|
"""
|
|
1152
|
-
if getattr(settings, "DRY_RUN", False):
|
|
1153
|
-
return True, {"dry_run": True, "transaction": tx}
|
|
1154
1151
|
if not self.web3:
|
|
1155
1152
|
return False, "web3 service not configured"
|
|
1156
1153
|
|
|
@@ -35,8 +35,7 @@ adapter = PoolAdapter()
|
|
|
35
35
|
|
|
36
36
|
```python
|
|
37
37
|
success, data = await adapter.get_pools_by_ids(
|
|
38
|
-
pool_ids=["pool-123", "pool-456"]
|
|
39
|
-
merge_external=True
|
|
38
|
+
pool_ids=["pool-123", "pool-456"]
|
|
40
39
|
)
|
|
41
40
|
if success:
|
|
42
41
|
pools = data.get("pools", [])
|
|
@@ -45,44 +44,29 @@ else:
|
|
|
45
44
|
print(f"Error: {data}")
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
### Get
|
|
47
|
+
### Get pools (all or filtered)
|
|
48
|
+
|
|
49
|
+
Pass at least `chain_id` when fetching all pools (e.g. `chain_id=8453` for Base):
|
|
49
50
|
|
|
50
51
|
```python
|
|
51
|
-
success, data = await adapter.
|
|
52
|
+
success, data = await adapter.get_pools(chain_id=8453)
|
|
52
53
|
if success:
|
|
53
54
|
matches = data.get("matches", [])
|
|
54
|
-
print(f"Found {len(matches)} Llama matches")
|
|
55
55
|
for match in matches:
|
|
56
|
-
if match.get("
|
|
57
|
-
print(f"
|
|
58
|
-
else:
|
|
59
|
-
print(f"Error: {data}")
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
### Get Llama Reports
|
|
63
|
-
|
|
64
|
-
```python
|
|
65
|
-
success, data = await adapter.get_llama_reports(
|
|
66
|
-
identifiers=["pool-123", "usd-coin-base", "base_0x1234..."]
|
|
67
|
-
)
|
|
68
|
-
if success:
|
|
69
|
-
reports = data
|
|
70
|
-
for identifier, report in reports.items():
|
|
71
|
-
print(f"Report for {identifier}: APY {report.get('llama_apy_pct', 0)}%")
|
|
56
|
+
if match.get("stablecoin"):
|
|
57
|
+
print(f"Pool {match.get('id')} - APY: {match.get('apy')}%")
|
|
72
58
|
else:
|
|
73
59
|
print(f"Error: {data}")
|
|
74
60
|
```
|
|
75
61
|
|
|
76
|
-
|
|
62
|
+
Optional: `project="lido"` to filter by project.
|
|
77
63
|
|
|
78
64
|
## API Endpoints
|
|
79
65
|
|
|
80
|
-
The adapter uses the
|
|
66
|
+
The adapter uses the Wayfinder API:
|
|
81
67
|
|
|
82
|
-
- `GET /
|
|
83
|
-
- `
|
|
84
|
-
- `GET /api/v1/public/pools/llama/matches/` - Get Llama matches
|
|
85
|
-
- `GET /api/v1/public/pools/llama/reports/` - Get Llama reports
|
|
68
|
+
- `GET /v1/blockchain/pools/?chain_id=1&project=lido` - List pools (filter by chain_id, optional project)
|
|
69
|
+
- `POST /v1/blockchain/pools/` - Get pools by IDs (body: `{"pool_ids": ["id1", "id2"]}`)
|
|
86
70
|
|
|
87
71
|
## Error Handling
|
|
88
72
|
|