wayfinder-paths 0.1.28__py3-none-any.whl → 0.1.29__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/boros_adapter/adapter.py +142 -12
- wayfinder_paths/adapters/boros_adapter/client.py +7 -5
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +147 -18
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +2 -12
- wayfinder_paths/adapters/multicall_adapter/adapter.py +2 -4
- wayfinder_paths/core/clients/TokenClient.py +1 -1
- wayfinder_paths/core/constants/__init__.py +23 -1
- wayfinder_paths/core/constants/contracts.py +22 -0
- wayfinder_paths/core/constants/hyperliquid.py +20 -3
- wayfinder_paths/core/engine/manifest.py +1 -1
- wayfinder_paths/mcp/scripting.py +2 -2
- wayfinder_paths/mcp/tools/discovery.py +3 -72
- wayfinder_paths/mcp/tools/execute.py +8 -4
- wayfinder_paths/mcp/tools/hyperliquid.py +1 -1
- wayfinder_paths/mcp/tools/quotes.py +7 -8
- wayfinder_paths/mcp/tools/wallets.py +4 -7
- wayfinder_paths/mcp/utils.py +0 -22
- wayfinder_paths/policies/lifi.py +5 -2
- wayfinder_paths/policies/moonwell.py +3 -1
- wayfinder_paths/policies/util.py +4 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +1 -2
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +23 -16
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +24 -63
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +2 -1
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.29.dist-info}/METADATA +1 -1
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.29.dist-info}/RECORD +28 -28
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.29.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.29.dist-info}/WHEEL +0 -0
|
@@ -5,12 +5,11 @@ import time
|
|
|
5
5
|
from collections.abc import Callable
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from eth_abi import encode
|
|
8
|
+
from eth_abi import decode, encode
|
|
9
9
|
from eth_utils import function_signature_to_4byte_selector, to_checksum_address
|
|
10
10
|
from loguru import logger
|
|
11
11
|
|
|
12
12
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
13
|
-
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
14
13
|
from wayfinder_paths.core.constants.contracts import BOROS_MARKET_HUB, BOROS_ROUTER
|
|
15
14
|
from wayfinder_paths.core.utils.tokens import build_approve_transaction
|
|
16
15
|
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
@@ -115,6 +114,121 @@ class BorosAdapter(BaseAdapter):
|
|
|
115
114
|
}
|
|
116
115
|
]
|
|
117
116
|
|
|
117
|
+
async def get_cash_fee_data(self, *, token_id: int) -> tuple[bool, dict[str, Any]]:
|
|
118
|
+
"""Read MarketHub.getCashFeeData(tokenId) from chain.
|
|
119
|
+
|
|
120
|
+
This is useful for guarding Boros actions that require a minimum amount
|
|
121
|
+
of cross cash (e.g., MMInsufficientMinCash()).
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
selector = function_signature_to_4byte_selector("getCashFeeData(uint16)")
|
|
125
|
+
params = encode(["uint16"], [int(token_id)])
|
|
126
|
+
data = "0x" + selector.hex() + params.hex()
|
|
127
|
+
|
|
128
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
129
|
+
raw: bytes = await web3.eth.call(
|
|
130
|
+
{
|
|
131
|
+
"to": to_checksum_address(BOROS_MARKET_HUB),
|
|
132
|
+
"data": data,
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if len(raw) % 32 != 0:
|
|
137
|
+
return False, {"error": f"Unexpected getCashFeeData() size: {len(raw)}"}
|
|
138
|
+
|
|
139
|
+
n_words = len(raw) // 32
|
|
140
|
+
values = decode(["uint256"] * n_words, raw)
|
|
141
|
+
if len(values) < 4:
|
|
142
|
+
return False, {
|
|
143
|
+
"error": f"Unexpected getCashFeeData() words: {len(values)}"
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Empirically (2026-02-01 on Arbitrum MarketHub), the return is 4 uint256s.
|
|
147
|
+
# We expose all 4, and provide float conversions for the commonly used ones.
|
|
148
|
+
scaling_factor_wei = int(values[0])
|
|
149
|
+
fee_rate_wei = int(values[1])
|
|
150
|
+
min_cash_cross_wei = int(values[2])
|
|
151
|
+
min_cash_isolated_wei = int(values[3])
|
|
152
|
+
|
|
153
|
+
return True, {
|
|
154
|
+
"token_id": int(token_id),
|
|
155
|
+
"scaling_factor_wei": scaling_factor_wei,
|
|
156
|
+
"fee_rate_wei": fee_rate_wei,
|
|
157
|
+
"min_cash_cross_wei": min_cash_cross_wei,
|
|
158
|
+
"min_cash_isolated_wei": min_cash_isolated_wei,
|
|
159
|
+
"fee_rate": fee_rate_wei / 1e18,
|
|
160
|
+
"min_cash_cross": min_cash_cross_wei / 1e18,
|
|
161
|
+
"min_cash_isolated": min_cash_isolated_wei / 1e18,
|
|
162
|
+
}
|
|
163
|
+
except Exception as exc: # noqa: BLE001
|
|
164
|
+
return False, {"error": str(exc)}
|
|
165
|
+
|
|
166
|
+
async def sweep_isolated_to_cross(
|
|
167
|
+
self,
|
|
168
|
+
*,
|
|
169
|
+
token_id: int,
|
|
170
|
+
market_id: int | None = None,
|
|
171
|
+
token_decimals: int = 18,
|
|
172
|
+
) -> tuple[bool, dict[str, Any]]:
|
|
173
|
+
"""Sweep isolated cash -> cross cash for a given token (optionally per-market).
|
|
174
|
+
|
|
175
|
+
Boros deposits can sometimes show up as isolated cash for the target market;
|
|
176
|
+
this helper moves that isolated cash back to cross margin using cash_transfer.
|
|
177
|
+
|
|
178
|
+
Notes:
|
|
179
|
+
- cash_transfer uses 1e18 internal cash units, not token native decimals.
|
|
180
|
+
- This does NOT touch isolated positions for other markets unless market_id is None.
|
|
181
|
+
"""
|
|
182
|
+
if self.simulation:
|
|
183
|
+
return True, {"status": "simulated"}
|
|
184
|
+
|
|
185
|
+
ok_state, state = await self.get_full_user_state(
|
|
186
|
+
token_id=int(token_id),
|
|
187
|
+
token_decimals=int(token_decimals),
|
|
188
|
+
include_open_orders=False,
|
|
189
|
+
include_withdrawal_status=False,
|
|
190
|
+
)
|
|
191
|
+
if not ok_state or not isinstance(state, dict):
|
|
192
|
+
return False, {"error": f"Failed to read Boros state: {state}"}
|
|
193
|
+
|
|
194
|
+
balances = state.get("balances") or {}
|
|
195
|
+
isolated_positions = balances.get("isolated_positions") or []
|
|
196
|
+
if not isinstance(isolated_positions, list):
|
|
197
|
+
isolated_positions = []
|
|
198
|
+
|
|
199
|
+
moved: list[dict[str, Any]] = []
|
|
200
|
+
for iso in isolated_positions:
|
|
201
|
+
try:
|
|
202
|
+
iso_market_id = int(iso.get("market_id"))
|
|
203
|
+
if market_id is not None and iso_market_id != int(market_id):
|
|
204
|
+
continue
|
|
205
|
+
balance_wei = int(iso.get("balance_wei") or 0)
|
|
206
|
+
if balance_wei <= 0:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
tx_ok, tx_res = await self.cash_transfer(
|
|
210
|
+
market_id=iso_market_id,
|
|
211
|
+
amount_wei=balance_wei,
|
|
212
|
+
is_deposit=False, # isolated -> cross
|
|
213
|
+
)
|
|
214
|
+
moved.append(
|
|
215
|
+
{
|
|
216
|
+
"market_id": iso_market_id,
|
|
217
|
+
"balance_wei": balance_wei,
|
|
218
|
+
"ok": tx_ok,
|
|
219
|
+
"tx": tx_res,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
if not tx_ok:
|
|
223
|
+
return False, {
|
|
224
|
+
"error": f"Failed sweep isolated->cross for market {iso_market_id}",
|
|
225
|
+
"moved": moved,
|
|
226
|
+
}
|
|
227
|
+
except Exception as exc: # noqa: BLE001
|
|
228
|
+
return False, {"error": f"Failed sweep isolated->cross: {exc}"}
|
|
229
|
+
|
|
230
|
+
return True, {"status": "ok", "moved": moved}
|
|
231
|
+
|
|
118
232
|
@staticmethod
|
|
119
233
|
def _unwrap_tx_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
120
234
|
"""Best-effort unwrap of API payloads that may nest the tx dict."""
|
|
@@ -195,7 +309,6 @@ class BorosAdapter(BaseAdapter):
|
|
|
195
309
|
self,
|
|
196
310
|
calldata: dict[str, Any],
|
|
197
311
|
*,
|
|
198
|
-
timeout: int = DEFAULT_TRANSACTION_TIMEOUT,
|
|
199
312
|
max_retries: int = 2,
|
|
200
313
|
) -> tuple[bool, dict[str, Any]]:
|
|
201
314
|
"""Broadcast calldata from Boros API with retry logic.
|
|
@@ -292,14 +405,6 @@ class BorosAdapter(BaseAdapter):
|
|
|
292
405
|
"attempts": max_retries + 1,
|
|
293
406
|
}
|
|
294
407
|
|
|
295
|
-
async def connect(self) -> bool:
|
|
296
|
-
try:
|
|
297
|
-
markets = await self.boros_client.list_markets(limit=1)
|
|
298
|
-
return len(markets) > 0
|
|
299
|
-
except Exception as exc:
|
|
300
|
-
logger.error(f"BorosAdapter connection failed: {exc}")
|
|
301
|
-
return False
|
|
302
|
-
|
|
303
408
|
# ------------------------------------------------------------------ #
|
|
304
409
|
# Tick Math Utilities #
|
|
305
410
|
# ------------------------------------------------------------------ #
|
|
@@ -1015,6 +1120,14 @@ class BorosAdapter(BaseAdapter):
|
|
|
1015
1120
|
token_id: int,
|
|
1016
1121
|
market_id: int,
|
|
1017
1122
|
) -> tuple[bool, dict[str, Any]]:
|
|
1123
|
+
"""Deposit collateral into Boros cross margin.
|
|
1124
|
+
|
|
1125
|
+
IMPORTANT: amount_wei is in the collateral token's native decimals.
|
|
1126
|
+
Example: USDT has 6 decimals, so 1 USDT = 1_000_000.
|
|
1127
|
+
|
|
1128
|
+
After deposit, Boros may credit the cash as isolated for market_id. This
|
|
1129
|
+
helper sweeps isolated -> cross for that market to match the method name.
|
|
1130
|
+
"""
|
|
1018
1131
|
if self.simulation:
|
|
1019
1132
|
logger.info(
|
|
1020
1133
|
f"[SIMULATION] deposit_to_cross_margin: {amount_wei} wei, "
|
|
@@ -1074,7 +1187,24 @@ class BorosAdapter(BaseAdapter):
|
|
|
1074
1187
|
"tx": tx_res,
|
|
1075
1188
|
}
|
|
1076
1189
|
|
|
1077
|
-
|
|
1190
|
+
sweep_ok, sweep_res = await self.sweep_isolated_to_cross(
|
|
1191
|
+
token_id=int(token_id),
|
|
1192
|
+
market_id=int(market_id),
|
|
1193
|
+
)
|
|
1194
|
+
if not sweep_ok:
|
|
1195
|
+
return False, {
|
|
1196
|
+
"error": f"Deposit succeeded but isolated->cross sweep failed: {sweep_res}",
|
|
1197
|
+
"approve": approve_res,
|
|
1198
|
+
"tx": tx_res,
|
|
1199
|
+
"sweep": sweep_res,
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return True, {
|
|
1203
|
+
"status": "ok",
|
|
1204
|
+
"approve": approve_res,
|
|
1205
|
+
"tx": tx_res,
|
|
1206
|
+
"sweep": sweep_res,
|
|
1207
|
+
}
|
|
1078
1208
|
except Exception as e:
|
|
1079
1209
|
logger.error(f"Failed to deposit to cross margin: {e}")
|
|
1080
1210
|
return False, {"error": str(e)}
|
|
@@ -250,10 +250,11 @@ class BorosClient:
|
|
|
250
250
|
|
|
251
251
|
Args:
|
|
252
252
|
token_id: Boros token ID (e.g., 3 for USDT).
|
|
253
|
-
amount_wei: Amount in
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
amount_wei: Amount in NATIVE token decimals (despite the param name).
|
|
254
|
+
Example: USDT has 6 decimals, so 1 USDT = 1_000_000.
|
|
255
|
+
market_id: Target market ID.
|
|
256
|
+
user_address: User wallet address.
|
|
257
|
+
account_id: Boros account ID (0 = cross margin).
|
|
257
258
|
|
|
258
259
|
Returns:
|
|
259
260
|
Calldata dictionary with 'to', 'data', 'value' fields.
|
|
@@ -283,7 +284,8 @@ class BorosClient:
|
|
|
283
284
|
|
|
284
285
|
Args:
|
|
285
286
|
token_id: Boros token ID.
|
|
286
|
-
amount_wei: Amount in
|
|
287
|
+
amount_wei: Amount in NATIVE token decimals (despite the param name).
|
|
288
|
+
Example: USDT has 6 decimals, so 1 USDT = 1_000_000.
|
|
287
289
|
user_address: User wallet address.
|
|
288
290
|
account_id: Account ID.
|
|
289
291
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from unittest.mock import AsyncMock, patch
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
|
+
from eth_abi import encode as abi_encode
|
|
6
7
|
|
|
7
8
|
from wayfinder_paths.adapters.boros_adapter.adapter import (
|
|
8
9
|
BorosAdapter,
|
|
@@ -44,24 +45,6 @@ class TestBorosAdapter:
|
|
|
44
45
|
"""Test adapter has correct type."""
|
|
45
46
|
assert adapter.adapter_type == "BOROS"
|
|
46
47
|
|
|
47
|
-
@pytest.mark.asyncio
|
|
48
|
-
async def test_connect_success(self, adapter, mock_boros_client):
|
|
49
|
-
"""Test successful connection."""
|
|
50
|
-
mock_boros_client.list_markets = AsyncMock(
|
|
51
|
-
return_value=[{"marketId": 1, "symbol": "HYPE-USD"}]
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
result = await adapter.connect()
|
|
55
|
-
assert result is True
|
|
56
|
-
|
|
57
|
-
@pytest.mark.asyncio
|
|
58
|
-
async def test_connect_failure(self, adapter, mock_boros_client):
|
|
59
|
-
"""Test connection failure."""
|
|
60
|
-
mock_boros_client.list_markets = AsyncMock(side_effect=Exception("API Error"))
|
|
61
|
-
|
|
62
|
-
result = await adapter.connect()
|
|
63
|
-
assert result is False
|
|
64
|
-
|
|
65
48
|
@pytest.mark.asyncio
|
|
66
49
|
async def test_list_markets_success(self, adapter, mock_boros_client):
|
|
67
50
|
"""Test successful market listing."""
|
|
@@ -358,6 +341,152 @@ class TestBorosAdapter:
|
|
|
358
341
|
assert success is True
|
|
359
342
|
assert result["status"] == "simulated"
|
|
360
343
|
|
|
344
|
+
@pytest.mark.asyncio
|
|
345
|
+
async def test_get_cash_fee_data_decodes_values(self, adapter):
|
|
346
|
+
"""Test MarketHub.getCashFeeData decoding (on-chain read is mocked)."""
|
|
347
|
+
scaling_factor_wei = 123
|
|
348
|
+
fee_rate_wei = 5_000_000_000_000_000 # 0.005e18
|
|
349
|
+
min_cash_cross_wei = 400_000_000_000_000_000 # 0.4e18
|
|
350
|
+
min_cash_isolated_wei = 1_000_000_000_000_000_000 # 1.0e18
|
|
351
|
+
raw = abi_encode(
|
|
352
|
+
["uint256", "uint256", "uint256", "uint256"],
|
|
353
|
+
[
|
|
354
|
+
scaling_factor_wei,
|
|
355
|
+
fee_rate_wei,
|
|
356
|
+
min_cash_cross_wei,
|
|
357
|
+
min_cash_isolated_wei,
|
|
358
|
+
],
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
mock_web3 = AsyncMock()
|
|
362
|
+
mock_web3.eth.call = AsyncMock(return_value=raw)
|
|
363
|
+
mock_cm = AsyncMock()
|
|
364
|
+
mock_cm.__aenter__.return_value = mock_web3
|
|
365
|
+
mock_cm.__aexit__.return_value = False
|
|
366
|
+
|
|
367
|
+
with patch(
|
|
368
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.web3_from_chain_id",
|
|
369
|
+
return_value=mock_cm,
|
|
370
|
+
):
|
|
371
|
+
ok, data = await adapter.get_cash_fee_data(token_id=5)
|
|
372
|
+
|
|
373
|
+
assert ok is True
|
|
374
|
+
assert data["token_id"] == 5
|
|
375
|
+
assert data["scaling_factor_wei"] == scaling_factor_wei
|
|
376
|
+
assert data["fee_rate_wei"] == fee_rate_wei
|
|
377
|
+
assert data["min_cash_cross_wei"] == min_cash_cross_wei
|
|
378
|
+
assert data["min_cash_isolated_wei"] == min_cash_isolated_wei
|
|
379
|
+
assert data["fee_rate"] == pytest.approx(fee_rate_wei / 1e18)
|
|
380
|
+
assert data["min_cash_cross"] == pytest.approx(0.4)
|
|
381
|
+
assert data["min_cash_isolated"] == pytest.approx(1.0)
|
|
382
|
+
|
|
383
|
+
@pytest.mark.asyncio
|
|
384
|
+
async def test_sweep_isolated_to_cross_filters_by_market(self, adapter):
|
|
385
|
+
"""Test isolated -> cross sweep only affects the specified market."""
|
|
386
|
+
adapter.simulation = False
|
|
387
|
+
adapter.get_full_user_state = AsyncMock(
|
|
388
|
+
return_value=(
|
|
389
|
+
True,
|
|
390
|
+
{
|
|
391
|
+
"balances": {
|
|
392
|
+
"isolated_positions": [
|
|
393
|
+
{"market_id": 18, "balance_wei": 111},
|
|
394
|
+
{"market_id": 19, "balance_wei": 222},
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
adapter.cash_transfer = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
401
|
+
|
|
402
|
+
ok, res = await adapter.sweep_isolated_to_cross(token_id=3, market_id=19)
|
|
403
|
+
assert ok is True
|
|
404
|
+
assert res["status"] == "ok"
|
|
405
|
+
assert len(res["moved"]) == 1
|
|
406
|
+
assert res["moved"][0]["market_id"] == 19
|
|
407
|
+
assert res["moved"][0]["balance_wei"] == 222
|
|
408
|
+
|
|
409
|
+
adapter.cash_transfer.assert_awaited_once_with(
|
|
410
|
+
market_id=19, amount_wei=222, is_deposit=False
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
@pytest.mark.asyncio
|
|
414
|
+
async def test_sweep_isolated_to_cross_errors_on_failed_transfer(self, adapter):
|
|
415
|
+
"""Test sweep fails fast when an isolated->cross transfer fails."""
|
|
416
|
+
adapter.simulation = False
|
|
417
|
+
adapter.get_full_user_state = AsyncMock(
|
|
418
|
+
return_value=(
|
|
419
|
+
True,
|
|
420
|
+
{
|
|
421
|
+
"balances": {
|
|
422
|
+
"isolated_positions": [
|
|
423
|
+
{"market_id": 18, "balance_wei": 111},
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
adapter.cash_transfer = AsyncMock(return_value=(False, {"error": "nope"}))
|
|
430
|
+
|
|
431
|
+
ok, res = await adapter.sweep_isolated_to_cross(token_id=3, market_id=18)
|
|
432
|
+
assert ok is False
|
|
433
|
+
assert "Failed sweep isolated->cross" in res["error"]
|
|
434
|
+
assert res["moved"][0]["market_id"] == 18
|
|
435
|
+
assert res["moved"][0]["ok"] is False
|
|
436
|
+
|
|
437
|
+
@pytest.mark.asyncio
|
|
438
|
+
async def test_deposit_to_cross_margin_sweeps_after_deposit(
|
|
439
|
+
self, adapter, mock_boros_client
|
|
440
|
+
):
|
|
441
|
+
"""Test deposit triggers isolated->cross sweep on success (non-simulation path is mocked)."""
|
|
442
|
+
adapter.simulation = False
|
|
443
|
+
adapter.sign_callback = object()
|
|
444
|
+
|
|
445
|
+
mock_boros_client.build_deposit_calldata = AsyncMock(
|
|
446
|
+
return_value={
|
|
447
|
+
"to": "0x0000000000000000000000000000000000000002",
|
|
448
|
+
"data": "0xdeadbeef",
|
|
449
|
+
"value": 0,
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
with (
|
|
454
|
+
patch(
|
|
455
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.build_approve_transaction",
|
|
456
|
+
new=AsyncMock(
|
|
457
|
+
return_value={"to": "0x0", "data": "0x0", "chainId": 42161}
|
|
458
|
+
),
|
|
459
|
+
),
|
|
460
|
+
patch(
|
|
461
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.send_transaction",
|
|
462
|
+
new=AsyncMock(return_value="0xapprove"),
|
|
463
|
+
),
|
|
464
|
+
patch.object(
|
|
465
|
+
adapter,
|
|
466
|
+
"_broadcast_calldata",
|
|
467
|
+
new=AsyncMock(return_value=(True, {"tx_hash": "0xdeposit"})),
|
|
468
|
+
),
|
|
469
|
+
patch.object(
|
|
470
|
+
adapter,
|
|
471
|
+
"sweep_isolated_to_cross",
|
|
472
|
+
new=AsyncMock(return_value=(True, {"status": "ok", "moved": []})),
|
|
473
|
+
) as mock_sweep,
|
|
474
|
+
):
|
|
475
|
+
ok, res = await adapter.deposit_to_cross_margin(
|
|
476
|
+
collateral_address="0x0000000000000000000000000000000000000001",
|
|
477
|
+
amount_wei=1_000_000, # 1 USDT
|
|
478
|
+
token_id=3,
|
|
479
|
+
market_id=18,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
assert ok is True
|
|
483
|
+
assert res["status"] == "ok"
|
|
484
|
+
assert res["approve"]["tx_hash"] == "0xapprove"
|
|
485
|
+
assert res["tx"]["tx_hash"] == "0xdeposit"
|
|
486
|
+
assert res["sweep"]["status"] == "ok"
|
|
487
|
+
|
|
488
|
+
mock_sweep.assert_awaited_once_with(token_id=3, market_id=18)
|
|
489
|
+
|
|
361
490
|
@pytest.mark.asyncio
|
|
362
491
|
async def test_withdraw_collateral_simulation(self, adapter):
|
|
363
492
|
"""Test withdraw in simulation mode."""
|
|
@@ -11,6 +11,7 @@ from loguru import logger
|
|
|
11
11
|
|
|
12
12
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
13
13
|
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
14
|
+
from wayfinder_paths.core.constants.contracts import HYPERCORE_SENTINEL_ADDRESS
|
|
14
15
|
from wayfinder_paths.core.constants.hyperliquid import (
|
|
15
16
|
ARBITRUM_USDC_ADDRESS as _ARBITRUM_USDC_ADDRESS,
|
|
16
17
|
)
|
|
@@ -113,17 +114,6 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
113
114
|
self._asset_to_sz_decimals: dict[int, int] | None = None
|
|
114
115
|
self._coin_to_asset: dict[str, int] | None = None
|
|
115
116
|
|
|
116
|
-
async def connect(self) -> bool:
|
|
117
|
-
try:
|
|
118
|
-
meta = self.info.meta_and_asset_ctxs()
|
|
119
|
-
if meta:
|
|
120
|
-
self.logger.debug("HyperliquidAdapter connected successfully")
|
|
121
|
-
return True
|
|
122
|
-
return False
|
|
123
|
-
except Exception as exc:
|
|
124
|
-
self.logger.error(f"HyperliquidAdapter connection failed: {exc}")
|
|
125
|
-
return False
|
|
126
|
-
|
|
127
117
|
# ------------------------------------------------------------------ #
|
|
128
118
|
# Market Data - Read Operations #
|
|
129
119
|
# ------------------------------------------------------------------ #
|
|
@@ -714,7 +704,7 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
714
704
|
@staticmethod
|
|
715
705
|
def hypercore_index_to_system_address(index: int) -> str:
|
|
716
706
|
if index == 150:
|
|
717
|
-
return
|
|
707
|
+
return HYPERCORE_SENTINEL_ADDRESS
|
|
718
708
|
|
|
719
709
|
hex_index = f"{index:x}"
|
|
720
710
|
padding_length = 42 - len("0x20") - len(hex_index)
|
|
@@ -7,9 +7,9 @@ from typing import Any
|
|
|
7
7
|
from hexbytes import HexBytes
|
|
8
8
|
|
|
9
9
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
10
|
+
from wayfinder_paths.core.constants.contracts import MULTICALL3_ADDRESS
|
|
10
11
|
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
11
12
|
|
|
12
|
-
MULTICALL3_DEFAULT_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11"
|
|
13
13
|
MULTICALL3_ABI = [
|
|
14
14
|
{
|
|
15
15
|
"inputs": [
|
|
@@ -79,9 +79,7 @@ class MulticallAdapter(BaseAdapter):
|
|
|
79
79
|
self.chain_id = int(chain_id) if chain_id is not None else None
|
|
80
80
|
self.web3 = web3
|
|
81
81
|
|
|
82
|
-
checksum_address = self.web3.to_checksum_address(
|
|
83
|
-
address or MULTICALL3_DEFAULT_ADDRESS
|
|
84
|
-
)
|
|
82
|
+
checksum_address = self.web3.to_checksum_address(address or MULTICALL3_ADDRESS)
|
|
85
83
|
self.contract = self.web3.eth.contract(
|
|
86
84
|
address=checksum_address, abi=abi or MULTICALL3_ABI
|
|
87
85
|
)
|
|
@@ -90,7 +90,7 @@ class FuzzyTokenResult(TypedDict):
|
|
|
90
90
|
class TokenClient(WayfinderClient):
|
|
91
91
|
def __init__(self):
|
|
92
92
|
super().__init__()
|
|
93
|
-
self.api_base_url = f"{get_api_base_url()}/
|
|
93
|
+
self.api_base_url = f"{get_api_base_url()}/blockchain/tokens"
|
|
94
94
|
|
|
95
95
|
async def get_token_details(
|
|
96
96
|
self, query: str, market_data: bool = False, chain_id: int | None = None
|
|
@@ -33,6 +33,18 @@ from wayfinder_paths.core.constants.chains import (
|
|
|
33
33
|
SUPPORTED_CHAINS,
|
|
34
34
|
)
|
|
35
35
|
from wayfinder_paths.core.constants.contracts import (
|
|
36
|
+
ENSO_ROUTER,
|
|
37
|
+
HYPE_FEE_WALLET,
|
|
38
|
+
HYPE_OFT_ADDRESS,
|
|
39
|
+
HYPERCORE_SENTINEL_ADDRESS,
|
|
40
|
+
HYPEREVM_WHYPE,
|
|
41
|
+
KHYPE_ADDRESS,
|
|
42
|
+
KHYPE_STAKING_ACCOUNTANT,
|
|
43
|
+
LHYPE_ACCOUNTANT,
|
|
44
|
+
LIFI_GENERIC,
|
|
45
|
+
LIFI_ROUTER_HYPEREVM,
|
|
46
|
+
LOOPED_HYPE_ADDRESS,
|
|
47
|
+
MULTICALL3_ADDRESS,
|
|
36
48
|
NATIVE_TOKEN_SENTINEL,
|
|
37
49
|
ZERO_ADDRESS,
|
|
38
50
|
)
|
|
@@ -42,7 +54,6 @@ from .hyperliquid import (
|
|
|
42
54
|
ARBITRUM_USDC_TOKEN_ID,
|
|
43
55
|
DEFAULT_HYPERLIQUID_BUILDER_FEE,
|
|
44
56
|
DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP,
|
|
45
|
-
HYPE_FEE_WALLET,
|
|
46
57
|
HYPERLIQUID_BRIDGE_ADDRESS,
|
|
47
58
|
)
|
|
48
59
|
|
|
@@ -85,4 +96,15 @@ __all__ = [
|
|
|
85
96
|
"HYPE_FEE_WALLET",
|
|
86
97
|
"DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP",
|
|
87
98
|
"DEFAULT_HYPERLIQUID_BUILDER_FEE",
|
|
99
|
+
"ENSO_ROUTER",
|
|
100
|
+
"HYPE_OFT_ADDRESS",
|
|
101
|
+
"HYPERCORE_SENTINEL_ADDRESS",
|
|
102
|
+
"HYPEREVM_WHYPE",
|
|
103
|
+
"KHYPE_ADDRESS",
|
|
104
|
+
"KHYPE_STAKING_ACCOUNTANT",
|
|
105
|
+
"LHYPE_ACCOUNTANT",
|
|
106
|
+
"LIFI_GENERIC",
|
|
107
|
+
"LIFI_ROUTER_HYPEREVM",
|
|
108
|
+
"LOOPED_HYPE_ADDRESS",
|
|
109
|
+
"MULTICALL3_ADDRESS",
|
|
88
110
|
]
|
|
@@ -45,6 +45,28 @@ USDT_ETHEREUM = to_checksum_address("0xdAC17F958D2ee523a2206206994597C13D831ec7"
|
|
|
45
45
|
USDT_POLYGON = to_checksum_address("0xc2132D05D31c914a87C6611C10748AEb04B58e8F")
|
|
46
46
|
USDT_BSC = to_checksum_address("0x55d398326f99059fF775485246999027B3197955")
|
|
47
47
|
|
|
48
|
+
HYPE_FEE_WALLET = to_checksum_address("0xaA1D89f333857eD78F8434CC4f896A9293EFE65c")
|
|
49
|
+
|
|
50
|
+
# HyperEVM token addresses
|
|
51
|
+
KHYPE_ADDRESS = to_checksum_address("0xfD739d4e423301CE9385c1fb8850539D657C296D")
|
|
52
|
+
LOOPED_HYPE_ADDRESS = to_checksum_address("0x5748ae796AE46A4F1348a1693de4b50560485562")
|
|
53
|
+
|
|
54
|
+
# Accountant contracts for exchange rate calculations
|
|
55
|
+
KHYPE_STAKING_ACCOUNTANT = to_checksum_address(
|
|
56
|
+
"0x9209648Ec9D448EF57116B73A2f081835643dc7A"
|
|
57
|
+
)
|
|
58
|
+
LHYPE_ACCOUNTANT = to_checksum_address("0xcE621a3CA6F72706678cFF0572ae8d15e5F001c3")
|
|
59
|
+
|
|
60
|
+
# LayerZero OFT bridge (HyperEVM native HYPE -> Arbitrum OFT HYPE)
|
|
61
|
+
HYPE_OFT_ADDRESS = to_checksum_address("0x007C26Ed5C33Fe6fEF62223d4c363A01F1b1dDc1")
|
|
62
|
+
|
|
63
|
+
# Multicall3 (deployed at same address on all EVM chains)
|
|
64
|
+
MULTICALL3_ADDRESS = to_checksum_address("0xcA11bde05977b3631167028862bE2a173976CA11")
|
|
65
|
+
|
|
66
|
+
# LI.FI routers
|
|
67
|
+
LIFI_ROUTER_HYPEREVM = to_checksum_address("0x0a0758d937d1059c356D4714e57F5df0239bce1A")
|
|
68
|
+
LIFI_GENERIC = to_checksum_address("0x31a9b1835864706Af10103b31Ea2b79bdb995F5F")
|
|
69
|
+
|
|
48
70
|
TOKENS_REQUIRING_APPROVAL_RESET: set[tuple[int, str]] = {
|
|
49
71
|
(1, USDT_ETHEREUM),
|
|
50
72
|
(137, USDT_POLYGON),
|
|
@@ -2,10 +2,27 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
from wayfinder_paths.core.constants.contracts import (
|
|
6
|
+
ARBITRUM_USDC as ARBITRUM_USDC_ADDRESS,
|
|
7
|
+
)
|
|
8
|
+
from wayfinder_paths.core.constants.contracts import (
|
|
9
|
+
HYPE_FEE_WALLET,
|
|
10
|
+
)
|
|
11
|
+
from wayfinder_paths.core.constants.contracts import (
|
|
12
|
+
HYPERLIQUID_BRIDGE as HYPERLIQUID_BRIDGE_ADDRESS,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Re-export addresses for backwards compatibility
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ARBITRUM_USDC_ADDRESS",
|
|
18
|
+
"ARBITRUM_USDC_TOKEN_ID",
|
|
19
|
+
"HYPE_FEE_WALLET",
|
|
20
|
+
"HYPERLIQUID_BRIDGE_ADDRESS",
|
|
21
|
+
"DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP",
|
|
22
|
+
"DEFAULT_HYPERLIQUID_BUILDER_FEE",
|
|
23
|
+
]
|
|
24
|
+
|
|
7
25
|
ARBITRUM_USDC_TOKEN_ID: str = "usd-coin-arbitrum"
|
|
8
|
-
HYPE_FEE_WALLET: str = "0xaA1D89f333857eD78F8434CC4f896A9293EFE65c"
|
|
9
26
|
|
|
10
27
|
# Tenths of a basis point: 30 -> 0.030% (3 bps)
|
|
11
28
|
DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP: int = 30
|
|
@@ -19,7 +19,7 @@ class StrategyManifest(BaseModel):
|
|
|
19
19
|
)
|
|
20
20
|
name: str | None = Field(
|
|
21
21
|
default=None,
|
|
22
|
-
description="Unique name identifier for this strategy instance. Used to look up dedicated wallet in
|
|
22
|
+
description="Unique name identifier for this strategy instance. Used to look up dedicated wallet in config.json by label.",
|
|
23
23
|
)
|
|
24
24
|
permissions: dict[str, Any] = Field(default_factory=dict)
|
|
25
25
|
adapters: list[AdapterRequirement] = Field(default_factory=list)
|
wayfinder_paths/mcp/scripting.py
CHANGED
|
@@ -57,8 +57,8 @@ def get_adapter[T](
|
|
|
57
57
|
wallet = find_wallet_by_label(wallet_label)
|
|
58
58
|
if not wallet:
|
|
59
59
|
raise ValueError(
|
|
60
|
-
f"Wallet '{wallet_label}' not found in
|
|
61
|
-
"Run 'just create-wallets'
|
|
60
|
+
f"Wallet '{wallet_label}' not found in config.json. "
|
|
61
|
+
"Run 'just create-wallets'."
|
|
62
62
|
)
|
|
63
63
|
|
|
64
64
|
private_key = wallet.get("private_key") or wallet.get("private_key_hex")
|