wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.15__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 (61) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +13 -14
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +73 -32
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
  4. wayfinder_paths/adapters/brap_adapter/README.md +11 -16
  5. wayfinder_paths/adapters/brap_adapter/adapter.py +144 -78
  6. wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
  7. wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
  8. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
  9. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
  10. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
  11. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
  12. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
  13. wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
  14. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
  15. wayfinder_paths/adapters/pool_adapter/README.md +9 -10
  16. wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
  17. wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
  18. wayfinder_paths/adapters/token_adapter/README.md +2 -14
  19. wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
  20. wayfinder_paths/adapters/token_adapter/examples.json +4 -8
  21. wayfinder_paths/adapters/token_adapter/test_adapter.py +9 -7
  22. wayfinder_paths/core/clients/BRAPClient.py +102 -61
  23. wayfinder_paths/core/clients/ClientManager.py +1 -68
  24. wayfinder_paths/core/clients/HyperlendClient.py +125 -64
  25. wayfinder_paths/core/clients/LedgerClient.py +1 -4
  26. wayfinder_paths/core/clients/PoolClient.py +122 -48
  27. wayfinder_paths/core/clients/TokenClient.py +91 -36
  28. wayfinder_paths/core/clients/WalletClient.py +26 -56
  29. wayfinder_paths/core/clients/WayfinderClient.py +28 -160
  30. wayfinder_paths/core/clients/__init__.py +0 -2
  31. wayfinder_paths/core/clients/protocols.py +35 -46
  32. wayfinder_paths/core/clients/sdk_example.py +37 -22
  33. wayfinder_paths/core/constants/erc20_abi.py +0 -11
  34. wayfinder_paths/core/engine/StrategyJob.py +10 -56
  35. wayfinder_paths/core/services/base.py +1 -0
  36. wayfinder_paths/core/services/local_evm_txn.py +25 -9
  37. wayfinder_paths/core/services/local_token_txn.py +2 -6
  38. wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
  39. wayfinder_paths/core/strategies/Strategy.py +16 -4
  40. wayfinder_paths/core/utils/evm_helpers.py +2 -9
  41. wayfinder_paths/policies/erc20.py +1 -1
  42. wayfinder_paths/run_strategy.py +13 -19
  43. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
  44. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
  45. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
  46. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
  47. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
  48. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
  49. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
  50. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
  51. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
  52. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
  53. wayfinder_paths/templates/adapter/README.md +1 -1
  54. wayfinder_paths/templates/strategy/README.md +3 -3
  55. wayfinder_paths/templates/strategy/test_strategy.py +3 -2
  56. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
  57. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
  58. wayfinder_paths/abis/generic/erc20.json +0 -383
  59. wayfinder_paths/core/clients/AuthClient.py +0 -83
  60. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
  61. {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
@@ -57,12 +57,10 @@ class Strategy(ABC):
57
57
  main_wallet: WalletConfig | dict[str, Any] | None = None,
58
58
  strategy_wallet: WalletConfig | dict[str, Any] | None = None,
59
59
  web3_service: Web3Service | None = None,
60
- api_key: str | None = None,
61
60
  ):
62
61
  self.adapters = {}
63
62
  self.ledger_adapter = None
64
63
  self.logger = logger.bind(strategy=self.__class__.__name__)
65
- # Note: api_key is passed to ClientManager, not set in environment
66
64
  self.config = config
67
65
 
68
66
  async def setup(self) -> None:
@@ -145,8 +143,22 @@ class Strategy(ABC):
145
143
  @abstractmethod
146
144
  async def update(self) -> StatusTuple:
147
145
  """
148
- Update strategy positions/rebalance
149
- Returns: (success: bool, message: str)
146
+ Deploy funds to protocols (no main wallet access).
147
+ Called after deposit() has transferred assets to strategy wallet.
148
+
149
+ Returns:
150
+ Tuple of (success: bool, message: str)
151
+ """
152
+ pass
153
+
154
+ @abstractmethod
155
+ async def exit(self, **kwargs) -> StatusTuple:
156
+ """
157
+ Transfer funds from strategy wallet to main wallet.
158
+ Called after withdraw() has liquidated all positions.
159
+
160
+ Returns:
161
+ Tuple of (success: bool, message: str)
150
162
  """
