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
@@ -18,6 +18,7 @@ from wayfinder_paths.adapters.hyperlend_adapter.adapter import HyperlendAdapter
18
18
  from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
19
19
  from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
20
20
  from wayfinder_paths.core.constants.base import DEFAULT_SLIPPAGE
21
+ from wayfinder_paths.core.constants.contracts import HYPEREVM_WHYPE
21
22
  from wayfinder_paths.core.strategies.descriptors import (
22
23
  Complexity,
23
24
  Directionality,
@@ -41,6 +42,7 @@ from wayfinder_paths.policies.hyperliquid import (
41
42
  any_hyperliquid_l1_payload,
42
43
  any_hyperliquid_user_payload,
43
44
  )
45
+ from wayfinder_paths.policies.lifi import LIFI_ROUTERS, lifi_swap
44
46
  from wayfinder_paths.policies.prjx import PRJX_ROUTER, prjx_swap
45
47
 
46
48
  SYMBOL_TRANSLATION_TABLE = str.maketrans(
@@ -50,7 +52,7 @@ SYMBOL_TRANSLATION_TABLE = str.maketrans(
50
52
  "Ξ": "X",
51
53
  }
52
54
  )
53
- WRAPPED_HYPE_ADDRESS = "0x5555555555555555555555555555555555555555"
55
+ WRAPPED_HYPE_ADDRESS = HYPEREVM_WHYPE
54
56
 
55
57
 
56
58
  class HyperlendStableYieldStrategy(Strategy):
@@ -245,15 +247,6 @@ class HyperlendStableYieldStrategy(Strategy):
245
247
  strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
246
248
  )
247
249
 
248
- self.register_adapters(
249
- [
250
- balance,
251
- token_adapter,
252
- ledger_adapter,
253
- brap_adapter,
254
- hyperlend_adapter,
255
- ]
256
- )
257
250
  self.balance_adapter = balance
258
251
  self.token_adapter = token_adapter
259
252
  self.ledger_adapter = ledger_adapter
@@ -338,7 +331,7 @@ class HyperlendStableYieldStrategy(Strategy):
338
331
  success,
339
332
  main_usdt0_balance,
340
333
  ) = await self.balance_adapter.get_balance(
341
- query=self.usdt_token_info.get("token_id"),
334
+ token_id=self.usdt_token_info.get("token_id"),
342
335
  wallet_address=self._get_main_wallet_address(),
343
336
  )
344
337
  if not success:
@@ -351,7 +344,7 @@ class HyperlendStableYieldStrategy(Strategy):
351
344
  success,
352
345
  main_hype_balance,
353
346
  ) = await self.balance_adapter.get_balance(
354
- query=self.hype_token_info.get("token_id"),
347
+ token_id=self.hype_token_info.get("token_id"),
355
348
  wallet_address=self._get_main_wallet_address(),
356
349
  )
357
350
  if not success:
@@ -839,7 +832,7 @@ class HyperlendStableYieldStrategy(Strategy):
839
832
  total_usdt = 0.0
840
833
  try:
841
834
  _, total_usdt_wei = await self.balance_adapter.get_balance(
842
- query=self.usdt_token_info.get("token_id"),
835
+ token_id=self.usdt_token_info.get("token_id"),
843
836
  wallet_address=self._get_strategy_wallet_address(),
844
837
  )
845
838
  if total_usdt_wei and total_usdt_wei > 0:
@@ -852,7 +845,7 @@ class HyperlendStableYieldStrategy(Strategy):
852
845
  total_hype = 0.0
853
846
  try:
854
847
  _, total_hype_wei = await self.balance_adapter.get_balance(
855
- query=self.hype_token_info.get("token_id"),
848
+ token_id=self.hype_token_info.get("token_id"),
856
849
  wallet_address=self._get_strategy_wallet_address(),
857
850
  )
858
851
  if total_hype_wei and total_hype_wei > 0:
@@ -865,7 +858,6 @@ class HyperlendStableYieldStrategy(Strategy):
865
858
  if sweep_actions:
866
859
  messages.append(f"Residual sweeps: {'; '.join(sweep_actions)}.")
867
860
 
868
- # Report balances in strategy wallet
869
861
  balance_parts = []
870
862
  if total_usdt > 0:
871
863
  balance_parts.append(
@@ -902,13 +894,12 @@ class HyperlendStableYieldStrategy(Strategy):
902
894
 
903
895
  transferred_items = []
904
896
 
905
- # Transfer USDT0 to main wallet
906
897
  usdt_ok, usdt_raw = await self.balance_adapter.get_balance(
907
898
  token_id="usdt0-hyperevm",
908
899
  wallet_address=strategy_address,
909
900
  )
910
901
  if usdt_ok and usdt_raw:
911
- usdt_balance = float(usdt_raw.get("balance", 0))
902
+ usdt_balance = float(usdt_raw) / 1e6 # USDT has 6 decimals
912
903
  if usdt_balance > 1.0:
913
904
  self.logger.info(
914
905
  f"Transferring {usdt_balance:.2f} USDT0 to main wallet"
@@ -917,7 +908,7 @@ class HyperlendStableYieldStrategy(Strategy):
917
908
  success,
918
909
  msg,
919
910
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
920
- query="usdt0-hyperevm",
911
+ token_id="usdt0-hyperevm",
921
912
  amount=usdt_balance,
922
913
  strategy_name=self.name,
923
914
  skip_ledger=False,
@@ -927,13 +918,12 @@ class HyperlendStableYieldStrategy(Strategy):
927
918
  else:
928
919
  self.logger.warning(f"USDT0 transfer failed: {msg}")
929
920
 
930
- # Transfer HYPE (minus reserve for tx fees) to main wallet
931
921
  hype_ok, hype_raw = await self.balance_adapter.get_balance(
932
922
  token_id="hyperliquid-hyperevm",
933
923
  wallet_address=strategy_address,
934
924
  )
935
925
  if hype_ok and hype_raw:
936
- hype_balance = float(hype_raw.get("balance", 0))
926
+ hype_balance = float(hype_raw) / 1e18 # HYPE has 18 decimals
937
927
  tx_fee_reserve = 0.1
938
928
  transferable_hype = hype_balance - tx_fee_reserve
939
929
  if transferable_hype > 0.01:
@@ -944,7 +934,7 @@ class HyperlendStableYieldStrategy(Strategy):
944
934
  success,
945
935
  msg,
946
936
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
947
- query="hyperliquid-hyperevm",
937
+ token_id="hyperliquid-hyperevm",
948
938
  amount=transferable_hype,
949
939
  strategy_name=self.name,
950
940
  skip_ledger=False,
@@ -1014,7 +1004,7 @@ class HyperlendStableYieldStrategy(Strategy):
1014
1004
  success,
1015
1005
  message,
1016
1006
  ) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
1017
- query=token_info.get("token_id"),
1007
+ token_id=token_info.get("token_id"),
1018
1008
  amount=amount_tokens,
1019
1009
  strategy_name=self.name,
1020
1010
  )
@@ -2275,7 +2265,7 @@ class HyperlendStableYieldStrategy(Strategy):
2275
2265
  success,
2276
2266
  strategy_hype_balance_wei,
2277
2267
  ) = await self.balance_adapter.get_balance(
2278
- query=self.hype_token_info.get("token_id"),
2268
+ token_id=self.hype_token_info.get("token_id"),
2279
2269
  wallet_address=self._get_strategy_wallet_address(),
2280
2270
  )
2281
2271
  hype_price = asset_map.get(WRAPPED_HYPE_ADDRESS, {}).get("price_usd") or 0.0
@@ -2381,4 +2371,6 @@ class HyperlendStableYieldStrategy(Strategy):
2381
2371
  await enso_swap(),
2382
2372
  erc20_spender_for_any_token(PRJX_ROUTER),
2383
2373
  await prjx_swap(),
2374
+ erc20_spender_for_any_token(LIFI_ROUTERS[999]),
2375
+ await lifi_swap(999),
2384
2376
  ]
@@ -1,36 +1,15 @@
1
- import sys
2
1
  from pathlib import Path
3
2
  from unittest.mock import AsyncMock
4
3
 
