wayfinder-paths 0.1.14__py3-none-any.whl → 0.1.16__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 (58) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +19 -20
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +91 -22
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +5 -11
  4. wayfinder_paths/adapters/brap_adapter/README.md +22 -19
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +95 -45
  6. wayfinder_paths/adapters/brap_adapter/test_adapter.py +8 -24
  7. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -42
  8. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +8 -15
  9. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
  10. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
  11. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
  12. wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +326 -364
  14. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +285 -189
  15. wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
  16. wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -4
  17. wayfinder_paths/core/config.py +8 -47
  18. wayfinder_paths/core/constants/base.py +0 -1
  19. wayfinder_paths/core/constants/erc20_abi.py +13 -24
  20. wayfinder_paths/core/engine/StrategyJob.py +3 -1
  21. wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
  22. wayfinder_paths/core/strategies/Strategy.py +22 -4
  23. wayfinder_paths/core/utils/erc20_service.py +100 -0
  24. wayfinder_paths/core/utils/evm_helpers.py +1 -8
  25. wayfinder_paths/core/utils/transaction.py +191 -0
  26. wayfinder_paths/core/utils/web3.py +66 -0
  27. wayfinder_paths/policies/erc20.py +1 -1
  28. wayfinder_paths/run_strategy.py +42 -6
  29. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +263 -220
  30. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +132 -155
  31. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
  32. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +123 -80
  33. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
  34. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -6
  35. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2270 -1328
  36. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +282 -121
  37. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
  38. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +107 -85
  39. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
  40. wayfinder_paths/templates/adapter/README.md +1 -1
  41. wayfinder_paths/templates/strategy/README.md +1 -5
  42. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
  43. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +45 -54
  44. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
  45. wayfinder_paths/abis/generic/erc20.json +0 -383
  46. wayfinder_paths/core/clients/sdk_example.py +0 -125
  47. wayfinder_paths/core/engine/__init__.py +0 -5
  48. wayfinder_paths/core/services/__init__.py +0 -0
  49. wayfinder_paths/core/services/base.py +0 -130
  50. wayfinder_paths/core/services/local_evm_txn.py +0 -334
  51. wayfinder_paths/core/services/local_token_txn.py +0 -242
  52. wayfinder_paths/core/services/web3_service.py +0 -43
  53. wayfinder_paths/core/wallets/README.md +0 -88
  54. wayfinder_paths/core/wallets/WalletManager.py +0 -56
  55. wayfinder_paths/core/wallets/__init__.py +0 -7
  56. wayfinder_paths/scripts/run_strategy.py +0 -152
  57. wayfinder_paths/strategies/config.py +0 -85
  58. {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
@@ -143,7 +143,8 @@ class TestBasisTradingStrategy:
143
143
  return_value=(True, 100.0) # (deposit_confirmed, final_balance)
144
144
  )
145
145
  mock.wait_for_withdrawal = AsyncMock(
146
- return_value=(True, {"0x123456": 100.0}) # tx_hash -> amount (float)
146
+ # tx_hash -> amount (float)
147
+ return_value=(True, {"0x123456": 100.0})
147
148
  )
148
149
  return mock
149
150
 
@@ -170,68 +171,53 @@ class TestBasisTradingStrategy:
170
171
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
171
172
  return_value=ledger_adapter,
172
173
  ):