151
163
  pass
152
164
 
@@ -7,7 +7,6 @@ across multiple adapters, extracted from evm_transaction_adapter.
7
7
 
8
8
  import json
9
9
  import os
10
- from pathlib import Path
11
10
  from typing import Any
12
11
 
13
12
  from loguru import logger
@@ -43,12 +42,12 @@ def resolve_chain_id(token_info: dict[str, Any], logger_instance=None) -> int |
43
42
  """
44
43
  log = logger_instance or logger
45
44
  chain_meta = token_info.get("chain") or {}
46
- chain_id = chain_meta.get("chain_id")
45
+ chain_id = chain_meta.get("id")
47
46
  try:
48
47
  if chain_id is not None:
49
48
  return int(chain_id)
50
49
  except (ValueError, TypeError):
51
- log.debug("Invalid chain_id in token_info.chain_id: %s", chain_id)
50
+ log.debug("Invalid chain_id in token_info.chain: %s", chain_id)
52
51
  return chain_code_to_chain_id(chain_meta.get("code"))
53
52
 
54
53
 
@@ -170,9 +169,3 @@ async def get_abi_filtered(
170
169
  if item.get("type") == "function" and item.get("name") in function_names
171
170
  ]
172
171
  return filtered_abi
173
-
174
-
175
- with open(Path(__file__).parent.parent.parent.joinpath("abis/generic/erc20.json")) as f:
176
- erc20_abi_raw = f.read()
177
-
178
- ERC20_ABI = json.loads(erc20_abi_raw)
@@ -1,4 +1,4 @@
1
- from wayfinder_paths.core.utils.evm_helpers import ERC20_ABI
1
+ from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
2
2
 
3
3
 
4
4
  def any_erc20_function(token_address: str) -> dict:
@@ -20,7 +20,6 @@ def load_strategy(
20
20
  strategy_name: str,
21
21
  *,
22
22
  strategy_config: dict | None = None,
23
- api_key: str | None = None,
24
23
  ):
25
24
  """
26
25
  Dynamically load a strategy by name
