wayfinder-paths 0.1.22__py3-none-any.whl → 0.1.24__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/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -6,6 +6,10 @@ import pytest
|
|
|
6
6
|
|
|
7
7
|
from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
8
8
|
from wayfinder_paths.core.clients.LedgerClient import LedgerClient
|
|
9
|
+
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
10
|
+
BasisPosition,
|
|
11
|
+
BasisTradingStrategy,
|
|
12
|
+
)
|
|
9
13
|
from wayfinder_paths.tests.test_utils import load_strategy_examples
|
|
10
14
|
|
|
11
15
|
|
|
@@ -122,6 +126,9 @@ class TestBasisTradingStrategy:
|
|
|
122
126
|
mock.get_spot_user_state = AsyncMock(return_value=(True, {"balances": []}))
|
|
123
127
|
mock.get_max_builder_fee = AsyncMock(return_value=(True, 0))
|
|
124
128
|
mock.approve_builder_fee = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
129
|
+
mock.ensure_builder_fee_approved = AsyncMock(
|
|
130
|
+
return_value=(True, "Builder fee approved: 0.030%")
|
|
131
|
+
)
|
|
125
132
|
mock.update_leverage = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
126
133
|
mock.transfer_perp_to_spot = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
127
134
|
mock.transfer_spot_to_perp = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
@@ -161,10 +168,6 @@ class TestBasisTradingStrategy:
|
|
|
161
168
|
"wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
|
|
162
169
|
return_value=ledger_adapter,
|
|
163
170
|
):
|
|
164
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
165
|
-
BasisTradingStrategy,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
171
|
s = BasisTradingStrategy(
|
|
169
172
|
config={
|
|
170
173
|
"main_wallet": {"address": "0x1234"},
|
|
@@ -306,15 +309,11 @@ class TestBasisTradingStrategy:
|
|
|
306
309
|
assert snapshot["wallet_address"] == "0x5678"
|
|
307
310
|
assert snapshot["portfolio_value"] == status["portfolio_value"]
|
|
308
311
|
|
|
309
|
-
def test_maintenance_rate(self):
|
|
310
|
-
|
|
311
|
-
BasisTradingStrategy,
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
rate = BasisTradingStrategy.maintenance_rate_from_max_leverage(50)
|
|
312
|
+
def test_maintenance_rate(self, strategy):
|
|
313
|
+
rate = strategy.maintenance_rate_from_max_leverage(50)
|
|
315
314
|
assert rate == 0.01
|
|
316
315
|
|
|
317
|
-
rate =
|
|
316
|
+
rate = strategy.maintenance_rate_from_max_leverage(10)
|
|
318
317
|
assert rate == 0.05
|
|
319
318
|
|
|
320
319
|
def test_rolling_min_sum(self, strategy):
|
|
@@ -403,10 +402,6 @@ class TestBasisTradingStrategy:
|
|
|
403
402
|
async def test_scale_up_position_below_minimum(
|
|
404
403
|
self, strategy, mock_hyperliquid_adapter
|
|
405
404
|
):
|
|
406
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
407
|
-
BasisPosition,
|
|
408
|
-
)
|
|
409
|
-
|
|
410
405
|
strategy.current_position = BasisPosition(
|
|
411
406
|
coin="ETH",
|
|
412
407
|
spot_asset_id=10000,
|
|
@@ -429,10 +424,6 @@ class TestBasisTradingStrategy:
|
|
|
429
424
|
async def test_update_with_idle_capital_scales_up(
|
|
430
425
|
self, strategy, mock_hyperliquid_adapter
|
|
431
426
|
):
|
|
432
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
433
|
-
BasisPosition,
|
|
434
|
-
)
|
|
435
|
-
|
|
436
427
|
strategy.deposit_amount = 100.0
|
|
437
428
|
strategy.current_position = BasisPosition(
|
|
438
429
|
coin="ETH",
|
|
@@ -543,10 +534,6 @@ class TestBasisTradingStrategy:
|
|
|
543
534
|
"wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
|
|
544
535
|
return_value=ledger_adapter,
|
|
545
536
|
):
|
|
546
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
547
|
-
BasisTradingStrategy,
|
|
548
|
-
)
|
|
549
|
-
|
|
550
537
|
s = BasisTradingStrategy(
|
|
551
538
|
config={
|
|
552
539
|
"main_wallet": {"address": "0x1234"},
|
|
@@ -556,22 +543,20 @@ class TestBasisTradingStrategy:
|
|
|
556
543
|
s.hyperliquid_adapter = mock_hyperliquid_adapter
|
|
557
544
|
s.ledger_adapter = ledger_adapter
|
|
558
545
|
|
|
559
|
-
# Mock
|
|
560
|
-
mock_hyperliquid_adapter.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
546
|
+
# Mock ensure_builder_fee_approved returning already approved
|
|
547
|
+
mock_hyperliquid_adapter.ensure_builder_fee_approved = (
|
|
548
|
+
AsyncMock(
|
|
549
|
+
return_value=(
|
|
550
|
+
True,
|
|
551
|
+
"Builder fee already approved (30 >= 30)",
|
|
552
|
+
)
|
|
564
553
|
)
|
|
565
554
|
)
|
|
566
|
-
mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
|
|
567
|
-
return_value=(True, {"status": "ok"})
|
|
568
|
-
)
|
|
569
555
|
|
|
570
556
|
success, msg = await s.ensure_builder_fee_approved()
|
|
571
557
|
assert success
|
|
572
558
|
assert "already approved" in msg.lower()
|
|
573
|
-
|
|
574
|
-
mock_hyperliquid_adapter.approve_builder_fee.assert_not_called()
|
|
559
|
+
mock_hyperliquid_adapter.ensure_builder_fee_approved.assert_called_once()
|
|
575
560
|
|
|
576
561
|
@pytest.mark.asyncio
|
|
577
562
|
async def test_ensure_builder_fee_approved_needs_approval(
|
|
@@ -591,10 +576,6 @@ class TestBasisTradingStrategy:
|
|
|
591
576
|
"wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
|
|
592
577
|
return_value=ledger_adapter,
|
|
593
578
|
):
|
|
594
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
595
|
-
BasisTradingStrategy,
|
|
596
|
-
)
|
|
597
|
-
|
|
598
579
|
s = BasisTradingStrategy(
|
|
599
580
|
config={
|
|
600
581
|
"main_wallet": {"address": "0x1234"},
|
|
@@ -604,19 +585,17 @@ class TestBasisTradingStrategy:
|
|
|
604
585
|
s.hyperliquid_adapter = mock_hyperliquid_adapter
|
|
605
586
|
s.ledger_adapter = ledger_adapter
|
|
606
587
|
|
|
607
|
-
# Mock
|
|
608
|
-
mock_hyperliquid_adapter.
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
return_value=(True, {"status": "ok"})
|
|
588
|
+
# Mock ensure_builder_fee_approved returning newly approved
|
|
589
|
+
mock_hyperliquid_adapter.ensure_builder_fee_approved = (
|
|
590
|
+
AsyncMock(
|
|
591
|
+
return_value=(True, "Builder fee approved: 0.030%")
|
|
592
|
+
)
|
|
613
593
|
)
|
|
614
594
|
|
|
615
595
|
success, msg = await s.ensure_builder_fee_approved()
|
|
616
596
|
assert success
|
|
617
597
|
assert "approved" in msg.lower()
|
|
618
|
-
|
|
619
|
-
mock_hyperliquid_adapter.approve_builder_fee.assert_called_once()
|
|
598
|
+
mock_hyperliquid_adapter.ensure_builder_fee_approved.assert_called_once()
|
|
620
599
|
|
|
621
600
|
@pytest.mark.asyncio
|
|
622
601
|
async def test_portfolio_value_includes_spot_holdings(
|
|
@@ -792,10 +771,6 @@ class TestBasisTradingStrategy:
|
|
|
792
771
|
async def test_update_near_liquidation_closes_and_redeploys(
|
|
793
772
|
self, strategy, mock_hyperliquid_adapter
|
|
794
773
|
):
|
|
795
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
796
|
-
BasisPosition,
|
|
797
|
-
)
|
|
798
|
-
|
|
799
774
|
strategy.deposit_amount = 100.0
|
|
800
775
|
strategy.current_position = BasisPosition(
|
|
801
776
|
coin="HYPE",
|
|
@@ -885,10 +860,6 @@ class TestBasisTradingStrategy:
|
|
|
885
860
|
"wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
|
|
886
861
|
return_value=ledger_adapter,
|
|
887
862
|
):
|
|
888
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
889
|
-
BasisTradingStrategy,
|
|
890
|
-
)
|
|
891
|
-
|
|
892
863
|
s = BasisTradingStrategy(
|
|
893
864
|
config={
|
|
894
865
|
"main_wallet": {"address": "0x1234"},
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Boros venue operations for BorosHypeStrategy.
|
|
3
|
+
|
|
4
|
+
Kept as a mixin so the main strategy file stays readable without changing behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from web3 import AsyncWeb3
|
|
15
|
+
|
|
16
|
+
from wayfinder_paths.core.utils.transaction import encode_call, send_transaction
|
|
17
|
+
from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
18
|
+
|
|
19
|
+
from .constants import (
|
|
20
|
+
BOROS_HYPE_MARKET_ID,
|
|
21
|
+
BOROS_HYPE_TOKEN_ID,
|
|
22
|
+
BOROS_MIN_DEPOSIT_USD,
|
|
23
|
+
HYPE_OFT_ABI,
|
|
24
|
+
HYPE_OFT_ADDRESS,
|
|
25
|
+
HYPEREVM_CHAIN_ID,
|
|
26
|
+
LZ_EID_ARBITRUM,
|
|
27
|
+
MIN_HYPE_GAS,
|
|
28
|
+
USDC_ARB,
|
|
29
|
+
USDT_ARB,
|
|
30
|
+
)
|
|
31
|
+
from .types import Inventory
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _pad_address_bytes32(address: str) -> bytes:
|
|
35
|
+
checksum = AsyncWeb3.to_checksum_address(address)
|
|
36
|
+
return bytes.fromhex(checksum[2:]).rjust(32, b"\x00")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BorosHypeBorosOpsMixin:
|
|
40
|
+
async def _fund_boros(
|
|
41
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
42
|
+
) -> tuple[bool, str]:
|
|
43
|
+
"""Fund Boros using native HYPE collateral.
|
|
44
|
+
|
|
45
|
+
Flow:
|
|
46
|
+
- If we already have Arbitrum OFT HYPE, deposit it to Boros cross margin.
|
|
47
|
+
- Otherwise, bridge native HYPE from HyperEVM -> Arbitrum via LayerZero OFT.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
amount_usd = float(params.get("amount_usd") or 0.0)
|
|
51
|
+
market_id = int(
|
|
52
|
+
params.get("market_id")
|
|
53
|
+
or self._planner_runtime.current_boros_market_id
|
|
54
|
+
or BOROS_HYPE_MARKET_ID
|
|
55
|
+
)
|
|
56
|
+
token_id = int(
|
|
57
|
+
params.get("token_id")
|
|
58
|
+
or self._planner_runtime.current_boros_token_id
|
|
59
|
+
or BOROS_HYPE_TOKEN_ID
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
collateral_address = str(
|
|
63
|
+
params.get("collateral_address")
|
|
64
|
+
or self._planner_runtime.current_boros_collateral_address
|
|
65
|
+
or ""
|
|
66
|
+
).strip()
|
|
67
|
+
if not collateral_address:
|
|
68
|
+
collateral_address = HYPE_OFT_ADDRESS
|
|
69
|
+
|
|
70
|
+
if amount_usd < BOROS_MIN_DEPOSIT_USD:
|
|
71
|
+
return True, f"Skipping small Boros funding (${amount_usd:.2f})"
|
|
72
|
+
|
|
73
|
+
if self.simulation:
|
|
74
|
+
return True, f"[SIMULATION] Funded Boros with ~${amount_usd:.2f} HYPE"
|
|
75
|
+
|
|
76
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
77
|
+
wallet_address = strategy_wallet.get("address")
|
|
78
|
+
if not wallet_address:
|
|
79
|
+
return False, "No strategy wallet address configured"
|
|
80
|
+
|
|
81
|
+
if not self._sign_callback:
|
|
82
|
+
return False, "No signing callback configured"
|
|
83
|
+
|
|
84
|
+
if not self.boros_adapter:
|
|
85
|
+
return False, "Boros adapter not configured"
|
|
86
|
+
|
|
87
|
+
hype_price = float(inventory.hype_price_usd or 0.0)
|
|
88
|
+
if hype_price <= 0:
|
|
89
|
+
return False, f"Invalid HYPE price (${hype_price:.6f})"
|
|
90
|
+
|
|
91
|
+
target_hype = float(amount_usd) / hype_price
|
|
92
|
+
target_wei = int(target_hype * 1e18)
|
|
93
|
+
|
|
94
|
+
# 1) If we have OFT HYPE on Arbitrum, deposit it first (idempotent; avoids double-bridging).
|
|
95
|
+
available_oft_hype = float(inventory.hype_oft_arb_balance or 0.0)
|
|
96
|
+
if available_oft_hype > 0:
|
|
97
|
+
deposit_hype = min(available_oft_hype, target_hype)
|
|
98
|
+
deposit_usd = deposit_hype * hype_price
|
|
99
|
+
if deposit_usd < 1.0:
|
|
100
|
+
return True, f"Skipping tiny OFT HYPE deposit (${deposit_usd:.2f})"
|
|
101
|
+
|
|
102
|
+
deposit_wei = int(deposit_hype * 1e18)
|
|
103
|
+
ok_dep, dep_res = await self.boros_adapter.deposit_to_cross_margin(
|
|
104
|
+
collateral_address=collateral_address,
|
|
105
|
+
amount_wei=deposit_wei,
|
|
106
|
+
token_id=token_id,
|
|
107
|
+
market_id=market_id,
|
|
108
|
+
)
|
|
109
|
+
if not ok_dep:
|
|
110
|
+
return False, f"Boros deposit failed: {dep_res}"
|
|
111
|
+
|
|
112
|
+
logger.info(
|
|
113
|
+
f"Deposited {deposit_hype:.6f} HYPE (≈${deposit_usd:.2f}) to Boros cross margin"
|
|
114
|
+
)
|
|
115
|
+
return True, (
|
|
116
|
+
f"Funded Boros with {deposit_hype:.6f} HYPE (≈${deposit_usd:.2f}) from Arbitrum OFT balance"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# 2) Bridge native HYPE from HyperEVM to Arbitrum using the OFT contract.
|
|
120
|
+
hype_balance = float(inventory.hype_hyperevm_balance or 0.0)
|
|
121
|
+
if hype_balance <= MIN_HYPE_GAS + 0.0005:
|
|
122
|
+
return (
|
|
123
|
+
False,
|
|
124
|
+
f"Insufficient HyperEVM HYPE to bridge (balance={hype_balance:.6f}, min_gas={MIN_HYPE_GAS:.6f})",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
reserve_wei = int(MIN_HYPE_GAS * 1e18)
|
|
128
|
+
balance_wei = int(hype_balance * 1e18)
|
|
129
|
+
max_value_wei = max(0, balance_wei - reserve_wei)
|
|
130
|
+
if max_value_wei <= 0:
|
|
131
|
+
return False, "Insufficient HyperEVM HYPE to bridge after reserving gas"
|
|
132
|
+
|
|
133
|
+
bridge_amount_wei = min(target_wei, max_value_wei)
|
|
134
|
+
if bridge_amount_wei <= 0:
|
|
135
|
+
return True, "Boros funding: nothing to bridge"
|
|
136
|
+
|
|
137
|
+
to_bytes32 = _pad_address_bytes32(wallet_address)
|
|
138
|
+
|
|
139
|
+
async with web3_from_chain_id(HYPEREVM_CHAIN_ID) as w3:
|
|
140
|
+
contract = w3.eth.contract(
|
|
141
|
+
address=w3.to_checksum_address(HYPE_OFT_ADDRESS),
|
|
142
|
+
abi=HYPE_OFT_ABI,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Quote fee, then clamp amount to fit balance (amount + fee) while
|
|
146
|
+
# still leaving MIN_HYPE_GAS behind for future gas.
|
|
147
|
+
send_params = (
|
|
148
|
+
int(LZ_EID_ARBITRUM),
|
|
149
|
+
to_bytes32,
|
|
150
|
+
int(bridge_amount_wei),
|
|
151
|
+
0,
|
|
152
|
+
b"",
|
|
153
|
+
b"",
|
|
154
|
+
b"",
|
|
155
|
+
)
|
|
156
|
+
fee = await contract.functions.quoteSend(send_params, False).call()
|
|
157
|
+
native_fee = int(fee[0])
|
|
158
|
+
lz_token_fee = int(fee[1])
|
|
159
|
+
|
|
160
|
+
max_send_amount_wei = max(0, max_value_wei - native_fee)
|
|
161
|
+
if bridge_amount_wei > max_send_amount_wei:
|
|
162
|
+
bridge_amount_wei = max_send_amount_wei
|
|
163
|
+
if bridge_amount_wei <= 0:
|
|
164
|
+
return False, "Insufficient HyperEVM HYPE to cover OFT bridge fee"
|
|
165
|
+
|
|
166
|
+
send_params = (
|
|
167
|
+
int(LZ_EID_ARBITRUM),
|
|
168
|
+
to_bytes32,
|
|
169
|
+
int(bridge_amount_wei),
|
|
170
|
+
0,
|
|
171
|
+
b"",
|
|
172
|
+
b"",
|
|
173
|
+
b"",
|
|
174
|
+
)
|
|
175
|
+
fee = await contract.functions.quoteSend(send_params, False).call()
|
|
176
|
+
native_fee = int(fee[0])
|
|
177
|
+
lz_token_fee = int(fee[1])
|
|
178
|
+
|
|
179
|
+
total_value_wei = int(bridge_amount_wei) + int(native_fee)
|
|
180
|
+
if total_value_wei > max_value_wei:
|
|
181
|
+
return False, "Insufficient HyperEVM HYPE to bridge after fee quote"
|
|
182
|
+
|
|
183
|
+
tx = await encode_call(
|
|
184
|
+
target=HYPE_OFT_ADDRESS,
|
|
185
|
+
abi=HYPE_OFT_ABI,
|
|
186
|
+
fn_name="send",
|
|
187
|
+
args=[
|
|
188
|
+
send_params,
|
|
189
|
+
(int(native_fee), int(lz_token_fee)),
|
|
190
|
+
AsyncWeb3.to_checksum_address(wallet_address),
|
|
191
|
+
],
|
|
192
|
+
from_address=wallet_address,
|
|
193
|
+
chain_id=HYPEREVM_CHAIN_ID,
|
|
194
|
+
value=total_value_wei,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
tx_hash = await send_transaction(tx, self._sign_callback, wait_for_receipt=True)
|
|
198
|
+
|
|
199
|
+
bridged_hype = float(bridge_amount_wei) / 1e18
|
|
200
|
+
bridged_usd = bridged_hype * hype_price
|
|
201
|
+
|
|
202
|
+
# Track in-flight amount so planner doesn't double-fund while the bridge settles.
|
|
203
|
+
self._planner_runtime.in_flight_boros_oft_hype = bridged_hype
|
|
204
|
+
self._planner_runtime.in_flight_boros_oft_hype_balance_before = float(
|
|
205
|
+
inventory.hype_oft_arb_balance or 0.0
|
|
206
|
+
)
|
|
207
|
+
self._planner_runtime.in_flight_boros_oft_hype_started_at = datetime.utcnow()
|
|
208
|
+
|
|
209
|
+
logger.info(
|
|
210
|
+
f"Initiated OFT bridge HyperEVM->Arbitrum: {bridged_hype:.6f} HYPE (≈${bridged_usd:.2f}), tx={tx_hash}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return True, (
|
|
214
|
+
f"Bridging {bridged_hype:.6f} HYPE (≈${bridged_usd:.2f}) HyperEVM→Arbitrum via OFT; "
|
|
215
|
+
f"tx={tx_hash} (LayerZero: https://layerzeroscan.com/tx/{tx_hash}). "
|
|
216
|
+
"Once bridged HYPE lands on Arbitrum, the next tick will deposit it to Boros."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def _ensure_boros_position(
|
|
220
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
221
|
+
) -> tuple[bool, str]:
|
|
222
|
+
# If Boros operations fail unexpectedly, triggers fail-safe liquidation.
|
|
223
|
+
market_id = int(
|
|
224
|
+
params.get("market_id")
|
|
225
|
+
or self._planner_runtime.current_boros_market_id
|
|
226
|
+
or BOROS_HYPE_MARKET_ID
|
|
227
|
+
)
|
|
228
|
+
token_id = int(
|
|
229
|
+
params.get("token_id")
|
|
230
|
+
or self._planner_runtime.current_boros_token_id
|
|
231
|
+
or BOROS_HYPE_TOKEN_ID
|
|
232
|
+
)
|
|
233
|
+
target_size_usd = float(params.get("target_size_usd") or 0.0)
|
|
234
|
+
|
|
235
|
+
if inventory.boros_pending_withdrawal_usd > 0:
|
|
236
|
+
return True, (
|
|
237
|
+
f"Boros withdrawal pending (~${inventory.boros_pending_withdrawal_usd:.2f}). "
|
|
238
|
+
"Skipping Boros rate position actions until it settles."
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not self.boros_adapter or not market_id:
|
|
242
|
+
return False, "Boros adapter not configured or no market selected"
|
|
243
|
+
|
|
244
|
+
if self.simulation:
|
|
245
|
+
return (
|
|
246
|
+
True,
|
|
247
|
+
f"[SIMULATION] Boros position at market {market_id} set to ${target_size_usd:.0f}",
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
return await self._ensure_boros_position_impl(
|
|
252
|
+
market_id=market_id,
|
|
253
|
+
token_id=token_id,
|
|
254
|
+
target_size_usd=target_size_usd,
|
|
255
|
+
inventory=inventory,
|
|
256
|
+
)
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
logger.error(
|
|
259
|
+
f"[BOROS_FAIL] Critical failure in Boros position management: {exc}"
|
|
260
|
+
)
|
|
261
|
+
# Trigger fail-safe liquidation
|
|
262
|
+
return await self._failsafe_liquidate_all(
|
|
263
|
+
f"Boros position management failed: {exc}"
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def _ensure_boros_position_impl(
|
|
267
|
+
self,
|
|
268
|
+
*,
|
|
269
|
+
market_id: int,
|
|
270
|
+
token_id: int,
|
|
271
|
+
target_size_usd: float,
|
|
272
|
+
inventory: Inventory,
|
|
273
|
+
) -> tuple[bool, str]:
|
|
274
|
+
# 0) Move any isolated collateral to cross margin (cleanup).
|
|
275
|
+
# Boros markets expire, so we need to get the actual market ID from isolated positions.
|
|
276
|
+
try:
|
|
277
|
+
ok_bal, balances = await self.boros_adapter.get_account_balances(
|
|
278
|
+
token_id=int(token_id)
|
|
279
|
+
)
|
|
280
|
+
if ok_bal and isinstance(balances, dict):
|
|
281
|
+
isolated_positions = balances.get("isolated_positions", [])
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"Boros position check: isolated={balances.get('isolated', 0):.6f}, "
|
|
284
|
+
f"cross={balances.get('cross', 0):.6f}, "
|
|
285
|
+
f"total={balances.get('total', 0):.6f}, "
|
|
286
|
+
f"isolated_positions={isolated_positions}"
|
|
287
|
+
)
|
|
288
|
+
for iso_pos in isolated_positions:
|
|
289
|
+
iso_market_id = iso_pos.get("market_id")
|
|
290
|
+
iso_balance = float(iso_pos.get("balance", 0) or 0.0)
|
|
291
|
+
if iso_market_id and iso_balance > 0.001:
|
|
292
|
+
iso_wei = int(iso_balance * 1e18) # Boros cash units
|
|
293
|
+
logger.info(
|
|
294
|
+
f"Moving {iso_balance:.6f} collateral from isolated market {iso_market_id} to cross"
|
|
295
|
+
)
|
|
296
|
+
ok_xfer, res_xfer = await self.boros_adapter.cash_transfer(
|
|
297
|
+
market_id=int(iso_market_id),
|
|
298
|
+
amount_wei=iso_wei,
|
|
299
|
+
is_deposit=False, # isolated -> cross
|
|
300
|
+
)
|
|
301
|
+
if ok_xfer:
|
|
302
|
+
await asyncio.sleep(2)
|
|
303
|
+
else:
|
|
304
|
+
logger.warning(
|
|
305
|
+
f"Failed Boros isolated->cross transfer for market {iso_market_id}: {res_xfer}"
|
|
306
|
+
)
|
|
307
|
+
except Exception as exc: # noqa: BLE001
|
|
308
|
+
logger.warning(f"Failed Boros isolated->cross transfer: {exc}")
|
|
309
|
+
|
|
310
|
+
# 1) Best-effort: if any OFT HYPE is sitting idle on Arbitrum, deposit it to cross margin.
|
|
311
|
+
if inventory.hype_oft_arb_balance > 0.0:
|
|
312
|
+
try:
|
|
313
|
+
deposit_hype = float(inventory.hype_oft_arb_balance or 0.0)
|
|
314
|
+
deposit_usd = deposit_hype * float(inventory.hype_price_usd or 0.0)
|
|
315
|
+
if deposit_usd >= 1.0:
|
|
316
|
+
ok_dep, dep_res = await self.boros_adapter.deposit_to_cross_margin(
|
|
317
|
+
collateral_address=HYPE_OFT_ADDRESS,
|
|
318
|
+
amount_wei=int(deposit_hype * 1e18),
|
|
319
|
+
token_id=int(token_id),
|
|
320
|
+
market_id=int(market_id),
|
|
321
|
+
)
|
|
322
|
+
if ok_dep:
|
|
323
|
+
logger.info(
|
|
324
|
+
f"Deposited idle OFT HYPE to Boros: {deposit_hype:.6f} (≈${deposit_usd:.2f})"
|
|
325
|
+
)
|
|
326
|
+
await asyncio.sleep(2)
|
|
327
|
+
else:
|
|
328
|
+
logger.warning(
|
|
329
|
+
f"Failed to deposit OFT HYPE to Boros: {dep_res}"
|
|
330
|
+
)
|
|
331
|
+
except Exception as exc: # noqa: BLE001
|
|
332
|
+
logger.warning(f"Failed to deposit OFT HYPE to Boros: {exc}")
|
|
333
|
+
|
|
334
|
+
# 2) Rollover: close positions in other markets (best effort).
|
|
335
|
+
try:
|
|
336
|
+
for mid in inventory.boros_position_market_ids or []:
|
|
337
|
+
try:
|
|
338
|
+
mid_int = int(mid)
|
|
339
|
+
except (TypeError, ValueError):
|
|
340
|
+
continue
|
|
341
|
+
if mid_int <= 0 or mid_int == int(market_id):
|
|
342
|
+
continue
|
|
343
|
+
try:
|
|
344
|
+
await self.boros_adapter.close_positions_market(
|
|
345
|
+
mid_int, token_id=int(token_id)
|
|
346
|
+
)
|
|
347
|
+
await asyncio.sleep(2)
|
|
348
|
+
except Exception as exc: # noqa: BLE001
|
|
349
|
+
logger.warning(f"Failed to close Boros market {mid_int}: {exc}")
|
|
350
|
+
except Exception as exc: # noqa: BLE001
|
|
351
|
+
logger.warning(f"Failed Boros rollover close: {exc}")
|
|
352
|
+
|
|
353
|
+
success, positions = await self.boros_adapter.get_active_positions(
|
|
354
|
+
market_id=int(market_id)
|
|
355
|
+
)
|
|
356
|
+
if not success:
|
|
357
|
+
return False, "Failed to get Boros positions"
|
|
358
|
+
|
|
359
|
+
current_size_usd = 0.0
|
|
360
|
+
if positions:
|
|
361
|
+
pos = positions[0]
|
|
362
|
+
current_size_usd = abs(float(pos.get("size", 0) or 0.0))
|
|
363
|
+
|
|
364
|
+
diff_usd = float(target_size_usd) - float(current_size_usd)
|
|
365
|
+
if abs(diff_usd) < self._planner_config.boros_resize_min_excess_usd:
|
|
366
|
+
return True, f"Boros position already at target (${current_size_usd:.0f})"
|
|
367
|
+
|
|
368
|
+
size_yu_wei = int(abs(diff_usd) * 1e18) # Boros YU wei
|
|
369
|
+
|
|
370
|
+
if diff_usd > 0:
|
|
371
|
+
# Open/increase SHORT side (receive fixed)
|
|
372
|
+
ok_open, open_res = await self.boros_adapter.place_rate_order(
|
|
373
|
+
market_id=int(market_id),
|
|
374
|
+
token_id=int(token_id),
|
|
375
|
+
size_yu_wei=size_yu_wei,
|
|
376
|
+
side="short",
|
|
377
|
+
tif="IOC",
|
|
378
|
+
)
|
|
379
|
+
if not ok_open:
|
|
380
|
+
return False, f"Failed to open Boros position: {open_res}"
|
|
381
|
+
return (
|
|
382
|
+
True,
|
|
383
|
+
f"Boros position increased by ${diff_usd:.0f} on market {market_id}",
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
# Close/decrease position
|
|
387
|
+
ok_close, close_res = await self.boros_adapter.close_positions_market(
|
|
388
|
+
market_id=int(market_id),
|
|
389
|
+
token_id=int(token_id),
|
|
390
|
+
size_yu_wei=size_yu_wei,
|
|
391
|
+
)
|
|
392
|
+
if not ok_close:
|
|
393
|
+
return False, f"Failed to close Boros position: {close_res}"
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
True,
|
|
397
|
+
f"Boros position decreased by ${abs(diff_usd):.0f} on market {market_id}",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
async def _complete_pending_withdrawal(
|
|
401
|
+
self, params: dict[str, Any], inventory: Inventory
|
|
402
|
+
) -> tuple[bool, str]:
|
|
403
|
+
# Legacy helper used by some withdrawal flows: swap USDT->USDC on Arbitrum
|
|
404
|
+
usdt_idle = float(params.get("usdt_idle") or 0.0)
|
|
405
|
+
|
|
406
|
+
if self.simulation:
|
|
407
|
+
return (
|
|
408
|
+
True,
|
|
409
|
+
f"[SIMULATION] Completed pending withdrawal: ${usdt_idle:.2f} USDT → USDC",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if usdt_idle < 1.0:
|
|
413
|
+
return True, f"Withdrawal completion: no USDT to swap (${usdt_idle:.2f})"
|
|
414
|
+
|
|
415
|
+
if not self.balance_adapter:
|
|
416
|
+
return False, "Balance adapter not configured"
|
|
417
|
+
if not self.brap_adapter:
|
|
418
|
+
return False, "BRAP adapter not configured"
|
|
419
|
+
|
|
420
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
421
|
+
address = strategy_wallet.get("address")
|
|
422
|
+
if not address:
|
|
423
|
+
return False, "No strategy wallet address configured"
|
|
424
|
+
|
|
425
|
+
ok_usdt, usdt_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
426
|
+
USDT_ARB
|
|
427
|
+
)
|
|
428
|
+
if not ok_usdt or usdt_raw <= 0:
|
|
429
|
+
return True, f"Withdrawal completion: no USDT to swap (${usdt_idle:.2f})"
|
|
430
|
+
|
|
431
|
+
ok_swap, swap_res = await self.brap_adapter.swap_from_token_ids(
|
|
432
|
+
from_token_id=USDT_ARB,
|
|
433
|
+
to_token_id=USDC_ARB,
|
|
434
|
+
from_address=address,
|
|
435
|
+
amount=str(int(usdt_raw)),
|
|
436
|
+
slippage=0.005,
|
|
437
|
+
strategy_name="boros_hype_strategy",
|
|
438
|
+
)
|
|
439
|
+
if not ok_swap:
|
|
440
|
+
return False, f"Withdrawal completion swap failed: {swap_res}"
|
|
441
|
+
|
|
442
|
+
ok_usdc, usdc_raw = await self.balance_adapter.get_vault_wallet_balance(
|
|
443
|
+
USDC_ARB
|
|
444
|
+
)
|
|
445
|
+
usdc_tokens = (float(usdc_raw) / 1e6) if ok_usdc and usdc_raw > 0 else 0.0
|
|
446
|
+
|
|
447
|
+
return True, (
|
|
448
|
+
f"Withdrawal completion: swapped ${usdt_idle:.2f} USDT→USDC "
|
|
449
|
+
f"(${usdc_tokens:.2f} USDC)"
|
|
450
|
+
)
|