173
- with patch(
174
- "wayfinder_paths.strategies.basis_trading_strategy.strategy.WalletManager"
175
- ):
176
- from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
177
- BasisTradingStrategy,
178
- )
179
-
180
- s = BasisTradingStrategy(
181
- config={
182
- "main_wallet": {"address": "0x1234"},
183
- "strategy_wallet": {"address": "0x5678"},
184
- },
185
- )
186
- s.hyperliquid_adapter = mock_hyperliquid_adapter
187
- s.ledger_adapter = ledger_adapter
188
- s.balance_adapter = MagicMock()
189
- s.balance_adapter.get_balance = AsyncMock(
190
- return_value=(True, 0)
191
- )
192
- s.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
193
- return_value=(True, {})
194
- )
195
- s.balance_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
196
- return_value=(True, {})
197
- )
198
- s.balance_adapter.send_to_address = AsyncMock(
199
- return_value=(True, {"tx_hash": "0x123"})
200
- )
201
- # Mock internal dependencies to prevent MagicMock await errors
202
- # These are needed if the real method somehow gets called
203
- s.balance_adapter.token_client = AsyncMock()
204
- s.balance_adapter.token_client.get_token_details = (
205
- AsyncMock(
206
- return_value={
207
- "id": "usdc",
208
- "address": "0x1234",
209
- "decimals": 6,
210
- }
211
- )
212
- )
213
- s.balance_adapter.token_transactions = AsyncMock()
214
- s.balance_adapter.token_transactions.build_send = AsyncMock(
215
- return_value=(True, {"transaction": "0xMOCK"})
216
- )
217
- s.balance_adapter.wallet_provider = AsyncMock()
218
- s.balance_adapter.wallet_provider.broadcast_transaction = (
219
- AsyncMock(
220
- return_value=(True, {"transaction_hash": "0x123"})
221
- )
222
- )
223
- s.balance_adapter.token_adapter = AsyncMock()
224
- s.balance_adapter.token_adapter.get_token_price = AsyncMock(
225
- return_value=(True, {"current_price": 1.0})
226
- )
227
- # ledger_adapter is real, but ensure its methods are async-mockable
228
- s.balance_adapter.ledger_adapter = ledger_adapter
229
- # Also ensure the balance_adapter's _move_between_wallets won't call real methods
230
- # by making sure all its dependencies return AsyncMock
231
- s.balance_adapter._move_between_wallets = AsyncMock(
232
- return_value=(True, {"transaction_hash": "0x123"})
233
- )
234
- return s
174
+ from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
175
+ BasisTradingStrategy,
176
+ )
177
+
178
+ s = BasisTradingStrategy(
179
+ config={
180
+ "main_wallet": {"address": "0x1234"},
181
+ "strategy_wallet": {"address": "0x5678"},
182
+ },
183
+ )
184
+ s.hyperliquid_adapter = mock_hyperliquid_adapter
185
+ s.ledger_adapter = ledger_adapter
186
+ s.balance_adapter = MagicMock()
187
+ s.balance_adapter.get_balance = AsyncMock(
188
+ return_value=(True, 0)
189
+ )
190
+ s.balance_adapter.move_from_main_wallet_to_strategy_wallet = (
191
+ AsyncMock(return_value=(True, {}))
192
+ )
193
+ s.balance_adapter.move_from_strategy_wallet_to_main_wallet = (
194
+ AsyncMock(return_value=(True, {}))
195
+ )
196
+ s.balance_adapter.send_to_address = AsyncMock(
197
+ return_value=(True, {"tx_hash": "0x123"})
198
+ )
199
+ # Mock internal dependencies to prevent MagicMock await errors
200
+ # These are needed if the real method somehow gets called
201
+ s.balance_adapter.token_client = AsyncMock()
202
+ s.balance_adapter.token_client.get_token_details = AsyncMock(
203
+ return_value={
204
+ "id": "usdc",
205
+ "address": "0x1234",
206
+ "decimals": 6,
207
+ }
208
+ )
209
+ s.balance_adapter.token_adapter = AsyncMock()
210
+ s.balance_adapter.token_adapter.get_token_price = AsyncMock(
211
+ return_value=(True, {"current_price": 1.0})
212
+ )
213
+ # ledger_adapter is real, but ensure its methods are async-mockable
214
+ s.balance_adapter.ledger_adapter = ledger_adapter
215
+ # Also ensure the balance_adapter's _move_between_wallets won't call real methods
216
+ # by making sure all its dependencies return AsyncMock
217
+ s.balance_adapter._move_between_wallets = AsyncMock(
218
+ return_value=(True, {"transaction_hash": "0x123"})
219
+ )
220
+ return s
235
221
 
236
222
  @pytest.mark.asyncio
237
223
  @pytest.mark.smoke
@@ -303,7 +289,7 @@ class TestBasisTradingStrategy:
303
289
 
304
290
  success, msg = await strategy.update()
305
291
  assert success is False
306
- assert "No deposit" in msg
292
+ assert "No funds to manage" in msg
307
293
 
308
294
  @pytest.mark.asyncio
309
295
  async def test_withdraw_without_deposit(self, strategy):
@@ -461,7 +447,7 @@ class TestBasisTradingStrategy:
461
447
  # Try to scale with $5 (below $10 minimum notional)
462
448
  # With 2x leverage, order_usd = 5 * (2/3) = 3.33, below $10