5
- # Ensure wayfinder-paths is on path for tests.test_utils import
6
- # This is a workaround until conftest loading order is resolved
7
- _wayfinder_path_dir = Path(__file__).parent.parent.parent.resolve()
8
- _wayfinder_path_str = str(_wayfinder_path_dir)
9
- if _wayfinder_path_str not in sys.path:
10
- sys.path.insert(0, _wayfinder_path_str)
11
- elif sys.path.index(_wayfinder_path_str) > 0:
12
- # Move to front to take precedence
13
- sys.path.remove(_wayfinder_path_str)
14
- sys.path.insert(0, _wayfinder_path_str)
15
-
16
- import pytest # noqa: E402
17
-
18
- try:
19
- from tests.test_utils import get_canonical_examples, load_strategy_examples
20
- except ImportError:
21
- # Fallback if path setup didn't work
22
- import importlib.util
23
-
24
- test_utils_path = Path(_wayfinder_path_dir) / "tests" / "test_utils.py"
25
- spec = importlib.util.spec_from_file_location("tests.test_utils", test_utils_path)
26
- test_utils = importlib.util.module_from_spec(spec)
27
- spec.loader.exec_module(test_utils)
28
- get_canonical_examples = test_utils.get_canonical_examples
29
- load_strategy_examples = test_utils.load_strategy_examples
30
-
31
- from wayfinder_paths.strategies.hyperlend_stable_yield_strategy.strategy import ( # noqa: E402
4
+ import pytest
5
+
6
+ from wayfinder_paths.strategies.hyperlend_stable_yield_strategy.strategy import (
32
7
  HyperlendStableYieldStrategy,
33
8
  )
9
+ from wayfinder_paths.tests.test_utils import (
10
+ get_canonical_examples,
11
+ load_strategy_examples,
12
+ )
34
13
 
35
14
 
36
15
  @pytest.fixture
@@ -48,10 +27,9 @@ def strategy():
48
27
 
49
28
  if hasattr(s, "balance_adapter") and s.balance_adapter:
50
29
  # Mock balances: 1000 USDT0 (with 6 decimals) and 2 HYPE (with 18 decimals)
51
- def get_balance_side_effect(query, wallet_address, **kwargs):
52
- token_id = (
53
- query if isinstance(query, str) else (query or {}).get("token_id")
54
- )
30
+ def get_balance_side_effect(
31
+ *, wallet_address, token_id=None, token_address=None, chain_id=None
32
+ ):
55
33
  token_id_str = str(token_id).lower() if token_id else ""
56
34
  if "usdt0" in token_id_str or token_id_str == "usdt0":
57
35
  # 1000 USDT0 with 6 decimals = 1000 * 10^6 = 1000000000
@@ -102,7 +80,6 @@ def strategy():
102
80
  )
103
81
 
104
82
  if hasattr(s, "balance_adapter") and s.balance_adapter:
105
- # Mock the main methods first
106
83
  s.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
107
84
  return_value=(True, "Transfer successful (simulated)")
108
85
  )
@@ -145,9 +122,7 @@ def strategy():
145
122
  )
146
123
 
147
124
  if hasattr(s, "ledger_adapter") and s.ledger_adapter:
148
- s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
149
- return_value=(True, {"net_deposit": 0})
150
- )
125
+ s.ledger_adapter.get_strategy_net_deposit = AsyncMock(return_value=(True, 0.0))
151
126
  s.ledger_adapter.get_strategy_transactions = AsyncMock(
152
127
  return_value=(True, {"transactions": []})
153
128
  )
@@ -155,46 +130,36 @@ def strategy():
155
130
  if hasattr(s, "brap_adapter") and s.brap_adapter:
156
131
  usdt0_address = "0x1234567890123456789012345678901234567890"
157
132
 
158
- def get_swap_quote_side_effect(*args, **kwargs):
133
+ def best_quote_side_effect(*args, **kwargs):
159
134
  to_token_address = kwargs.get("to_token_address", "")
160
135
  if to_token_address == usdt0_address:
161
- return (
162
- True,
163
- {
164
- "quotes": {
165
- "best_quote": {
166
- "output_amount": "99900000",
167
- }
168
- }
169
- },
170
- )
136
+ return (True, {"output_amount": "99900000"})
171
137
  return (
172
138
  True,
173
139
  {
174
- "quotes": {
175
- "best_quote": {
176
- "output_amount": "105000000",
177
- "input_amount": "50000000000000",
178
- "toAmount": "105000000",
179
- "estimatedGas": "1000000000",
180
- "fromAmount": "100000000",
181
- "fromToken": {"symbol": "USDT0"},
182
- "toToken": {"symbol": "HYPE"},
183
- }
184
- }
140
+ "output_amount": "105000000",
141
+ "input_amount": "50000000000000",
142
+ "toAmount": "105000000",
143
+ "estimatedGas": "1000000000",
144
+ "fromAmount": "100000000",
145
+ "fromToken": {"symbol": "USDT0"},
146
+ "toToken": {"symbol": "HYPE"},
185
147
  },
186
148
  )