@@ -28,7 +27,6 @@ def load_strategy(
28
27
  Args:
29
28
  strategy_name: Name of the strategy to load (directory name in strategies/)
30
29
  strategy_config: Configuration dict for the strategy
31
- api_key: Optional API key for service account authentication
32
30
 
33
31
  Returns:
34
32
  Strategy instance
@@ -70,7 +68,7 @@ def load_strategy(
70
68
  if strategy_class is None:
71
69
  raise ValueError(f"No Strategy class found in {module_path}")
72
70
 
73
- return strategy_class(config=strategy_config, api_key=api_key)
71
+ return strategy_class(config=strategy_config)
74
72
 
75
73
 
76
74
  def load_config(
@@ -135,18 +133,15 @@ async def run_strategy(
135
133
  # Load configuration with strategy name for wallet lookup
136
134
  logger.debug(f"Config path provided: {config_path}")
137
135
  config = load_config(config_path, strategy_name=strategy_name)
138
- creds = (
139
- "yes"
140
- if (config.user.username and config.user.password)
141
- or config.user.refresh_token
142
- else "no"
143
- )
144
- main_wallet = config.user.main_wallet_address or "none"
145
- strategy_wallet = config.user.strategy_wallet_address or "none"
146
136
  logger.debug(
147
- f"Loaded config: creds={creds} wallets(main={main_wallet} strategy={strategy_wallet})"
137
+ "Loaded config: wallets(main={} strategy={})",
138
+ config.user.main_wallet_address or "none",
139
+ config.user.strategy_wallet_address or "none",
148
140
  )
149
141
 
142
+ # Validate required configuration
143
+ # Authentication is via system.api_key in config.json
144
+
150
145
  # Load strategy with the enriched config
151
146
  strategy = load_strategy(
152
147
  strategy_name,
@@ -159,13 +154,7 @@ async def run_strategy(
159
154
 
160
155
  # Setup strategy job
161
156
  logger.info("Setting up strategy job...")
162
- auth_mode = (
163
- "credentials"
164
- if (config.user.username and config.user.password)
165
- or config.user.refresh_token
166
- else "missing"
167
- )
168
- logger.debug(f"Auth mode: {auth_mode}")
157
+ logger.debug("Auth mode: API key (from system.api_key)")
169
158
  await strategy_job.setup()
170
159
 
171
160
  # Execute action
@@ -208,6 +197,10 @@ async def run_strategy(
208
197
  result = await strategy_job.execute_strategy("update")
209
198
  logger.info(f"Update result: {result}")
210
199
 
200
+ elif action == "exit":
201
+ result = await strategy_job.execute_strategy("exit")
202
+ logger.info(f"Exit result: {result}")
203
+
211
204
  elif action == "partial-liquidate":
212
205
  usd_value = kwargs.get("amount")
213
206
  if not usd_value:
@@ -303,6 +296,7 @@ def main():
303
296
  "withdraw",
304
297
  "status",
305
298
  "update",
299
+ "exit",
306
300
  "policy",
307
301
  "script",
308
302
  "partial-liquidate",
@@ -203,9 +203,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
203
203
  strategy_wallet: dict[str, Any] | None = None,
204
204
  web3_service: Web3Service | None = None,
205
205
  hyperliquid_executor: HyperliquidExecutor | None = None,
206
- api_key: str | None = None,
207
206
  ) -> None:
208
- super().__init__(api_key=api_key)
207
+ super().__init__()
209
208
 
210
209
  merged_config = dict(config or {})
211
210
  if main_wallet:
@@ -479,7 +478,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
479
478
  gas_ok,
480
479
  gas_res,
481
480
  ) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
482
- token_id="ethereum-arbitrum", # Native ETH on Arbitrum
481
+ query="ethereum-arbitrum", # Native ETH on Arbitrum
483
482
  amount=gas_token_amount,
484
483
  strategy_name=self.name or "basis_trading_strategy",
485
484
  skip_ledger=True,
@@ -500,7 +499,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
500
499
  strategy_balance_ok,
501
500
  strategy_balance,
502
501
  ) = await self.balance_adapter.get_balance(
503
- token_id=USDC_ARBITRUM_TOKEN_ID,
502
+ query=USDC_ARBITRUM_TOKEN_ID,
504
503
  wallet_address=strategy_address,
505
504
  )
506
505
  strategy_usdc = 0.0
@@ -518,7 +517,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
518
517
  move_ok,
519
518
  move_res,
520
519
  ) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
521
- token_id=USDC_ARBITRUM_TOKEN_ID,
520
+ query=USDC_ARBITRUM_TOKEN_ID,
522
521
  amount=need_to_move,
523
522
  strategy_name=self.name or "basis_trading_strategy",
524
523
  skip_ledger=True,
@@ -543,7 +542,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
543
542
 
544
543
  # Send USDC to bridge address (deposit credits the sender address on Hyperliquid)
545
544
  success, result = await self.balance_adapter.send_to_address(
546
- token_id=USDC_ARBITRUM_TOKEN_ID,
545
+ query=USDC_ARBITRUM_TOKEN_ID,
547
546
  amount=main_token_amount,
548
547
  from_wallet=strategy_wallet,
549
548
  to_address=HYPERLIQUID_BRIDGE_ADDRESS,
@@ -821,7 +820,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
821
820
  strategy_usdc = 0.0
822
821
  try:
823
822
  success, balance_data = await self.balance_adapter.get_balance(
824
- token_id=usdc_token_id,
823
+ query=usdc_token_id,
825
824
  wallet_address=address,
826
825
  )
827
826
  if success:
@@ -866,7 +865,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
866
865
  send_success,
867
866
  send_result,
868
867
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
869
- token_id=usdc_token_id,
868
+ query=usdc_token_id,
870
869
  amount=amount_to_send,
871
870
  strategy_name=self.name,
872
871
  skip_ledger=False,
@@ -1011,7 +1010,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1011
1010
  final_balance = 0.0
1012
1011
  try:
1013
1012
  success, balance_data = await self.balance_adapter.get_balance(
1014
- token_id=usdc_token_id,
1013
+ query=usdc_token_id,
1015
1014
  wallet_address=address,
1016
1015
  )
1017
1016
  if success:
@@ -1032,7 +1031,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1032
1031
  send_success,
1033
1032
  send_result,
1034
1033
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
1035
- token_id=usdc_token_id,
1034
+ query=usdc_token_id,
1036
1035
  amount=amount_to_send,
1037
1036
  strategy_name=self.name,
1038
1037
  skip_ledger=False, # Record in ledger
@@ -1073,6 +1072,73 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
1073
1072
  f"Withdrew ${total_withdrawn:.2f} total to main wallet ({main_address}).",
1074
1073
  )
1075
1074
 
1075
+ async def exit(self, **kwargs) -> StatusTuple:
1076
+ """Transfer funds from strategy wallet to main wallet."""
1077
+ self.logger.info("EXIT: Transferring remaining funds to main wallet")
1078
+
1079
+ strategy_address = self._get_strategy_wallet_address()
1080
+ main_address = self._get_main_wallet_address()
1081
+
1082
+ if strategy_address.lower() == main_address.lower():
1083
+ return (True, "Main wallet is strategy wallet, no transfer needed")
1084
+
1085
+ transferred_items = []
1086
+
1087
+ # Transfer USDC to main wallet
1088
+ usdc_ok, usdc_raw = await self.balance_adapter.get_balance(
1089
+ token_id=USDC_ARBITRUM_TOKEN_ID,
1090
+ wallet_address=strategy_address,
1091
+ )
1092
+ if usdc_ok and usdc_raw:
1093
+ usdc_balance = float(usdc_raw.get("balance", 0))
1094
+ if usdc_balance > 1.0:
1095
+ self.logger.info(f"Transferring {usdc_balance:.2f} USDC to main wallet")
1096
+ (
1097
+ success,
1098
+ msg,
1099
+ ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
1100
+ query=USDC_ARBITRUM_TOKEN_ID,
1101
+ amount=usdc_balance,
1102
+ strategy_name=self.name,
1103
+ skip_ledger=False,
1104
+ )
1105
+ if success:
1106
+ transferred_items.append(f"{usdc_balance:.2f} USDC")
1107
+ else:
1108
+ self.logger.warning(f"USDC transfer failed: {msg}")
1109
+
1110
+ # Transfer ETH (minus reserve for tx fees) to main wallet
1111
+ eth_ok, eth_raw = await self.balance_adapter.get_balance(
1112
+ token_id="ethereum-arbitrum",
1113
+ wallet_address=strategy_address,
1114
+ )
1115
+ if eth_ok and eth_raw:
1116
+ eth_balance = float(eth_raw.get("balance", 0))
1117
+ tx_fee_reserve = 0.0002
1118
+ transferable_eth = eth_balance - tx_fee_reserve
1119
+ if transferable_eth > 0.0001:
1120
+ self.logger.info(
1121
+ f"Transferring {transferable_eth:.6f} ETH to main wallet"
1122
+ )
1123
+ (
1124
+ success,
1125
+ msg,
1126
+ ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
1127
+ query="ethereum-arbitrum",
1128
+ amount=transferable_eth,
1129
+ strategy_name=self.name,
1130
+ skip_ledger=False,
1131
+ )
1132
+ if success:
1133
+ transferred_items.append(f"{transferable_eth:.6f} ETH")
1134
+ else:
1135
+ self.logger.warning(f"ETH transfer failed: {msg}")
1136
+
1137
+ if not transferred_items:
1138
+ return (True, "No funds to transfer to main wallet")
1139
+
1140
+ return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
1141
+
1076
1142
  async def _status(self) -> StatusDict:
1077
1143
  """Return portfolio value and strategy status with live data."""
1078
1144
  total_value, hl_value, vault_value = await self._get_total_portfolio_value()
@@ -2569,7 +2635,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
2569
2635
  try:
2570
2636
  strategy_address = self._get_strategy_wallet_address()
2571
2637
  success, balance = await self.balance_adapter.get_balance(
2572
- token_id=USDC_ARBITRUM_TOKEN_ID,
2638
+ query=USDC_ARBITRUM_TOKEN_ID,
2573
2639
  wallet_address=strategy_address,
2574
2640
  )
2575
2641
  if success and balance:
@@ -461,7 +461,7 @@ class TestBasisTradingStrategy:
461
461
  # Try to scale with $5 (below $10 minimum notional)
462
462
  # With 2x leverage, order_usd = 5 * (2/3) = 3.33, below $10
463
463
  success, msg = await strategy._scale_up_position(5.0)
464
- assert success is True # Returns success=True but with message
464
+ assert success # Returns success=True but with message
465
465
  assert "below minimum notional" in msg
466
466
 
467
467
  @pytest.mark.asyncio
@@ -563,7 +563,7 @@ class TestBasisTradingStrategy:
563
563
 
564
564
  # Should have called fill_pair_units to scale up
565
565
  assert mock_filler.fill_pair_units.called
566
- assert success is True
566
+ assert success
567
567
 
568
568
  @pytest.mark.asyncio
569
569
  async def test_ensure_builder_fee_approved_already_approved(
@@ -612,7 +612,7 @@ class TestBasisTradingStrategy:
612
612
  )
613
613
 
614
614
  success, msg = await s.ensure_builder_fee_approved()
615
- assert success is True
615
+ assert success
616
616
  assert "already approved" in msg.lower()
617
617
  # Should not have called approve_builder_fee
618
618
  mock_hyperliquid_adapter.approve_builder_fee.assert_not_called()
@@ -661,7 +661,7 @@ class TestBasisTradingStrategy:
661
661
  )
662
662
 
663
663
  success, msg = await s.ensure_builder_fee_approved()
664
- assert success is True
664
+ assert success
665
665
  assert "approved" in msg.lower()
666
666
  # Should have called approve_builder_fee
667
667
  mock_hyperliquid_adapter.approve_builder_fee.assert_called_once()
@@ -832,7 +832,7 @@ class TestBasisTradingStrategy:
832
832
  mock_filler_class.return_value = mock_filler
833
833
 
834
834
  success, _ = await strategy.update()
835
- assert success is True
835
+ assert success
836
836
 
837
837
  # Target spot was $66.67, so we should transfer $33.33 spot->perp.
838
838
  mock_hyperliquid_adapter.transfer_spot_to_perp.assert_called_once()
@@ -907,7 +907,7 @@ class TestBasisTradingStrategy:
907
907
  strategy._find_and_open_position = AsyncMock(return_value=(True, "redeployed"))
908
908
 
909
909
  success, msg = await strategy.update()
910
- assert success is True
910
+ assert success
911
911
  assert msg == "redeployed"
912
912
  strategy._close_position.assert_awaited_once()
913
913
  strategy._find_and_open_position.assert_awaited_once()
@@ -197,9 +197,8 @@ class HyperlendStableYieldStrategy(Strategy):
197
197
  main_wallet: dict[str, Any] | None = None,
198
198
  strategy_wallet: dict[str, Any] | None = None,
199
199
  web3_service: Web3Service = None,
200
- api_key: str | None = None,
201
200
  ):
202
- super().__init__(api_key=api_key)
201
+ super().__init__()
203
202
  merged_config: dict[str, Any] = dict(config or {})
204
203
  if main_wallet is not None:
205
204
  merged_config["main_wallet"] = main_wallet
@@ -350,7 +349,7 @@ class HyperlendStableYieldStrategy(Strategy):
350
349
  success,
351
350
  main_usdt0_balance,
352
351
  ) = await self.balance_adapter.get_balance(
353
- token_id=self.usdt_token_info.get("token_id"),
352
+ query=self.usdt_token_info.get("token_id"),
354
353
  wallet_address=self._get_main_wallet_address(),
355
354
  )