463
449
  success, msg = await strategy._scale_up_position(5.0)
464
- assert success is True # Returns success=True but with message
450
+ assert success # Returns success=True but with message
465
451
  assert "below minimum notional" in msg
466
452
 
467
453
  @pytest.mark.asyncio
@@ -563,7 +549,7 @@ class TestBasisTradingStrategy:
563
549
 
564
550
  # Should have called fill_pair_units to scale up
565
551
  assert mock_filler.fill_pair_units.called
566
- assert success is True
552
+ assert success
567
553
 
568
554
  @pytest.mark.asyncio
569
555
  async def test_ensure_builder_fee_approved_already_approved(
@@ -584,38 +570,35 @@ class TestBasisTradingStrategy:
584
570
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
585
571
  return_value=ledger_adapter,
586
572
  ):
587
- with patch(
588
- "wayfinder_paths.strategies.basis_trading_strategy.strategy.WalletManager"
589
- ):
590
- from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
591
- BasisTradingStrategy,
592
- )
593
-
594
- s = BasisTradingStrategy(
595
- config={
596
- "main_wallet": {"address": "0x1234"},
597
- "strategy_wallet": {"address": "0x5678"},
598
- },
599
- )
600
- s.hyperliquid_adapter = mock_hyperliquid_adapter
601
- s.ledger_adapter = ledger_adapter
602
-
603
- # Mock get_max_builder_fee returning sufficient approval
604
- mock_hyperliquid_adapter.get_max_builder_fee = AsyncMock(
605
- return_value=(
606
- True,
607
- 30,
608
- ) # Already approved for 30 tenths bp
609
- )
610
- mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
611
- return_value=(True, {"status": "ok"})
612
- )
613
-
614
- success, msg = await s.ensure_builder_fee_approved()
615
- assert success is True
616
- assert "already approved" in msg.lower()
617
- # Should not have called approve_builder_fee
618
- mock_hyperliquid_adapter.approve_builder_fee.assert_not_called()
573
+ from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
574
+ BasisTradingStrategy,
575
+ )
576
+
577
+ s = BasisTradingStrategy(
578
+ config={
579
+ "main_wallet": {"address": "0x1234"},
580
+ "strategy_wallet": {"address": "0x5678"},
581
+ },
582
+ )
583
+ s.hyperliquid_adapter = mock_hyperliquid_adapter
584
+ s.ledger_adapter = ledger_adapter
585
+
586
+ # Mock get_max_builder_fee returning sufficient approval
587
+ mock_hyperliquid_adapter.get_max_builder_fee = AsyncMock(
588
+ return_value=(
589
+ True,
590
+ 30,
591
+ ) # Already approved for 30 tenths bp
592
+ )
593
+ mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
594
+ return_value=(True, {"status": "ok"})
595
+ )
596
+
597
+ success, msg = await s.ensure_builder_fee_approved()
598
+ assert success
599
+ assert "already approved" in msg.lower()
600
+ # Should not have called approve_builder_fee
601
+ mock_hyperliquid_adapter.approve_builder_fee.assert_not_called()
619
602
 
620
603
  @pytest.mark.asyncio
621
604
  async def test_ensure_builder_fee_approved_needs_approval(
@@ -636,35 +619,32 @@ class TestBasisTradingStrategy:
636
619
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
637
620
  return_value=ledger_adapter,
638
621
  ):