187
149
 
188
- s.brap_adapter.get_swap_quote = AsyncMock(
189
- side_effect=get_swap_quote_side_effect
190
- )
150
+ s.brap_adapter.best_quote = AsyncMock(side_effect=best_quote_side_effect)
191
151
 
192
152
  if (
193
153
  hasattr(s, "brap_adapter")
194
154
  and s.brap_adapter
195
155
  and hasattr(s.brap_adapter, "swap_from_quote")
196
156
  ):
197
- s.brap_adapter.swap_from_quote = AsyncMock(return_value=None)
157
+ s.brap_adapter.swap_from_quote = AsyncMock(
158
+ return_value=(
159
+ True,
160
+ {"tx_hash": "0xmockhash", "from_amount": "100", "to_amount": "99"},
161
+ )
162
+ )
198
163
 
199
164
  if hasattr(s, "hyperlend_adapter") and s.hyperlend_adapter:
200
165
  s.hyperlend_adapter.get_assets_view = AsyncMock(
@@ -13,7 +13,17 @@ from wayfinder_paths.adapters.brap_adapter.adapter import BRAPAdapter
13
13
  from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
14
14
  from wayfinder_paths.adapters.moonwell_adapter.adapter import MoonwellAdapter
15
15
  from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
16
+ from wayfinder_paths.core.constants.chains import CHAIN_ID_BASE
17
+ from wayfinder_paths.core.constants.contracts import BASE_USDC, BASE_WSTETH
16
18
  from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
19
+ from wayfinder_paths.core.constants.tokens import (
20
+ TOKEN_ID_ETH_BASE,
21
+ TOKEN_ID_STETH,
22
+ TOKEN_ID_USDC_BASE,
23
+ TOKEN_ID_WELL_BASE,
24
+ TOKEN_ID_WETH_BASE,
25
+ TOKEN_ID_WSTETH_BASE,
26
+ )
17
27
  from wayfinder_paths.core.strategies.descriptors import (
18
28
  Complexity,
19
29
  Directionality,
@@ -38,28 +48,22 @@ from wayfinder_paths.policies.moonwell import (
38
48
  weth_deposit,
39
49
  )
40
50
 
41
- # Token addresses on Base
42
- USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
43
- WSTETH = "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452"
44
-
45
- # Token IDs
46
- USDC_TOKEN_ID = "usd-coin-base"
47
- WETH_TOKEN_ID = "l2-standard-bridged-weth-base-base"
48
- WSTETH_TOKEN_ID = "superbridge-bridged-wsteth-base-base"
49
- ETH_TOKEN_ID = "ethereum-base"
50
- WELL_TOKEN_ID = "moonwell-artemis-base"
51
- STETH_TOKEN_ID = "staked-ether-ethereum"
51
+ USDC = BASE_USDC
52
+ WSTETH = BASE_WSTETH
52
53
 
53
- # Base chain ID
54
- BASE_CHAIN_ID = 8453
54
+ USDC_TOKEN_ID = TOKEN_ID_USDC_BASE
55
+ WETH_TOKEN_ID = TOKEN_ID_WETH_BASE
56
+ WSTETH_TOKEN_ID = TOKEN_ID_WSTETH_BASE
57
+ ETH_TOKEN_ID = TOKEN_ID_ETH_BASE
58
+ WELL_TOKEN_ID = TOKEN_ID_WELL_BASE
59
+ STETH_TOKEN_ID = TOKEN_ID_STETH
55
60
 
56
- # Safety parameters
57
- # 0.98 = 2% safety margin when borrowing to avoid hitting exact liquidation threshold
61
+ BASE_CHAIN_ID = CHAIN_ID_BASE
58
62
  COLLATERAL_SAFETY_FACTOR = 0.98
59
63
 
60
64
 
61
65
  class SwapOutcomeUnknownError(RuntimeError):
62
- "Raised when the outcome of a swap operation is unknown."
66
+ pass
63
67
 
64
68
 
65
69
  class MoonwellWstethLoopStrategy(Strategy):
@@ -192,14 +196,12 @@ class MoonwellWstethLoopStrategy(Strategy):
192
196
 
193
197
  self.config = merged_config
194
198
 
195
- # Adapter references
196
199
  self.balance_adapter: BalanceAdapter | None = None
197
200
  self.moonwell_adapter: MoonwellAdapter | None = None
198
201
  self.brap_adapter: BRAPAdapter | None = None
199
202
  self.token_adapter: TokenAdapter | None = None
200
203
  self.ledger_adapter: LedgerAdapter | None = None
201
204
 
202
- # Token info cache
203
205
  self._token_info_cache: dict[str, dict] = {}
204
206
  self._token_price_cache: dict[str, float] = {}
205
207
  self._token_price_timestamps: dict[str, float] = {}
@@ -235,16 +237,6 @@ class MoonwellWstethLoopStrategy(Strategy):
235
237
  strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
236
238
  )
237
239
 
238
- self.register_adapters(
239
- [
240
- balance,
241
- token_adapter,
242
- ledger_adapter,
243
- brap_adapter,
244
- moonwell_adapter,
245
- ]
246
- )
247
-
248
240
  self.balance_adapter = balance
249
241
  self.token_adapter = token_adapter
250
242
  self.ledger_adapter = ledger_adapter
@@ -373,11 +365,9 @@ class MoonwellWstethLoopStrategy(Strategy):
373
365
  int((weth_pos or {}).get("borrow_balance", 0) or 0) if weth_pos_ok else 0
374
366
  )