356
355
  if not success:
@@ -363,7 +362,7 @@ class HyperlendStableYieldStrategy(Strategy):
363
362
  success,
364
363
  main_hype_balance,
365
364
  ) = await self.balance_adapter.get_balance(
366
- token_id=self.hype_token_info.get("token_id"),
365
+ query=self.hype_token_info.get("token_id"),
367
366
  wallet_address=self._get_main_wallet_address(),
368
367
  )
369
368
  if not success:
@@ -530,11 +529,10 @@ class HyperlendStableYieldStrategy(Strategy):
530
529
  return self._assets_snapshot
531
530
 
532
531
  _, snapshot = await self.hyperlend_adapter.get_assets_view(
533
- chain_id=self.hype_token_info.get("chain").get("id"),
534
532
  user_address=self._get_strategy_wallet_address(),
535
533
  )
536
534
 
537
- assets = snapshot.get("assets_view", {}).get("assets", [])
535
+ assets = snapshot.get("assets", [])
538
536
  asset_map = {}
539
537
 
540
538
  for asset in assets:
@@ -582,11 +580,9 @@ class HyperlendStableYieldStrategy(Strategy):
582
580
 
583
581
  try:
584
582
  _, data = await self.hyperlend_adapter.get_stable_markets(
585
- chain_id=self.hype_token_info.get("chain").get("id"),
586
583
  required_underlying_tokens=required_tokens,
587
584
  buffer_bps=self.SUPPLY_CAP_BUFFER_BPS,
588
585
  min_buffer_tokens=self.SUPPLY_CAP_MIN_BUFFER_TOKENS,
589
- is_stable_symbol=True,
590
586
  )