639
- with patch(
640
- "wayfinder_paths.strategies.basis_trading_strategy.strategy.WalletManager"
641
- ):
642
- from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
643
- BasisTradingStrategy,
644
- )
645
-
646
- s = BasisTradingStrategy(
647
- config={
648
- "main_wallet": {"address": "0x1234"},
649
- "strategy_wallet": {"address": "0x5678"},
650
- },
651
- )
652
- s.hyperliquid_adapter = mock_hyperliquid_adapter
653
- s.ledger_adapter = ledger_adapter
654
-
655
- # Mock get_max_builder_fee returning insufficient approval
656
- mock_hyperliquid_adapter.get_max_builder_fee = AsyncMock(
657
- return_value=(True, 0) # Not approved yet
658
- )
659
- mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
660
- return_value=(True, {"status": "ok"})
661
- )
662
-
663
- success, msg = await s.ensure_builder_fee_approved()
664
- assert success is True
665
- assert "approved" in msg.lower()
666
- # Should have called approve_builder_fee
667
- mock_hyperliquid_adapter.approve_builder_fee.assert_called_once()
622
+ from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
623
+ BasisTradingStrategy,
624
+ )
625
+
626
+ s = BasisTradingStrategy(
627
+ config={
628
+ "main_wallet": {"address": "0x1234"},
629
+ "strategy_wallet": {"address": "0x5678"},
630
+ },
631
+ )
632
+ s.hyperliquid_adapter = mock_hyperliquid_adapter
633
+ s.ledger_adapter = ledger_adapter
634
+
635
+ # Mock get_max_builder_fee returning insufficient approval
636
+ mock_hyperliquid_adapter.get_max_builder_fee = AsyncMock(
637
+ return_value=(True, 0) # Not approved yet
638
+ )
639
+ mock_hyperliquid_adapter.approve_builder_fee = AsyncMock(
640
+ return_value=(True, {"status": "ok"})
641
+ )
642
+
643
+ success, msg = await s.ensure_builder_fee_approved()
644
+ assert success
645
+ assert "approved" in msg.lower()
646
+ # Should have called approve_builder_fee
647
+ mock_hyperliquid_adapter.approve_builder_fee.assert_called_once()
668
648
 
669
649
  @pytest.mark.asyncio
670
650
  async def test_portfolio_value_includes_spot_holdings(
@@ -832,7 +812,7 @@ class TestBasisTradingStrategy:
832
812
  mock_filler_class.return_value = mock_filler
833
813
 
834
814
  success, _ = await strategy.update()
835
- assert success is True
815
+ assert success
836
816
 
837
817
  # Target spot was $66.67, so we should transfer $33.33 spot->perp.
838
818
  mock_hyperliquid_adapter.transfer_spot_to_perp.assert_called_once()
@@ -907,7 +887,7 @@ class TestBasisTradingStrategy:
907
887
  strategy._find_and_open_position = AsyncMock(return_value=(True, "redeployed"))
908
888
 
909
889
  success, msg = await strategy.update()
910
- assert success is True
890
+ assert success
911
891
  assert msg == "redeployed"
912
892
  strategy._close_position.assert_awaited_once()
913
893
  strategy._find_and_open_position.assert_awaited_once()
@@ -954,29 +934,26 @@ class TestBasisTradingStrategy:
954
934
  "wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
955
935
  return_value=ledger_adapter,
956
936
  ):
957
- with patch(
958
- "wayfinder_paths.strategies.basis_trading_strategy.strategy.WalletManager"
959
- ):
960
- from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
961
- BasisTradingStrategy,
962
- )
963
-
964
- s = BasisTradingStrategy(
965
- config={
966
- "main_wallet": {"address": "0x1234"},
967
- "strategy_wallet": {"address": "0x5678"},
968
- },
969
- )
970
- s.hyperliquid_adapter = mock_hyperliquid_adapter
971
- s.ledger_adapter = ledger_adapter
972
-
973
- # Mock get_strategy_net_deposit to return float (not dict)
974
- s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
975
- return_value=(True, 2500.0)
976
- )
977
-
978
- # Run setup - should not raise AttributeError
979
- await s.setup()
980
-
981
- # Verify deposit_amount was set from the float
982
- assert s.deposit_amount == 2500.0
937
+ from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
938
+ BasisTradingStrategy,
939
+ )
940
+
941
+ s = BasisTradingStrategy(
942
+ config={
943
+ "main_wallet": {"address": "0x1234"},
944
+ "strategy_wallet": {"address": "0x5678"},
945
+ },
946
+ )
947
+ s.hyperliquid_adapter = mock_hyperliquid_adapter
948
+ s.ledger_adapter = ledger_adapter
949
+
950
+ # Mock get_strategy_net_deposit to return float (not dict)
951
+ s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
952
+ return_value=(True, 2500.0)
953
+ )
954
+
955
+ # Run setup - should not raise AttributeError
956
+ await s.setup()
957
+
958
+ # Verify deposit_amount was set from the float
959
+ assert s.deposit_amount == 2500.0
@@ -30,7 +30,6 @@ Allocates USDT0 on HyperEVM across HyperLend stablecoin markets. The strategy:
30
30
  - `LedgerAdapter` for net deposit + rotation history.
31
31
  - `BRAPAdapter` to source quotes/swap stablecoins.