375
367
 
376
- # Gas reserve
377
368
  gas_keep_wei = int(self._gas_keep_wei())
378
369
  eth_usable_wei = max(0, int(wallet_eth) - int(gas_keep_wei))
379
370
 
380
- # USD conversions
381
371
  def _usd(raw: int, price: float, dec: int) -> float:
382
372
  if raw <= 0 or not price or price <= 0:
383
373
  return 0.0
@@ -1554,7 +1544,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1554
1544
  if self.token_adapter is None:
1555
1545
  raise RuntimeError("Token adapter not initialized.")
1556
1546
 
1557
- # Pre-fetch token info
1558
1547
  for token_id in [USDC_TOKEN_ID, WETH_TOKEN_ID, WSTETH_TOKEN_ID, ETH_TOKEN_ID]:
1559
1548
  try:
1560
1549
  success, info = await self.token_adapter.get_token(token_id)
@@ -1718,7 +1707,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1718
1707
  logger.info(
1719
1708
  f"Swap succeeded on attempt {i + 1} with slippage {slippage * 100:.1f}%"
1720
1709
  )
1721
- # Ensure result is a dict with to_amount
1722
1710
  if isinstance(result, dict):
1723
1711
  return result
1724
1712
  return {"to_amount": result if isinstance(result, int) else 0}
@@ -1744,7 +1732,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1744
1732
  f"failed with slippage {slippage * 100:.1f}%: {e}"
1745
1733
  )
1746
1734
  if i < max_retries - 1:
1747
- # Exponential backoff: 1s, 2s, 4s
1748
1735
  await asyncio.sleep(2**i)
1749
1736
 
1750
1737
  logger.error(
@@ -1834,7 +1821,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1834
1821
  if ("429" in err or "Too Many Requests" in err) and attempt < (
1835
1822
  max_retries - 1
1836
1823
  ):
1837
- # Backoff: 1s, 2s
1838
1824
  await asyncio.sleep(2**attempt)
1839
1825
  continue
1840
1826
  logger.warning(
@@ -1940,6 +1926,34 @@ class MoonwellWstethLoopStrategy(Strategy):
1940
1926
  token_id=USDC_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
1941
1927
  )
1942
1928
 
1929
+ async def _get_wallet_balances_usd(self) -> dict[str, dict]:
1930
+ strategy_addr = self._get_strategy_wallet_address()
1931
+
1932
+ weth_raw, usdc_raw, wsteth_raw = await asyncio.gather(
1933
+ self._get_balance_raw(token_id=WETH_TOKEN_ID, wallet_address=strategy_addr),
1934
+ self._get_balance_raw(token_id=USDC_TOKEN_ID, wallet_address=strategy_addr),
1935
+ self._get_balance_raw(
1936
+ token_id=WSTETH_TOKEN_ID, wallet_address=strategy_addr
1937
+ ),
1938
+ )
1939
+
1940
+ # Get token data (prices cached from _aggregate_positions)
1941
+ token_data = await asyncio.gather(
1942
+ self._get_token_data(WETH_TOKEN_ID),
1943
+ self._get_token_data(USDC_TOKEN_ID),
1944
+ self._get_token_data(WSTETH_TOKEN_ID),
1945
+ )
1946
+
1947
+ def calc(raw: int, decimals: int, price: float) -> dict:
1948
+ tokens = raw / 10**decimals if raw > 0 else 0.0
1949
+ return {"tokens": tokens, "usd": tokens * price}
1950
+
1951
+ return {
1952
+ "WETH": calc(weth_raw, token_data[0][1], token_data[0][0]),
1953
+ "USDC": calc(usdc_raw, token_data[1][1], token_data[1][0]),
1954
+ "wstETH": calc(wsteth_raw, token_data[2][1], token_data[2][0]),
1955
+ }
1956
+
1943
1957
  async def _validate_gas_balance(self) -> tuple[bool, str]:
1944
1958
  gas_balance = await self._get_gas_balance()
1945
1959
  main_gas = await self._get_balance_raw(
@@ -1978,7 +1992,7 @@ class MoonwellWstethLoopStrategy(Strategy):
1978
1992
  return (True, "USDC deposit amount validated", usdc_amount)
1979
1993
 
1980
1994
  async def _check_quote_profitability(self) -> tuple[bool, str]:
1981
- quote = await self.quote()
1995
+ quote = await self._quote()
1982
1996
  if quote.get("apy", 0) < 0:
1983
1997
  return (
1984
1998
  False,
@@ -2023,7 +2037,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2023
2037
  if exclude is None:
2024
2038
  exclude = set()
2025
2039
 
2026
- # Always exclude gas token and target
2027
2040
  exclude.add(ETH_TOKEN_ID)
2028
2041
  exclude.add(target_token_id)
2029
2042
 
@@ -2078,7 +2091,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2078
2091
  return (True, f"Swept {swept_count} tokens totaling ${total_swept_usd:.2f}")
2079
2092
 
2080
2093
  async def _claim_and_reinvest_rewards(self) -> tuple[bool, str]:
2081
- # Claim rewards if above threshold
2082
2094
  claimed_ok, claimed = await self.moonwell_adapter.claim_rewards(
2083
2095
  min_rewards_usd=self.MIN_REWARD_CLAIM_USD
2084
2096
  )
@@ -2105,7 +2117,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2105
2117
  )
2106
2118
  return (True, f"WELL value ${well_value_usd:.2f} below threshold")
2107
2119
 
2108
- # Swap WELL → USDC
2109
2120
  logger.info(
2110
2121
  f"Swapping {well_balance / 10**well_decimals:.4f} WELL "
2111
2122
  f"(${well_value_usd:.2f}) to USDC"
@@ -2173,7 +2184,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2173
2184
  if not success:
2174
2185
  return (False, message)
2175
2186
 
2176
- # Transfer USDC to vault wallet
2177
2187
  success, message = await self._transfer_usdc_to_vault(usdc_amount)
2178
2188
  if not success:
2179
2189
  return (False, message)
@@ -2228,7 +2238,7 @@ class MoonwellWstethLoopStrategy(Strategy):
2228
2238
  logger.warning(f"Failed to fetch stETH APY: {e}")
2229
2239
  return None
2230
2240
 
2231
- async def quote(self) -> dict:
2241
+ async def _quote(self) -> dict:
2232
2242
  (
2233
2243
  usdc_apy_result,
2234
2244
  weth_apy_result,
@@ -2350,7 +2360,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2350
2360
  weth_bal = int(weth_after)
2351
2361
 
2352
2362
  if eth_delta > 0 and usable_eth > 0:
2353
- # Borrow arrived as native ETH - wrap it first
2354
2363
  wrap_amt = min(int(safe_borrow_amt), int(usable_eth))
2355
2364
  logger.info(
2356
2365
  f"Borrow arrived as native ETH, wrapping {wrap_amt / 10**18:.6f} ETH to WETH"
@@ -2478,7 +2487,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2478
2487
  min(to_amount_wei, wsteth_bal) if wsteth_bal > 0 else to_amount_wei
2479
2488
  )
2480
2489
 
2481
- # If swap produced 0 wstETH, rollback the borrow
2482
2490
  if lend_amt_wei <= 0:
2483
2491
  logger.warning("Swap resulted in 0 wstETH. Rolling back borrow...")
2484
2492
  try:
@@ -2818,7 +2826,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2818
2826
  borrow_bal = weth_pos[1].get("borrow_balance", 0)
2819
2827
  current_borrowed_value = (borrow_bal / 10**18) * weth_price
2820
2828
 
2821
- # Lend USDC and enable as collateral
2822
2829
  success, msg = await self.moonwell_adapter.lend(
2823
2830
  mtoken=M_USDC,
2824
2831
  underlying_token=USDC,
@@ -2894,7 +2901,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2894
2901
  )
2895
2902
  logger.info("Entered M_WETH market to enable borrowing")
2896
2903
 
2897
- # Use provided collateral factors or fetch them
2898
2904
  if collateral_factors is not None:
2899
2905
  cf_u, cf_w = collateral_factors
2900
2906
  else:
@@ -2902,7 +2908,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2902
2908
 
2903
2909
  max_safe_f = self._max_safe_F(cf_w)
2904
2910
 
2905
- # Guard against division by zero/negative denominator
2906
2911
  denominator = self.TARGET_HEALTH_FACTOR + 0.001 - cf_w
2907
2912
  if denominator <= 0:
2908
2913
  logger.warning(
@@ -3053,7 +3058,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3053
3058
 
3054
3059
  snap, _ = await self._accounting_snapshot(collateral_factors=collateral_factors)
3055
3060
 
3056
- # Log current state
3057
3061
  logger.info("-" * 40)
3058
3062
  logger.info("CURRENT STATE:")
3059
3063
  logger.info(f" Health Factor: {snap.hf:.3f}")
@@ -3410,7 +3414,7 @@ class MoonwellWstethLoopStrategy(Strategy):
3410
3414
  err: Exception | None = None
3411
3415
 
3412
3416
  try:
3413
- status = await self._withdraw_impl(amount)
3417
+ status = await self._withdraw_impl()
3414
3418
  except Exception as exc:
3415
3419
  err = exc
3416
3420
  if isinstance(exc, SwapOutcomeUnknownError):
@@ -3428,8 +3432,7 @@ class MoonwellWstethLoopStrategy(Strategy):
3428
3432
  )
3429
3433
  return (False, f"{status[1]} | {suffix}")
3430
3434
 
3431
- async def _withdraw_impl(self, amount: float | None = None) -> StatusTuple:
3432
- # NOTE: amount is currently unused; withdraw() is all-or-nothing in this strategy.
3435
+ async def _withdraw_impl(self) -> StatusTuple:
3433
3436
  logger.info("=" * 60)
3434
3437
  logger.info("WITHDRAW START - Full position unwind")
3435
3438
  logger.info("=" * 60)
@@ -3537,7 +3540,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3537
3540
 
3538
3541
  transferred_items = []
3539
3542
 
3540
- # Transfer USDC to main wallet
3541
3543
  usdc_balance = await self._get_balance_raw(
3542
3544
  token_id=USDC_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
3543
3545
  )
@@ -3559,7 +3561,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3559
3561
  transferred_items.append(f"{usdc_amount:.2f} USDC")
3560
3562
  logger.info(f"USDC transfer successful: {usdc_amount:.2f} USDC")
3561
3563
 
3562
- # Transfer ETH (minus small reserve for tx fees) to main wallet
3563
3564
  gas_balance = await self._get_balance_raw(
3564
3565
  token_id=ETH_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
3565
3566
  )
@@ -3588,7 +3589,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3588
3589
  async def _unlend_remaining_positions(self) -> tuple[bool, str]:
3589
3590
  logger.info("UNLEND: Redeeming remaining Moonwell positions...")
3590
3591
 
3591
- # Unlend remaining wstETH
3592
3592
  wsteth_pos = await self.moonwell_adapter.get_pos(mtoken=M_WSTETH)
3593
3593
  if wsteth_pos[0]:
3594
3594
  mtoken_bal = wsteth_pos[1].get("mtoken_balance", 0)
@@ -3600,7 +3600,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3600
3600
  )
3601
3601
  if not ok:
3602
3602
  return (False, f"Failed to unlend wstETH: {msg}")
3603
- # Swap to USDC with retries
3604
3603
  wsteth_bal = await self._get_balance_raw(
3605
3604
  token_id=WSTETH_TOKEN_ID,
3606
3605
  wallet_address=self._get_strategy_wallet_address(),
@@ -3614,7 +3613,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3614
3613
  if swap_result is None:
3615
3614
  return (False, "Failed to swap wstETH to USDC after retries")
3616
3615
 
3617
- # Unlend remaining USDC
3618
3616
  usdc_pos = await self.moonwell_adapter.get_pos(mtoken=M_USDC)
3619
3617
  if usdc_pos[0]:
3620
3618
  mtoken_bal = usdc_pos[1].get("mtoken_balance", 0)
@@ -3627,10 +3625,8 @@ class MoonwellWstethLoopStrategy(Strategy):
3627
3625
  if not ok:
3628
3626
  return (False, f"Failed to unlend USDC: {msg}")
3629
3627
 
3630
- # Claim any remaining rewards
3631
3628
  await self.moonwell_adapter.claim_rewards(min_rewards_usd=0)
3632
3629
 
3633
- # Sweep any remaining tokens to USDC
3634
3630
  ok, msg = await self._sweep_token_balances(
3635
3631
  target_token_id=USDC_TOKEN_ID,
3636
3632
  exclude={ETH_TOKEN_ID, WELL_TOKEN_ID},
@@ -3661,6 +3657,27 @@ class MoonwellWstethLoopStrategy(Strategy):
3661
3657
  totals_usd = dict(snap.totals_usd)
3662
3658
 
3663
3659
  ltv = float(snap.ltv)
3660
+
3661
+ wallet_balances: dict[str, dict[str, float]] = {}
3662
+ wallet_tokens = {
3663
+ "WETH": ("wallet_weth", TOKEN_ID_WETH_BASE),
3664
+ "USDC": ("wallet_usdc", TOKEN_ID_USDC_BASE),
3665
+ "wstETH": ("wallet_wsteth", TOKEN_ID_WSTETH_BASE),
3666
+ }
3667
+ # Assume snapshot exposes token prices and decimals keyed by token id
3668
+ prices_usd = getattr(snap, "prices_usd", {}) or {}
3669
+ token_decimals = getattr(snap, "token_decimals", {}) or {}
3670
+ for symbol, (attr_name, token_id) in wallet_tokens.items():
3671
+ raw_balance = getattr(snap, attr_name, 0) or 0
3672
+ if raw_balance:
3673
+ decimals = token_decimals.get(token_id, 18)
3674
+ price = prices_usd.get(token_id, 0.0) or 0.0
3675
+ tokens = float(raw_balance) / float(10**decimals)
3676
+ usd_value = tokens * float(price)
3677
+ wallet_balances[symbol] = {
3678
+ "tokens": tokens,
3679
+ "usd": usd_value,
3680
+ }
3664
3681
  hf = (1 / ltv) if ltv and ltv > 0 and not (ltv != ltv) else None
3665
3682
 
3666
3683
  gas_balance = await self._get_gas_balance()
@@ -3678,12 +3695,21 @@ class MoonwellWstethLoopStrategy(Strategy):
3678
3695
 
3679
3696
  peg_diff = await self.get_peg_diff()
3680
3697
 
3681
- portfolio_value = float(snap.net_equity_usd)
3698
+ # Calculate displayed portfolio value.
3699
+ # Note: snap.net_equity_usd represents net equity (wallet + supplies - debt),
3700
+ # so wallet_value is added here only for this aggregate display metric.
3701
+ net_equity_value = float(snap.net_equity_usd)
3702
+ wallet_value = sum(wb["usd"] for wb in wallet_balances.values())
3703
+ portfolio_value = net_equity_value + wallet_value
3682
3704
 
3683
- quote = await self.quote()
3705
+ quote = await self._quote()
3684
3706
 
3685
3707
  strategy_status = {
3686
3708
  "current_positions_usd_value": totals_usd,
3709
+ "wallet_balances": {
3710
+ k: v for k, v in wallet_balances.items() if v["tokens"] > 0
3711
+ },
3712
+ "wallet_balances_total_usd": wallet_value,
3687
3713
  "credit_remaining": f"{credit_remaining * 100:.2f}%",
3688
3714
  "LTV": ltv,
3689
3715
  "health_factor": hf,