591
587
  markets = data.get("markets", {}) if isinstance(data, dict) else {}
592
588
  except Exception:
@@ -609,7 +605,7 @@ class HyperlendStableYieldStrategy(Strategy):
609
605
  async def _get_lent_positions(self, snapshot=None) -> dict[str, dict[str, Any]]:
610
606
  if not snapshot:
611
607
  snapshot = await self._get_assets_snapshot()
612
- assets = snapshot.get("assets_view", {}).get("assets", None)
608
+ assets = snapshot.get("assets", None)
613
609
 
614
610
  if not assets:
615
611
  return {}
@@ -630,7 +626,14 @@ class HyperlendStableYieldStrategy(Strategy):
630
626
  continue
631
627
 
632
628
  try:
633
- success, token = await self.token_adapter.get_token(checksum)
629
+ chain_id = None
630
+ try:
631
+ chain_id = int((self.hype_token_info.get("chain") or {}).get("id"))
632
+ except Exception:
633
+ chain_id = None
634
+ success, token = await self.token_adapter.get_token(
635
+ checksum, chain_id=chain_id
636
+ )
634
637
  if not success or not isinstance(token, dict):
635
638
  logger.info(f"Error getting token for asset: {asset}")
636
639
  continue
@@ -862,7 +865,7 @@ class HyperlendStableYieldStrategy(Strategy):
862
865
 