32
32
  - `HyperlendAdapter` for asset views, lend/withdraw ops, supply caps.
33
- - `LocalTokenTxnService` via `DefaultWeb3Service` for low-level sends/approvals leveraged by the adapters.
34
33
 
35
34
  ## Actions
36
35
 
@@ -2,6 +2,7 @@ import asyncio
2
2
  import math
3
3
  import time
4
4
  import unicodedata
5
+ from collections.abc import Awaitable, Callable
5
6
  from datetime import UTC, datetime, timedelta, timezone
6
7
  from decimal import ROUND_DOWN, ROUND_UP, Decimal
7
8
  from typing import Any, Literal
@@ -17,11 +18,6 @@ from wayfinder_paths.adapters.hyperlend_adapter.adapter import HyperlendAdapter
17
18
  from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
18
19
  from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
19
20
  from wayfinder_paths.core.constants.base import DEFAULT_SLIPPAGE
20
- from wayfinder_paths.core.services.base import Web3Service
21
- from wayfinder_paths.core.services.local_token_txn import (
22
- LocalTokenTxnService,
23
- )
24
- from wayfinder_paths.core.services.web3_service import DefaultWeb3Service
25
21
  from wayfinder_paths.core.strategies.descriptors import (
26
22
  Complexity,
27
23
  Directionality,
@@ -31,7 +27,6 @@ from wayfinder_paths.core.strategies.descriptors import (
31
27
  Volatility,
32
28
  )
33
29
  from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
34
- from wayfinder_paths.core.wallets.WalletManager import WalletManager
35
30
  from wayfinder_paths.policies.enso import ENSO_ROUTER, enso_swap
36
31
  from wayfinder_paths.policies.erc20 import erc20_spender_for_any_token
37
32
  from wayfinder_paths.policies.hyper_evm import (
@@ -196,9 +191,16 @@ class HyperlendStableYieldStrategy(Strategy):
196
191
  *,
197
192
  main_wallet: dict[str, Any] | None = None,
198
193
  strategy_wallet: dict[str, Any] | None = None,
199
- web3_service: Web3Service = None,
194
+ api_key: str | None = None,
195
+ main_wallet_signing_callback: Callable[[dict], Awaitable[str]] | None = None,
196
+ strategy_wallet_signing_callback: Callable[[dict], Awaitable[str]]
197
+ | None = None,
200
198
  ):
201
- super().__init__()
199
+ super().__init__(
200
+ api_key=api_key,
201
+ main_wallet_signing_callback=main_wallet_signing_callback,
202
+ strategy_wallet_signing_callback=strategy_wallet_signing_callback,
203
+ )
202
204
  merged_config: dict[str, Any] = dict(config or {})
203
205
  if main_wallet is not None:
204
206
  merged_config["main_wallet"] = main_wallet
@@ -207,10 +209,7 @@ class HyperlendStableYieldStrategy(Strategy):
207
209
 
208
210
  self.config = merged_config
209
211
  self.balance_adapter = None
210
- self.tx_adapter = None
211
212
  self.token_adapter = None
212
- self.evm_transaction_adapter = None
213
- self.web3_service = web3_service
214
213
  self.pool_adapter = None
215
214
  self.brap_adapter = None
216
215
  self.hyperlend_adapter = None
@@ -231,26 +230,20 @@ class HyperlendStableYieldStrategy(Strategy):
231
230
  "strategy": self.config,
232
231
  }
233
232
 
234
- if self.web3_service is None:
235
- wallet_provider = WalletManager.get_provider(adapter_config)
236
- token_transaction_service = LocalTokenTxnService(
237
- adapter_config,
238
- wallet_provider=wallet_provider,
239
- )
240
- web3_service = DefaultWeb3Service(
241
- wallet_provider=wallet_provider,
242
- evm_transactions=token_transaction_service,
243
- )
244
- else:
245
- web3_service = self.web3_service
246
- token_transaction_service = web3_service.token_transactions
247
- balance = BalanceAdapter(adapter_config, web3_service=web3_service)
233
+ balance = BalanceAdapter(
234
+ adapter_config,
235
+ main_wallet_signing_callback=self.main_wallet_signing_callback,
236
+ strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
237
+ )
248
238
  token_adapter = TokenAdapter()
