wayfinder-paths 0.1.28__py3-none-any.whl → 0.1.30__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 +445 -14
- wayfinder_paths/adapters/boros_adapter/client.py +7 -5
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +259 -19
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +4 -14
- wayfinder_paths/adapters/hyperliquid_adapter/exchange.py +2 -2
- wayfinder_paths/adapters/hyperliquid_adapter/local_signer.py +2 -33
- 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/hype_oft_abi.py +151 -0
- wayfinder_paths/core/constants/hyperliquid.py +20 -3
- wayfinder_paths/core/engine/manifest.py +1 -1
- wayfinder_paths/core/strategies/Strategy.py +1 -2
- 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 +11 -133
- 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 +2 -4
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +57 -201
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +24 -168
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +24 -63
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +2 -0
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2 -0
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -1
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/METADATA +1 -1
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/RECORD +35 -35
- wayfinder_paths/core/types.py +0 -19
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.28.dist-info → wayfinder_paths-0.1.30.dist-info}/WHEEL +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""Tests for BorosAdapter."""
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
7
|
+
from eth_abi import encode as abi_encode
|
|
6
8
|
|
|
7
9
|
from wayfinder_paths.adapters.boros_adapter.adapter import (
|
|
8
10
|
BorosAdapter,
|
|
@@ -44,24 +46,6 @@ class TestBorosAdapter:
|
|
|
44
46
|
"""Test adapter has correct type."""
|
|
45
47
|
assert adapter.adapter_type == "BOROS"
|
|
46
48
|
|
|
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
49
|
@pytest.mark.asyncio
|
|
66
50
|
async def test_list_markets_success(self, adapter, mock_boros_client):
|
|
67
51
|
"""Test successful market listing."""
|
|
@@ -358,6 +342,262 @@ class TestBorosAdapter:
|
|
|
358
342
|
assert success is True
|
|
359
343
|
assert result["status"] == "simulated"
|
|
360
344
|
|
|
345
|
+
@pytest.mark.asyncio
|
|
346
|
+
async def test_get_cash_fee_data_decodes_values(self, adapter):
|
|
347
|
+
"""Test MarketHub.getCashFeeData decoding (on-chain read is mocked)."""
|
|
348
|
+
scaling_factor_wei = 123
|
|
349
|
+
fee_rate_wei = 5_000_000_000_000_000 # 0.005e18
|
|
350
|
+
min_cash_cross_wei = 400_000_000_000_000_000 # 0.4e18
|
|
351
|
+
min_cash_isolated_wei = 1_000_000_000_000_000_000 # 1.0e18
|
|
352
|
+
raw = abi_encode(
|
|
353
|
+
["uint256", "uint256", "uint256", "uint256"],
|
|
354
|
+
[
|
|
355
|
+
scaling_factor_wei,
|
|
356
|
+
fee_rate_wei,
|
|
357
|
+
min_cash_cross_wei,
|
|
358
|
+
min_cash_isolated_wei,
|
|
359
|
+
],
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
mock_web3 = AsyncMock()
|
|
363
|
+
mock_web3.eth.call = AsyncMock(return_value=raw)
|
|
364
|
+
mock_cm = AsyncMock()
|
|
365
|
+
mock_cm.__aenter__.return_value = mock_web3
|
|
366
|
+
mock_cm.__aexit__.return_value = False
|
|
367
|
+
|
|
368
|
+
with patch(
|
|
369
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.web3_from_chain_id",
|
|
370
|
+
return_value=mock_cm,
|
|
371
|
+
):
|
|
372
|
+
ok, data = await adapter.get_cash_fee_data(token_id=5)
|
|
373
|
+
|
|
374
|
+
assert ok is True
|
|
375
|
+
assert data["token_id"] == 5
|
|
376
|
+
assert data["scaling_factor_wei"] == scaling_factor_wei
|
|
377
|
+
assert data["fee_rate_wei"] == fee_rate_wei
|
|
378
|
+
assert data["min_cash_cross_wei"] == min_cash_cross_wei
|
|
379
|
+
assert data["min_cash_isolated_wei"] == min_cash_isolated_wei
|
|
380
|
+
assert data["fee_rate"] == pytest.approx(fee_rate_wei / 1e18)
|
|
381
|
+
assert data["min_cash_cross"] == pytest.approx(0.4)
|
|
382
|
+
assert data["min_cash_isolated"] == pytest.approx(1.0)
|
|
383
|
+
|
|
384
|
+
@pytest.mark.asyncio
|
|
385
|
+
async def test_sweep_isolated_to_cross_filters_by_market(self, adapter):
|
|
386
|
+
"""Test isolated -> cross sweep only affects the specified market."""
|
|
387
|
+
adapter.simulation = False
|
|
388
|
+
adapter.get_account_balances = AsyncMock(
|
|
389
|
+
return_value=(
|
|
390
|
+
True,
|
|
391
|
+
{
|
|
392
|
+
"isolated_positions": [
|
|
393
|
+
{"market_id": 18, "balance_wei": 111},
|
|
394
|
+
{"market_id": 19, "balance_wei": 222},
|
|
395
|
+
]
|
|
396
|
+
},
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
adapter.cash_transfer = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
400
|
+
|
|
401
|
+
ok, res = await adapter.sweep_isolated_to_cross(token_id=3, market_id=19)
|
|
402
|
+
assert ok is True
|
|
403
|
+
assert res["status"] == "ok"
|
|
404
|
+
assert len(res["moved"]) == 1
|
|
405
|
+
assert res["moved"][0]["market_id"] == 19
|
|
406
|
+
assert res["moved"][0]["balance_wei"] == 222
|
|
407
|
+
|
|
408
|
+
adapter.cash_transfer.assert_awaited_once_with(
|
|
409
|
+
market_id=19, amount_wei=222, is_deposit=False
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
@pytest.mark.asyncio
|
|
413
|
+
async def test_sweep_isolated_to_cross_errors_on_failed_transfer(self, adapter):
|
|
414
|
+
"""Test sweep fails fast when an isolated->cross transfer fails."""
|
|
415
|
+
adapter.simulation = False
|
|
416
|
+
adapter.get_account_balances = AsyncMock(
|
|
417
|
+
return_value=(
|
|
418
|
+
True,
|
|
419
|
+
{
|
|
420
|
+
"isolated_positions": [
|
|
421
|
+
{"market_id": 18, "balance_wei": 111},
|
|
422
|
+
]
|
|
423
|
+
},
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
adapter.cash_transfer = AsyncMock(return_value=(False, {"error": "nope"}))
|
|
427
|
+
|
|
428
|
+
ok, res = await adapter.sweep_isolated_to_cross(token_id=3, market_id=18)
|
|
429
|
+
assert ok is False
|
|
430
|
+
assert "Failed sweep isolated->cross" in res["error"]
|
|
431
|
+
assert res["moved"][0]["market_id"] == 18
|
|
432
|
+
assert res["moved"][0]["ok"] is False
|
|
433
|
+
|
|
434
|
+
@pytest.mark.asyncio
|
|
435
|
+
async def test_deposit_to_cross_margin_sweeps_after_deposit(
|
|
436
|
+
self, adapter, mock_boros_client
|
|
437
|
+
):
|
|
438
|
+
"""Test deposit triggers isolated->cross sweep on success (non-simulation path is mocked)."""
|
|
439
|
+
adapter.simulation = False
|
|
440
|
+
adapter.sign_callback = object()
|
|
441
|
+
|
|
442
|
+
mock_boros_client.build_deposit_calldata = AsyncMock(
|
|
443
|
+
return_value={
|
|
444
|
+
"to": "0x0000000000000000000000000000000000000002",
|
|
445
|
+
"data": "0xdeadbeef",
|
|
446
|
+
"value": 0,
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
with (
|
|
451
|
+
patch(
|
|
452
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.build_approve_transaction",
|
|
453
|
+
new=AsyncMock(
|
|
454
|
+
return_value={"to": "0x0", "data": "0x0", "chainId": 42161}
|
|
455
|
+
),
|
|
456
|
+
),
|
|
457
|
+
patch(
|
|
458
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.send_transaction",
|
|
459
|
+
new=AsyncMock(return_value="0xapprove"),
|
|
460
|
+
),
|
|
461
|
+
patch.object(
|
|
462
|
+
adapter,
|
|
463
|
+
"_broadcast_calldata",
|
|
464
|
+
new=AsyncMock(return_value=(True, {"tx_hash": "0xdeposit"})),
|
|
465
|
+
),
|
|
466
|
+
patch.object(
|
|
467
|
+
adapter,
|
|
468
|
+
"sweep_isolated_to_cross",
|
|
469
|
+
new=AsyncMock(return_value=(True, {"status": "ok", "moved": []})),
|
|
470
|
+
) as mock_sweep,
|
|
471
|
+
):
|
|
472
|
+
ok, res = await adapter.deposit_to_cross_margin(
|
|
473
|
+
collateral_address="0x0000000000000000000000000000000000000001",
|
|
474
|
+
amount_wei=1_000_000, # 1 USDT
|
|
475
|
+
token_id=3,
|
|
476
|
+
market_id=18,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
assert ok is True
|
|
480
|
+
assert res["status"] == "ok"
|
|
481
|
+
assert res["approve"]["tx_hash"] == "0xapprove"
|
|
482
|
+
assert res["tx"]["tx_hash"] == "0xdeposit"
|
|
483
|
+
assert res["sweep"]["status"] == "ok"
|
|
484
|
+
|
|
485
|
+
mock_sweep.assert_awaited_once_with(token_id=3, market_id=18)
|
|
486
|
+
|
|
487
|
+
@pytest.mark.asyncio
|
|
488
|
+
async def test_close_positions_except_skips_keep_market(self, adapter):
|
|
489
|
+
adapter.simulation = False
|
|
490
|
+
adapter.close_positions_market = AsyncMock(
|
|
491
|
+
return_value=(True, {"status": "ok"})
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
ok, res = await adapter.close_positions_except(
|
|
495
|
+
keep_market_id=19, token_id=3, market_ids=[18, 19, 20]
|
|
496
|
+
)
|
|
497
|
+
assert ok is True
|
|
498
|
+
assert res["status"] == "ok"
|
|
499
|
+
|
|
500
|
+
calls = adapter.close_positions_market.await_args_list
|
|
501
|
+
assert len(calls) == 2
|
|
502
|
+
assert calls[0].args[0] == 18
|
|
503
|
+
assert calls[1].args[0] == 20
|
|
504
|
+
|
|
505
|
+
@pytest.mark.asyncio
|
|
506
|
+
async def test_ensure_position_size_yu_increases_short(self, adapter):
|
|
507
|
+
adapter.simulation = False
|
|
508
|
+
adapter.get_active_positions = AsyncMock(return_value=(True, []))
|
|
509
|
+
adapter.place_rate_order = AsyncMock(return_value=(True, {"tx_hash": "0xopen"}))
|
|
510
|
+
|
|
511
|
+
ok, res = await adapter.ensure_position_size_yu(
|
|
512
|
+
market_id=18, token_id=3, target_size_yu=1.5
|
|
513
|
+
)
|
|
514
|
+
assert ok is True
|
|
515
|
+
assert res["action"] == "increase_short"
|
|
516
|
+
adapter.place_rate_order.assert_awaited_once()
|
|
517
|
+
|
|
518
|
+
_, kwargs = adapter.place_rate_order.await_args
|
|
519
|
+
assert kwargs["market_id"] == 18
|
|
520
|
+
assert kwargs["token_id"] == 3
|
|
521
|
+
assert kwargs["side"] == "short"
|
|
522
|
+
assert kwargs["tif"] == "IOC"
|
|
523
|
+
assert kwargs["size_yu_wei"] == int(1.5 * 1e18)
|
|
524
|
+
|
|
525
|
+
@pytest.mark.asyncio
|
|
526
|
+
async def test_ensure_position_size_yu_decreases(self, adapter):
|
|
527
|
+
adapter.simulation = False
|
|
528
|
+
adapter.get_active_positions = AsyncMock(
|
|
529
|
+
return_value=(True, [{"size": 2.0, "sizeWei": int(2e18)}])
|
|
530
|
+
)
|
|
531
|
+
adapter.close_positions_market = AsyncMock(
|
|
532
|
+
return_value=(True, {"tx_hash": "0xclose"})
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
ok, res = await adapter.ensure_position_size_yu(
|
|
536
|
+
market_id=18, token_id=3, target_size_yu=1.0
|
|
537
|
+
)
|
|
538
|
+
assert ok is True
|
|
539
|
+
assert res["action"] == "decrease"
|
|
540
|
+
adapter.close_positions_market.assert_awaited_once_with(
|
|
541
|
+
market_id=18, token_id=3, size_yu_wei=int(1.0 * 1e18)
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
@pytest.mark.asyncio
|
|
545
|
+
async def test_bridge_hype_oft_rounds_amount_and_builds_tx(self, adapter):
|
|
546
|
+
adapter.simulation = False
|
|
547
|
+
adapter.sign_callback = object()
|
|
548
|
+
|
|
549
|
+
mock_contract = SimpleNamespace()
|
|
550
|
+
mock_dec_fn = SimpleNamespace(call=AsyncMock(return_value=10))
|
|
551
|
+
mock_quote_fn = SimpleNamespace(call=AsyncMock(return_value=(5, 0)))
|
|
552
|
+
mock_functions = SimpleNamespace(
|
|
553
|
+
decimalConversionRate=MagicMock(return_value=mock_dec_fn),
|
|
554
|
+
quoteSend=MagicMock(return_value=mock_quote_fn),
|
|
555
|
+
)
|
|
556
|
+
mock_contract.functions = mock_functions
|
|
557
|
+
|
|
558
|
+
mock_web3 = SimpleNamespace(
|
|
559
|
+
eth=SimpleNamespace(contract=MagicMock(return_value=mock_contract)),
|
|
560
|
+
to_checksum_address=lambda x: x,
|
|
561
|
+
)
|
|
562
|
+
mock_cm = AsyncMock()
|
|
563
|
+
mock_cm.__aenter__.return_value = mock_web3
|
|
564
|
+
mock_cm.__aexit__.return_value = False
|
|
565
|
+
|
|
566
|
+
with (
|
|
567
|
+
patch(
|
|
568
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.web3_from_chain_id",
|
|
569
|
+
return_value=mock_cm,
|
|
570
|
+
),
|
|
571
|
+
patch(
|
|
572
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.encode_call",
|
|
573
|
+
new=AsyncMock(return_value={"chainId": 999}),
|
|
574
|
+
) as mock_encode,
|
|
575
|
+
patch(
|
|
576
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.send_transaction",
|
|
577
|
+
new=AsyncMock(return_value="0xtx"),
|
|
578
|
+
),
|
|
579
|
+
):
|
|
580
|
+
ok, res = await adapter.bridge_hype_oft_hyperevm_to_arbitrum(
|
|
581
|
+
amount_wei=123,
|
|
582
|
+
max_value_wei=1000,
|
|
583
|
+
to_address=adapter.user_address,
|
|
584
|
+
from_address=adapter.user_address,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
assert ok is True
|
|
588
|
+
assert res["status"] == "ok"
|
|
589
|
+
assert res["tx_hash"] == "0xtx"
|
|
590
|
+
assert res["amount_wei"] == 120 # rounded down by conversion rate 10
|
|
591
|
+
assert res["native_fee_wei"] == 5
|
|
592
|
+
assert res["total_value_wei"] == 125
|
|
593
|
+
|
|
594
|
+
_, kwargs = mock_encode.await_args
|
|
595
|
+
assert kwargs["fn_name"] == "send"
|
|
596
|
+
assert kwargs["value"] == 125
|
|
597
|
+
send_params = kwargs["args"][0]
|
|
598
|
+
assert send_params[0] == 30110
|
|
599
|
+
assert send_params[2] == 120
|
|
600
|
+
|
|
361
601
|
@pytest.mark.asyncio
|
|
362
602
|
async def test_withdraw_collateral_simulation(self, adapter):
|
|
363
603
|
"""Test withdraw in simulation mode."""
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import time
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
5
6
|
from decimal import ROUND_DOWN, Decimal, getcontext
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ from loguru import logger
|
|
|
11
12
|
|
|
12
13
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
13
14
|
from wayfinder_paths.core.constants import ZERO_ADDRESS
|
|
15
|
+
from wayfinder_paths.core.constants.contracts import HYPERCORE_SENTINEL_ADDRESS
|
|
14
16
|
from wayfinder_paths.core.constants.hyperliquid import (
|
|
15
17
|
ARBITRUM_USDC_ADDRESS as _ARBITRUM_USDC_ADDRESS,
|
|
16
18
|
)
|
|
@@ -21,7 +23,6 @@ from wayfinder_paths.core.constants.hyperliquid import (
|
|
|
21
23
|
from wayfinder_paths.core.constants.hyperliquid import (
|
|
22
24
|
HYPERLIQUID_BRIDGE_ADDRESS as _HYPERLIQUID_BRIDGE_ADDRESS,
|
|
23
25
|
)
|
|
24
|
-
from wayfinder_paths.core.types import HyperliquidSignCallback
|
|
25
26
|
|
|
26
27
|
# Re-export Bridge2 constants for backwards compatibility.
|
|
27
28
|
HYPERLIQUID_BRIDGE_ADDRESS = _HYPERLIQUID_BRIDGE_ADDRESS
|
|
@@ -67,7 +68,7 @@ class HyperliquidAdapter(BaseAdapter):
|
|
|
67
68
|
config: dict[str, Any] | None = None,
|
|
68
69
|
*,
|
|
69
70
|
simulation: bool = False,
|
|
70
|
-
sign_callback:
|
|
71
|
+
sign_callback: Callable[[dict], Awaitable[str]] | None = None,
|
|
71
72
|
) -> None:
|
|
72
73
|
super().__init__("hyperliquid_adapter", config)
|
|
73
74
|
|
|
@@ -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,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
1
2
|
from decimal import Decimal
|
|
2
3
|
from typing import Any, Literal
|
|
3
4
|
|
|
@@ -24,7 +25,6 @@ from loguru import logger
|
|
|
24
25
|
from web3 import Web3
|
|
25
26
|
|
|
26
27
|
from wayfinder_paths.adapters.hyperliquid_adapter.util import Util
|
|
27
|
-
from wayfinder_paths.core.types import HyperliquidSignCallback
|
|
28
28
|
|
|
29
29
|
ARBITRUM_CHAIN_ID = "0xa4b1"
|
|
30
30
|
MAINNET = "Mainnet"
|
|
@@ -39,7 +39,7 @@ class Exchange:
|
|
|
39
39
|
self,
|
|
40
40
|
info: Info,
|
|
41
41
|
util: Util,
|
|
42
|
-
sign_callback:
|
|
42
|
+
sign_callback: Callable[[dict], Awaitable[str]],
|
|
43
43
|
signing_type: Literal["eip712", "local"],
|
|
44
44
|
):
|
|
45
45
|
self.info = info
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
from collections.abc import Awaitable, Callable
|
|
1
2
|
from typing import Any
|
|
2
3
|
|
|
3
4
|
from eth_account import Account
|
|
4
5
|
|
|
5
|
-
from wayfinder_paths.core.types import HyperliquidSignCallback
|
|
6
|
-
|
|
7
6
|
|
|
8
7
|
def _resolve_private_key(config: dict[str, Any]) -> str | None:
|
|
9
8
|
"""Extract private key from config."""
|
|
@@ -26,23 +25,7 @@ def _resolve_private_key(config: dict[str, Any]) -> str | None:
|
|
|
26
25
|
return None
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
def create_local_signer(config: dict[str, Any]) ->
|
|
30
|
-
"""
|
|
31
|
-
Create a Hyperliquid signing callback using private key from config.
|
|
32
|
-
|
|
33
|
-
For local signing, the payload is a keccak hash (0x...) of the encoded EIP-712 typed data.
|
|
34
|
-
The callback signs with the private key and returns a signature dict.
|
|
35
|
-
|
|
36
|
-
Args:
|
|
37
|
-
config: Configuration dict containing private key in strategy_wallet or main_wallet
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
HyperliquidSignCallback that signs payloads with the local private key
|
|
41
|
-
|
|
42
|
-
Raises:
|
|
43
|
-
ValueError: If no private key found in config
|
|
44
|
-
"""
|
|
45
|
-
# Extract private key from config
|
|
28
|
+
def create_local_signer(config: dict[str, Any]) -> Callable[[dict], Awaitable[str]]:
|
|
46
29
|
private_key = _resolve_private_key(config)
|
|
47
30
|
if not private_key:
|
|
48
31
|
raise ValueError(
|
|
@@ -57,20 +40,6 @@ def create_local_signer(config: dict[str, Any]) -> HyperliquidSignCallback:
|
|
|
57
40
|
async def sign(
|
|
58
41
|
action: dict[str, Any], payload: str, address: str
|
|
59
42
|
) -> dict[str, str] | None:
|
|
60
|
-
"""
|
|
61
|
-
Sign a Hyperliquid action with local private key.
|
|
62
|
-
|
|
63
|
-
For local signing, payload is keccak hash (0x...) of encoded typed data.
|
|
64
|
-
Sign with account and return signature dict.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
action: The action being signed (not used for local signing)
|
|
68
|
-
payload: Keccak hash (0x...) of encoded EIP-712 typed data
|
|
69
|
-
address: The address signing (validation check)
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
Signature dict {"r": "0x...", "s": "0x...", "v": 28} or None if validation fails
|
|
73
|
-
"""
|
|
74
43
|
# Verify address matches account
|
|
75
44
|
if address.lower() != account.address.lower():
|
|
76
45
|
return None
|
|
@@ -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),
|