863
866
  try:
864
867
  _, total_usdt_wei = await self.balance_adapter.get_balance(
865
- token_id=self.usdt_token_info.get("token_id"),
868
+ query=self.usdt_token_info.get("token_id"),
866
869
  wallet_address=self._get_strategy_wallet_address(),
867
870
  )
868
871
  except Exception:
@@ -891,7 +894,7 @@ class HyperlendStableYieldStrategy(Strategy):
891
894
 
892
895
  try:
893
896
  _, total_hype_wei = await self.balance_adapter.get_balance(
894
- token_id=self.hype_token_info.get("token_id"),
897
+ query=self.hype_token_info.get("token_id"),
895
898
  wallet_address=self._get_strategy_wallet_address(),
896
899
  )
897
900
  except Exception:
@@ -932,6 +935,75 @@ class HyperlendStableYieldStrategy(Strategy):
932
935
 
933
936
  return (True, ". ".join(messages))
934
937
 
938
+ async def exit(self, **kwargs) -> StatusTuple:
939
+ """Transfer funds from strategy wallet to main wallet."""
940
+ self.logger.info("EXIT: Transferring remaining funds to main wallet")
941
+
942
+ strategy_address = self._get_strategy_wallet_address()
943
+ main_address = self._get_main_wallet_address()
944
+
945
+ if strategy_address.lower() == main_address.lower():
946
+ return (True, "Main wallet is strategy wallet, no transfer needed")
947
+
948
+ transferred_items = []
949
+
950
+ # Transfer USDT0 to main wallet
951
+ usdt_ok, usdt_raw = await self.balance_adapter.get_balance(
952
+ token_id="usdt0-hyperevm",
953
+ wallet_address=strategy_address,
954
+ )
955
+ if usdt_ok and usdt_raw:
956
+ usdt_balance = float(usdt_raw.get("balance", 0))
957
+ if usdt_balance > 1.0:
958
+ self.logger.info(
959
+ f"Transferring {usdt_balance:.2f} USDT0 to main wallet"
960
+ )
961
+ (
962
+ success,
963
+ msg,
964
+ ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
965
+ query="usdt0-hyperevm",
966
+ amount=usdt_balance,
967
+ strategy_name=self.name,
968
+ skip_ledger=False,
969
+ )
970
+ if success:
971
+ transferred_items.append(f"{usdt_balance:.2f} USDT0")
972
+ else:
973
+ self.logger.warning(f"USDT0 transfer failed: {msg}")
974
+
975
+ # Transfer HYPE (minus reserve for tx fees) to main wallet
976
+ hype_ok, hype_raw = await self.balance_adapter.get_balance(
977
+ token_id="hyperliquid-hyperevm",
978
+ wallet_address=strategy_address,
979
+ )
980
+ if hype_ok and hype_raw:
981
+ hype_balance = float(hype_raw.get("balance", 0))
982
+ tx_fee_reserve = 0.1 # Reserve 0.1 HYPE for tx fees
983
+ transferable_hype = hype_balance - tx_fee_reserve
984
+ if transferable_hype > 0.01:
985
+ self.logger.info(
986
+ f"Transferring {transferable_hype:.4f} HYPE to main wallet"
987
+ )
988
+ (
989
+ success,
990
+ msg,
991
+ ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
992
+ query="hyperliquid-hyperevm",
993
+ amount=transferable_hype,
994
+ strategy_name=self.name,
995
+ skip_ledger=False,
996
+ )
997
+ if success:
998
+ transferred_items.append(f"{transferable_hype:.4f} HYPE")
999
+ else:
1000
+ self.logger.warning(f"HYPE transfer failed: {msg}")
1001
+
1002
+ if not transferred_items:
1003
+ return (True, "No funds to transfer to main wallet")
1004
+
1005
+ return (True, f"Transferred to main wallet: {', '.join(transferred_items)}")
1006
+
935
1007
  async def _swap_residual_balances_to_token(
936
1008
  self, token_info: dict[str, Any], include_native: bool = False
937
1009
  ) -> list[str]:
@@ -987,7 +1059,7 @@ class HyperlendStableYieldStrategy(Strategy):
987
1059
  success,
988
1060
  message,
989
1061
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
990
- token_id=token_info.get("token_id"),
1062
+ query=token_info.get("token_id"),
991
1063
  amount=amount_tokens,
992
1064
  strategy_name=self.name,
993
1065
  )
@@ -1549,11 +1621,9 @@ class HyperlendStableYieldStrategy(Strategy):
1549
1621
  )
1550
1622
 
1551
1623
  _, stable_markets = await self.hyperlend_adapter.get_stable_markets(
1552
- chain_id=self.hype_token_info.get("chain").get("id"),
1553
1624
  required_underlying_tokens=required_underlying_tokens,
1554
1625
  buffer_bps=self.SUPPLY_CAP_BUFFER_BPS,
1555
1626
  min_buffer_tokens=self.SUPPLY_CAP_MIN_BUFFER_TOKENS,
1556
- is_stable_symbol=True,
1557
1627
  )
1558
1628
  filtered_notes = stable_markets.get("notes", [])
1559
1629
  filtered_map = stable_markets.get("markets", {})
@@ -1585,8 +1655,7 @@ class HyperlendStableYieldStrategy(Strategy):
1585
1655
  if current_checksum_lower not in existing_addresses:
1586
1656
  try:
1587
1657
  _, current_entry = await self.hyperlend_adapter.get_market_entry(
1588
- chain_id=self.hype_token_info.get("chain").get("id"),
1589
- token_address=current_checksum_value,
1658
+ token=current_checksum_value,
1590
1659
  )
1591
1660
  except Exception:
1592
1661
  current_entry = None
@@ -1633,8 +1702,7 @@ class HyperlendStableYieldStrategy(Strategy):
1633
1702
  histories = await asyncio.gather(
1634
1703
  *[
1635
1704
  self.hyperlend_adapter.get_lend_rate_history(
1636
- chain_id=self.hype_token_info.get("chain").get("id"),
1637
- token_address=addr,
1705
+ token=addr,
1638
1706
  lookback_hours=lookback_hours,
1639
1707
  )
1640
1708
  for addr, _ in filtered
@@ -1656,7 +1724,7 @@ class HyperlendStableYieldStrategy(Strategy):
1656
1724
  if not history_status:
1657
1725
  continue
1658
1726
  history_data = history[1]
1659
- for row in history_data.get("rate_history", []):
1727
+ for row in history_data.get("history", []):
1660
1728
  ts_ms = row.get("timestamp_ms")
1661
1729
  if ts_ms is None:
1662
1730
  continue
@@ -1812,7 +1880,14 @@ class HyperlendStableYieldStrategy(Strategy):
1812
1880
  token = None
1813
1881
  if address:
1814
1882
  try:
1815
- success, token = await self.token_adapter.get_token(address.lower())
1883
+ chain_id = None
1884
+ try:
1885
+ chain_id = int((self.hype_token_info.get("chain") or {}).get("id"))
1886
+ except Exception:
1887
+ chain_id = None
1888
+ success, token = await self.token_adapter.get_token(
1889
+ address.lower(), chain_id=chain_id
1890
+ )
1816
1891
  except Exception:
1817
1892
  token = None
1818
1893
  if not success:
@@ -2128,7 +2203,16 @@ class HyperlendStableYieldStrategy(Strategy):
2128
2203
  continue
2129
2204
 
2130
2205
  try:
2131
- success, token = await self.token_adapter.get_token(checksum)
2206
+ chain_id = None
2207
+ try:
2208
+ chain_id = int(
2209
+ (self.hype_token_info.get("chain") or {}).get("id")
2210
+ )
2211
+ except Exception:
2212
+ chain_id = None
2213
+ success, token = await self.token_adapter.get_token(
2214
+ checksum, chain_id=chain_id
2215
+ )
2132
2216
  if not success or not isinstance(token, dict):
2133
2217
  continue
2134
2218
  except Exception:
@@ -2239,7 +2323,7 @@ class HyperlendStableYieldStrategy(Strategy):
2239
2323
  success,
2240
2324
  strategy_hype_balance_wei,
2241
2325
  ) = await self.balance_adapter.get_balance(
2242
- token_id=self.hype_token_info.get("token_id"),
2326
+ query=self.hype_token_info.get("token_id"),
2243
2327
  wallet_address=self._get_strategy_wallet_address(),
2244
2328
  )
2245
2329
  hype_price = asset_map.get(WRAPPED_HYPE_ADDRESS, {}).get("price_usd") or 0.0