249
- ledger_adapter = LedgerAdapter() # here
250
- brap_adapter = BRAPAdapter(web3_service=web3_service)
239
+ ledger_adapter = LedgerAdapter()
240
+ brap_adapter = BRAPAdapter(
241
+ adapter_config,
242
+ strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
243
+ )
251
244
  hyperlend_adapter = HyperlendAdapter(
252
245
  adapter_config,
253
- web3_service=web3_service,
246
+ strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
254
247
  )
255
248
 
256
249
  self.register_adapters(
@@ -260,12 +253,9 @@ class HyperlendStableYieldStrategy(Strategy):
260
253
  ledger_adapter,
261
254
  brap_adapter,
262
255
  hyperlend_adapter,
263
- token_transaction_service,
264
256
  ]
265
257
  )
266
258
  self.balance_adapter = balance
267
- self.evm_transaction_adapter = token_transaction_service
268
- self.web3_service = web3_service
269
259
  self.token_adapter = token_adapter
270
260
  self.ledger_adapter = ledger_adapter
271
261
  self.brap_adapter = brap_adapter
@@ -719,8 +709,8 @@ class HyperlendStableYieldStrategy(Strategy):
719
709
  result,
720
710
  tx_data,
721
711
  ) = await self.brap_adapter.swap_from_token_ids(
722
- from_query=from_token_id,
723
- to_query=to_token_id,
712
+ from_token_id=from_token_id,
713
+ to_token_id=to_token_id,
724
714
  from_address=strategy_address,
725
715
  amount=amount_wei_str,
726
716
  slippage=slippage,
@@ -863,78 +853,131 @@ class HyperlendStableYieldStrategy(Strategy):
863
853
  self.usdt_token_info
864
854
  )
865
855
 
856
+ # Get final balances in strategy wallet (don't transfer to main)
857
+ total_usdt = 0.0
866
858
  try:
867
859
  _, total_usdt_wei = await self.balance_adapter.get_balance(
868
860
  query=self.usdt_token_info.get("token_id"),
869
861
  wallet_address=self._get_strategy_wallet_address(),
870
862
  )
