wayfinder-paths 0.1.27__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/hyperliquid_adapter/exchange.py +8 -40
- wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +2 -2
- 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.27.dist-info → wayfinder_paths-0.1.29.dist-info}/METADATA +3 -2
- {wayfinder_paths-0.1.27.dist-info → wayfinder_paths-0.1.29.dist-info}/RECORD +30 -30
- {wayfinder_paths-0.1.27.dist-info → wayfinder_paths-0.1.29.dist-info}/WHEEL +1 -1
- {wayfinder_paths-0.1.27.dist-info → wayfinder_paths-0.1.29.dist-info}/LICENSE +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)
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import json
|
|
2
1
|
from decimal import Decimal
|
|
3
2
|
from typing import Any, Literal
|
|
4
3
|
|
|
5
4
|
from eth_account.messages import encode_typed_data
|
|
6
|
-
from eth_utils import keccak
|
|
7
5
|
from hyperliquid.api import API
|
|
8
6
|
from hyperliquid.exchange import get_timestamp_ms
|
|
9
7
|
from hyperliquid.info import Info
|
|
@@ -204,7 +202,7 @@ class Exchange:
|
|
|
204
202
|
"time": nonce,
|
|
205
203
|
"type": "withdraw3",
|
|
206
204
|
}
|
|
207
|
-
payload =
|
|
205
|
+
payload = user_signed_payload(
|
|
208
206
|
"HyperliquidTransaction:Withdraw", WITHDRAW_SIGN_TYPES, action
|
|
209
207
|
)
|
|
210
208
|
if not (sig := await self.sign(payload, action, address)):
|
|
@@ -230,7 +228,7 @@ class Exchange:
|
|
|
230
228
|
"amount": amount,
|
|
231
229
|
"time": nonce,
|
|
232
230
|
}
|
|
233
|
-
payload =
|
|
231
|
+
payload = user_signed_payload(
|
|
234
232
|
"HyperliquidTransaction:SpotSend", SPOT_TRANSFER_SIGN_TYPES, action
|
|
235
233
|
)
|
|
236
234
|
if not (sig := await self.sign(payload, action, address)):
|
|
@@ -248,7 +246,7 @@ class Exchange:
|
|
|
248
246
|
"nonce": nonce,
|
|
249
247
|
"type": "usdClassTransfer",
|
|
250
248
|
}
|
|
251
|
-
payload =
|
|
249
|
+
payload = user_signed_payload(
|
|
252
250
|
"HyperliquidTransaction:UsdClassTransfer",
|
|
253
251
|
USD_CLASS_TRANSFER_SIGN_TYPES,
|
|
254
252
|
action,
|
|
@@ -268,7 +266,7 @@ class Exchange:
|
|
|
268
266
|
"nonce": nonce,
|
|
269
267
|
"type": "userDexAbstraction",
|
|
270
268
|
}
|
|
271
|
-
payload =
|
|
269
|
+
payload = user_signed_payload(
|
|
272
270
|
"HyperliquidTransaction:UserDexAbstraction",
|
|
273
271
|
USER_DEX_ABSTRACTION_SIGN_TYPES,
|
|
274
272
|
action,
|
|
@@ -288,56 +286,26 @@ class Exchange:
|
|
|
288
286
|
"nonce": nonce,
|
|
289
287
|
"type": "approveBuilderFee",
|
|
290
288
|
}
|
|
291
|
-
payload =
|
|
289
|
+
payload = user_signed_payload(
|
|
292
290
|
"HyperliquidTransaction:ApproveBuilderFee", BUILDER_FEE_SIGN_TYPES, action
|
|
293
291
|
)
|
|
294
292
|
if not (sig := await self.sign(payload, action, address)):
|
|
295
293
|
return USER_DECLINED_ERROR
|
|
296
294
|
return self._broadcast_hypecore(action, nonce, sig)
|
|
297
295
|
|
|
298
|
-
def build_sign(self, raw_payload: dict) -> str:
|
|
299
|
-
"""Build and format the signing payload based on signing_type."""
|
|
300
|
-
if self.signing_type == "eip712":
|
|
301
|
-
# Remote signing: Return JSON string of typed data
|
|
302
|
-
return json.dumps(
|
|
303
|
-
raw_payload,
|
|
304
|
-
default=lambda o: (
|
|
305
|
-
"0x" + o.hex() if isinstance(o, (bytes, bytearray)) else o
|
|
306
|
-
),
|
|
307
|
-
separators=(",", ":"),
|
|
308
|
-
)
|
|
309
|
-
# Local signing: Return keccak hash
|
|
310
|
-
encoded = encode_typed_data(full_message=raw_payload)
|
|
311
|
-
full_msg = b"\x19" + encoded.version + encoded.header + encoded.body
|
|
312
|
-
return f"0x{keccak(full_msg).hex()}"
|
|
313
|
-
|
|
314
296
|
async def sign(
|
|
315
297
|
self, payload: str, action: dict, address: str
|
|
316
298
|
) -> dict[str, Any] | None:
|
|
317
299
|
"""Sign the payload and return Hyperliquid-format signature."""
|
|
318
300
|
if self.signing_type == "eip712":
|
|
319
|
-
|
|
320
|
-
typed_data = json.loads(payload)
|
|
321
|
-
sig_hex = await self.sign_callback(typed_data)
|
|
301
|
+
sig_hex = await self.sign_callback(payload)
|
|
322
302
|
if not sig_hex:
|
|
323
303
|
return None
|
|
324
304
|
return self.util._sig_hex_to_hl_signature(sig_hex)
|
|
325
305
|
|
|
326
|
-
|
|
306
|
+
payload = encode_typed_data(full_message=payload)
|
|
327
307
|
return await self.sign_callback(action, payload, address)
|
|
328
308
|
|
|
329
|
-
def _get_hypecore_l1_payload(
|
|
330
|
-
self, action, nonce, expires_after=None, is_mainnet=True
|
|
331
|
-
):
|
|
332
|
-
"""Build L1 action payload (orders, leverage, cancels)."""
|
|
333
|
-
payload = get_l1_action_payload(action, None, nonce, expires_after, is_mainnet)
|
|
334
|
-
return self.build_sign(payload)
|
|
335
|
-
|
|
336
|
-
def _get_hypecore_user_signature_payload(self, primary_type, payload_types, action):
|
|
337
|
-
"""Build user signed action payload (withdrawals, transfers, etc.)."""
|
|
338
|
-
payload = user_signed_payload(primary_type, payload_types, action)
|
|
339
|
-
return self.build_sign(payload)
|
|
340
|
-
|
|
341
309
|
def _broadcast_hypecore(self, action, nonce, signature):
|
|
342
310
|
"""Broadcast a signed action to the Hyperliquid exchange."""
|
|
343
311
|
payload = {
|
|
@@ -351,7 +319,7 @@ class Exchange:
|
|
|
351
319
|
async def sign_and_broadcast_hypecore(self, action, address):
|
|
352
320
|
"""Sign and broadcast an L1 action (orders, leverage, cancels)."""
|
|
353
321
|
nonce = get_timestamp_ms()
|
|
354
|
-
payload =
|
|
322
|
+
payload = payload = get_l1_action_payload(action, None, nonce, None, True)
|
|
355
323
|
if not (sig := await self.sign(payload, action, address)):
|
|
356
324
|
return USER_DECLINED_ERROR
|
|
357
325
|
return self._broadcast_hypecore(action, nonce, sig)
|
|
@@ -52,7 +52,7 @@ def create_local_signer(config: dict[str, Any]) -> HyperliquidSignCallback:
|
|
|
52
52
|
|
|
53
53
|
# Create account
|
|
54
54
|
pk = private_key if private_key.startswith("0x") else "0x" + private_key
|
|
55
|
-
account = Account.from_key(pk)
|
|
55
|
+
account: Account = Account.from_key(pk)
|
|
56
56
|
|
|
57
57
|
async def sign(
|
|
58
58
|
action: dict[str, Any], payload: str, address: str
|
|
@@ -76,7 +76,7 @@ def create_local_signer(config: dict[str, Any]) -> HyperliquidSignCallback:
|
|
|
76
76
|
return None
|
|
77
77
|
|
|
78
78
|
# Sign the hash
|
|
79
|
-
signed = account.
|
|
79
|
+
signed = account.sign_message(payload)
|
|
80
80
|
return {"r": hex(signed.r), "s": hex(signed.s), "v": signed.v}
|
|
81
81
|
|
|
82
82
|
return sign
|
|
@@ -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),
|