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.

Files changed (156) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
  4. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  5. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
  6. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  7. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  8. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  9. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  10. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  11. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  12. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  13. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  14. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  15. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  16. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  17. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  18. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  19. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  20. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
  21. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  22. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
  23. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  24. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
  25. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  26. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  28. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  29. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  30. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  31. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  32. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  33. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  34. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  35. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  36. wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
  37. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  38. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
  39. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  40. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  41. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  42. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  43. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  44. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  45. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  46. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  47. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  48. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  49. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  50. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  51. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  52. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  53. wayfinder_paths/conftest.py +24 -17
  54. wayfinder_paths/core/__init__.py +0 -3
  55. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  56. wayfinder_paths/core/adapters/models.py +17 -7
  57. wayfinder_paths/core/clients/BRAPClient.py +4 -1
  58. wayfinder_paths/core/clients/ClientManager.py +0 -7
  59. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  60. wayfinder_paths/core/clients/TokenClient.py +47 -1
  61. wayfinder_paths/core/clients/WayfinderClient.py +1 -3
  62. wayfinder_paths/core/clients/__init__.py +0 -5
  63. wayfinder_paths/core/clients/protocols.py +21 -35
  64. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  65. wayfinder_paths/core/config.py +10 -162
  66. wayfinder_paths/core/constants/__init__.py +73 -2
  67. wayfinder_paths/core/constants/base.py +8 -17
  68. wayfinder_paths/core/constants/chains.py +36 -0
  69. wayfinder_paths/core/constants/contracts.py +52 -0
  70. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  71. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  72. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  73. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  74. wayfinder_paths/core/constants/tokens.py +9 -0
  75. wayfinder_paths/core/engine/manifest.py +66 -0
  76. wayfinder_paths/core/strategies/Strategy.py +0 -71
  77. wayfinder_paths/core/strategies/__init__.py +10 -1
  78. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  79. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  80. wayfinder_paths/core/utils/test_transaction.py +289 -0
  81. wayfinder_paths/core/utils/tokens.py +28 -0
  82. wayfinder_paths/core/utils/transaction.py +57 -8
  83. wayfinder_paths/core/utils/web3.py +8 -3
  84. wayfinder_paths/mcp/__init__.py +5 -0
  85. wayfinder_paths/mcp/preview.py +185 -0
  86. wayfinder_paths/mcp/scripting.py +84 -0
  87. wayfinder_paths/mcp/server.py +52 -0
  88. wayfinder_paths/mcp/state/profile_store.py +195 -0
  89. wayfinder_paths/mcp/state/store.py +89 -0
  90. wayfinder_paths/mcp/test_scripting.py +267 -0
  91. wayfinder_paths/mcp/tools/__init__.py +0 -0
  92. wayfinder_paths/mcp/tools/balances.py +290 -0
  93. wayfinder_paths/mcp/tools/discovery.py +158 -0
  94. wayfinder_paths/mcp/tools/execute.py +770 -0
  95. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  96. wayfinder_paths/mcp/tools/quotes.py +288 -0
  97. wayfinder_paths/mcp/tools/run_script.py +286 -0
  98. wayfinder_paths/mcp/tools/strategies.py +188 -0
  99. wayfinder_paths/mcp/tools/tokens.py +46 -0
  100. wayfinder_paths/mcp/tools/wallets.py +354 -0
  101. wayfinder_paths/mcp/utils.py +129 -0
  102. wayfinder_paths/policies/enso.py +1 -2
  103. wayfinder_paths/policies/hyper_evm.py +6 -3
  104. wayfinder_paths/policies/hyperlend.py +1 -2
  105. wayfinder_paths/policies/hyperliquid.py +1 -1
  106. wayfinder_paths/policies/lifi.py +18 -0
  107. wayfinder_paths/policies/moonwell.py +12 -7
  108. wayfinder_paths/policies/prjx.py +1 -3
  109. wayfinder_paths/policies/util.py +8 -2
  110. wayfinder_paths/run_strategy.py +97 -300
  111. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  112. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
  113. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  114. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  115. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  116. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  117. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  118. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  119. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  120. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  121. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  122. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  123. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  124. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  125. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  126. wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
  127. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  128. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  129. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
  130. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
  131. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
  132. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  133. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
  134. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
  135. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  136. wayfinder_paths/tests/test_test_coverage.py +1 -4
  137. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  138. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  139. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  140. wayfinder_paths/core/clients/WalletClient.py +0 -41
  141. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  142. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  143. wayfinder_paths/scripts/create_strategy.py +0 -139
  144. wayfinder_paths/scripts/make_wallets.py +0 -142
  145. wayfinder_paths/templates/adapter/README.md +0 -150
  146. wayfinder_paths/templates/adapter/adapter.py +0 -16
  147. wayfinder_paths/templates/adapter/examples.json +0 -8
  148. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  149. wayfinder_paths/templates/strategy/README.md +0 -186
  150. wayfinder_paths/templates/strategy/examples.json +0 -11
  151. wayfinder_paths/templates/strategy/strategy.py +0 -35
  152. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  153. wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
  154. wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
  155. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  156. {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
- from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
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 = BasisTradingStrategy.maintenance_rate_from_max_leverage(10)
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 get_max_builder_fee returning sufficient approval
560
- mock_hyperliquid_adapter.get_max_builder_fee = AsyncMock(
561
- return_value=(
562
- True,
563
- 30,
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
- # Should not have called approve_builder_fee
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 get_max_builder_fee returning insufficient approval
608
- mock_hyperliquid_adapter.get_max_builder_fee = AsyncMock(
609
- return_value=(True, 0)
610
- )
611
- mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
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
- # Should have called approve_builder_fee
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,3 @@
1
+ from .strategy import BorosHypeStrategy
2
+
3
+ __all__ = ["BorosHypeStrategy"]
@@ -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
+ )