871
- except Exception:
872
- total_usdt_wei = 0
873
-
874
- if total_usdt_wei and total_usdt_wei > 0:
875
- total_usdt = float(total_usdt_wei) / (
876
- 10 ** self.usdt_token_info.get("decimals", 18)
877
- )
878
- (
879
- transfer_success,
880
- transfer_message,
881
- ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
882
- self.usdt_token_info.get("token_id"),
883
- total_usdt,
884
- strategy_name=self.name,
885
- )
886
- if transfer_success:
887
- messages.append(
888
- f"Returned {total_usdt:.2f} {self.usdt_token_info.get('symbol')} from strategy wallet to main wallet"
889
- )
890
- else:
891
- messages.append(
892
- "Returned USDT0 to ledger but on-chain transfer failed; treating as withdrawn for simulation"
863
+ if total_usdt_wei and total_usdt_wei > 0:
864
+ total_usdt = float(total_usdt_wei) / (
865
+ 10 ** self.usdt_token_info.get("decimals", 18)
893
866
  )
867
+ except Exception:
868
+ pass
894
869
 
870
+ total_hype = 0.0
895
871
  try:
896
872
  _, total_hype_wei = await self.balance_adapter.get_balance(
897
873
  query=self.hype_token_info.get("token_id"),
898
874
  wallet_address=self._get_strategy_wallet_address(),
899
875
  )
900
- except Exception:
901
- total_hype_wei = 0
902
-
903
- if total_hype_wei and total_hype_wei > 0:
904
- total_hype = float(total_hype_wei) / (
905
- 10 ** self.hype_token_info.get("decimals", 18)
906
- )
907
- total_hype = total_hype * 0.9
908
- (
909
- transfer_success,
910
- transfer_message,
911
- ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
912
- self.hype_token_info.get("token_id"),
913
- total_hype,
914
- strategy_name=self.name,
915
- )
916
- if transfer_success:
917
- messages.append(
918
- f"Returned {total_hype:.2f} {self.hype_token_info.get('symbol')} from strategy wallet to main wallet"
919
- )
920
- else:
921
- messages.append(
922
- "Returned HYPE to ledger but on-chain transfer failed; treating as withdrawn for simulation"
876
+ if total_hype_wei and total_hype_wei > 0:
877
+ total_hype = float(total_hype_wei) / (
878
+ 10 ** self.hype_token_info.get("decimals", 18)
923
879
  )
880
+ except Exception:
881
+ pass
924
882
 
925
883
  if sweep_actions:
926
884
  messages.append(f"Residual sweeps: {'; '.join(sweep_actions)}.")
927
885
 
928
- if not messages:
929
- messages.append("Withdrawal complete; no balances detected to transfer")
886
+ # Report balances in strategy wallet
887
+ balance_parts = []
888
+ if total_usdt > 0:
889
+ balance_parts.append(
890
+ f"{total_usdt:.2f} {self.usdt_token_info.get('symbol')}"
891
+ )
892
+ if total_hype > 0:
893
+ balance_parts.append(
894
+ f"{total_hype:.4f} {self.hype_token_info.get('symbol')}"
895
+ )
896
+
897
+ if balance_parts:
898
+ messages.append(f"Strategy wallet balance: {', '.join(balance_parts)}")
930
899
 
931
900
  self.current_token = None
932
901
  self.current_symbol = None
933
902
  self.current_avg_apy = 0.0
934
903
  self.kept_hype_tokens = 0.0
935
904
 
905
+ strategy_address = self._get_strategy_wallet_address()
906
+ messages.append(
907
+ f"Call exit() to transfer funds from strategy wallet ({strategy_address}) to main wallet"
908
+ )
909
+
936
910
  return (True, ". ".join(messages))
937
911
 
912
+ async def exit(self, **kwargs) -> StatusTuple:
913
+ """Transfer funds from strategy wallet to main wallet."""
914
+ self.logger.info("EXIT: Transferring remaining funds to main wallet")
915
+
916
+ strategy_address = self._get_strategy_wallet_address()
917
+ main_address = self._get_main_wallet_address()
918
+
919
+ if strategy_address.lower() == main_address.lower():
920
+ return (True, "Main wallet is strategy wallet, no transfer needed")
921
+
922
+ transferred_items = []
923
+
924
+ # Transfer USDT0 to main wallet
925
+ usdt_ok, usdt_raw = await self.balance_adapter.get_balance(
926
+ token_id="usdt0-hyperevm",
927
+ wallet_address=strategy_address,
928
+ )
929
+ if usdt_ok and usdt_raw:
930
+ usdt_balance = float(usdt_raw.get("balance", 0))
931
+ if usdt_balance > 1.0:
932
+ self.logger.info(
933
+ f"Transferring {usdt_balance:.2f} USDT0 to main wallet"
934
+ )
935
+ (
936
+ success,
937
+ msg,
938
+ ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
939
+ query="usdt0-hyperevm",
940
+ amount=usdt_balance,
941
+ strategy_name=self.name,
942
+ skip_ledger=False,
943
+ )
944
+ if success:
945
+ transferred_items.append(f"{usdt_balance:.2f} USDT0")
946
+ else:
947
+ self.logger.warning(f"USDT0 transfer failed: {msg}")
948
+
949
+ # Transfer HYPE (minus reserve for tx fees) to main wallet
950
+ hype_ok, hype_raw = await self.balance_adapter.get_balance(
951
+ token_id="hyperliquid-hyperevm",
952
+ wallet_address=strategy_address,
953
+ )
954
+ if hype_ok and hype_raw:
955
+ hype_balance = float(hype_raw.get("balance", 0))
956
+ tx_fee_reserve = 0.1 # Reserve 0.1 HYPE for tx fees
957
+ transferable_hype = hype_balance - tx_fee_reserve
958
+ if transferable_hype > 0.01:
959
+ self.logger.info(
960
+ f"Transferring {transferable_hype:.4f} HYPE to main wallet"
961
+ )
962
+ (
963
+ success,
964
+ msg,
965
+ ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
966
+ query="hyperliquid-hyperevm",
967
+ amount=transferable_hype,
968
+ strategy_name=self.name,
969
+ skip_ledger=False,
970
+ )
971
+ if success:
972
+ transferred_items.append(f"{transferable_hype:.4f} HYPE")
973
+ else:
974
+ self.logger.warning(f"HYPE transfer failed: {msg}")
975
+
976
+ if not transferred_items:
977
+ return (True, "No funds to transfer to main wallet")
978
+
979
+ return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
980
+
938
981
  async def _swap_residual_balances_to_token(
939
982
  self, token_info: dict[str, Any], include_native: bool = False
940
983
  ) -> list[str]: