wayfinder-paths 0.1.23__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 (122) hide show
  1. wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
  2. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
  4. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  5. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  6. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  7. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  8. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  9. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  10. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  11. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  12. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  13. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  15. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
  16. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
  18. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  19. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
  20. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  21. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  23. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  24. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  27. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  28. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  29. wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
  30. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  31. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
  32. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  33. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  34. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  35. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  36. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  37. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  38. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  39. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  40. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  41. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  42. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  43. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  44. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  45. wayfinder_paths/conftest.py +24 -17
  46. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  47. wayfinder_paths/core/adapters/models.py +17 -7
  48. wayfinder_paths/core/clients/BRAPClient.py +1 -1
  49. wayfinder_paths/core/clients/TokenClient.py +47 -1
  50. wayfinder_paths/core/clients/WayfinderClient.py +1 -2
  51. wayfinder_paths/core/clients/protocols.py +21 -22
  52. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  53. wayfinder_paths/core/config.py +12 -0
  54. wayfinder_paths/core/constants/__init__.py +15 -0
  55. wayfinder_paths/core/constants/base.py +6 -1
  56. wayfinder_paths/core/constants/contracts.py +39 -26
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  59. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  60. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  61. wayfinder_paths/core/engine/manifest.py +66 -0
  62. wayfinder_paths/core/strategies/Strategy.py +0 -61
  63. wayfinder_paths/core/strategies/__init__.py +10 -1
  64. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  65. wayfinder_paths/core/utils/test_transaction.py +289 -0
  66. wayfinder_paths/core/utils/transaction.py +44 -1
  67. wayfinder_paths/core/utils/web3.py +3 -0
  68. wayfinder_paths/mcp/__init__.py +5 -0
  69. wayfinder_paths/mcp/preview.py +185 -0
  70. wayfinder_paths/mcp/scripting.py +84 -0
  71. wayfinder_paths/mcp/server.py +52 -0
  72. wayfinder_paths/mcp/state/profile_store.py +195 -0
  73. wayfinder_paths/mcp/state/store.py +89 -0
  74. wayfinder_paths/mcp/test_scripting.py +267 -0
  75. wayfinder_paths/mcp/tools/__init__.py +0 -0
  76. wayfinder_paths/mcp/tools/balances.py +290 -0
  77. wayfinder_paths/mcp/tools/discovery.py +158 -0
  78. wayfinder_paths/mcp/tools/execute.py +770 -0
  79. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  80. wayfinder_paths/mcp/tools/quotes.py +288 -0
  81. wayfinder_paths/mcp/tools/run_script.py +286 -0
  82. wayfinder_paths/mcp/tools/strategies.py +188 -0
  83. wayfinder_paths/mcp/tools/tokens.py +46 -0
  84. wayfinder_paths/mcp/tools/wallets.py +354 -0
  85. wayfinder_paths/mcp/utils.py +129 -0
  86. wayfinder_paths/policies/hyperliquid.py +1 -1
  87. wayfinder_paths/policies/lifi.py +18 -0
  88. wayfinder_paths/policies/util.py +8 -2
  89. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
  90. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  91. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  92. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  93. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  94. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  95. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  96. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  97. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  98. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  99. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  100. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  101. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  102. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  103. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
  104. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  105. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  106. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
  107. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
  108. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
  109. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  110. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
  111. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
  112. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  113. wayfinder_paths/tests/test_test_coverage.py +1 -4
  114. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  115. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  116. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  117. wayfinder_paths/scripts/create_strategy.py +0 -139
  118. wayfinder_paths/scripts/make_wallets.py +0 -142
  119. wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
  120. wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
  121. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  122. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
@@ -42,6 +42,7 @@ from wayfinder_paths.policies.hyperliquid import (
42
42
  any_hyperliquid_l1_payload,
43
43
  any_hyperliquid_user_payload,
44
44
  )
45
+ from wayfinder_paths.policies.lifi import LIFI_ROUTERS, lifi_swap
45
46
  from wayfinder_paths.policies.prjx import PRJX_ROUTER, prjx_swap
46
47
 
47
48
  SYMBOL_TRANSLATION_TABLE = str.maketrans(
@@ -246,15 +247,6 @@ class HyperlendStableYieldStrategy(Strategy):
246
247
  strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
247
248
  )
248
249
 
249
- self.register_adapters(
250
- [
251
- balance,
252
- token_adapter,
253
- ledger_adapter,
254
- brap_adapter,
255
- hyperlend_adapter,
256
- ]
257
- )
258
250
  self.balance_adapter = balance
259
251
  self.token_adapter = token_adapter
260
252
  self.ledger_adapter = ledger_adapter
@@ -866,7 +858,6 @@ class HyperlendStableYieldStrategy(Strategy):
866
858
  if sweep_actions:
867
859
  messages.append(f"Residual sweeps: {'; '.join(sweep_actions)}.")
868
860
 
869
- # Report balances in strategy wallet
870
861
  balance_parts = []
871
862
  if total_usdt > 0:
872
863
  balance_parts.append(
@@ -903,7 +894,6 @@ class HyperlendStableYieldStrategy(Strategy):
903
894
 
904
895
  transferred_items = []
905
896
 
906
- # Transfer USDT0 to main wallet
907
897
  usdt_ok, usdt_raw = await self.balance_adapter.get_balance(
908
898
  token_id="usdt0-hyperevm",
909
899
  wallet_address=strategy_address,
@@ -928,7 +918,6 @@ class HyperlendStableYieldStrategy(Strategy):
928
918
  else:
929
919
  self.logger.warning(f"USDT0 transfer failed: {msg}")
930
920
 
931
- # Transfer HYPE (minus reserve for tx fees) to main wallet
932
921
  hype_ok, hype_raw = await self.balance_adapter.get_balance(
933
922
  token_id="hyperliquid-hyperevm",
934
923
  wallet_address=strategy_address,
@@ -2382,4 +2371,6 @@ class HyperlendStableYieldStrategy(Strategy):
2382
2371
  await enso_swap(),
2383
2372
  erc20_spender_for_any_token(PRJX_ROUTER),
2384
2373
  await prjx_swap(),
2374
+ erc20_spender_for_any_token(LIFI_ROUTERS[999]),
2375
+ await lifi_swap(999),
2385
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
@@ -101,7 +80,6 @@ def strategy():
101
80
  )
102
81
 
103
82
  if hasattr(s, "balance_adapter") and s.balance_adapter:
104
- # Mock the main methods first
105
83
  s.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
106
84
  return_value=(True, "Transfer successful (simulated)")
107
85
  )
@@ -63,7 +63,7 @@ COLLATERAL_SAFETY_FACTOR = 0.98
63
63
 
64
64
 
65
65
  class SwapOutcomeUnknownError(RuntimeError):
66
- "Raised when the outcome of a swap operation is unknown."
66
+ pass
67
67
 
68
68
 
69
69
  class MoonwellWstethLoopStrategy(Strategy):
@@ -196,14 +196,12 @@ class MoonwellWstethLoopStrategy(Strategy):
196
196
 
197
197
  self.config = merged_config
198
198
 
199
- # Adapter references
200
199
  self.balance_adapter: BalanceAdapter | None = None
201
200
  self.moonwell_adapter: MoonwellAdapter | None = None
202
201
  self.brap_adapter: BRAPAdapter | None = None
203
202
  self.token_adapter: TokenAdapter | None = None
204
203
  self.ledger_adapter: LedgerAdapter | None = None
205
204
 
206
- # Token info cache
207
205
  self._token_info_cache: dict[str, dict] = {}
208
206
  self._token_price_cache: dict[str, float] = {}
209
207
  self._token_price_timestamps: dict[str, float] = {}
@@ -239,16 +237,6 @@ class MoonwellWstethLoopStrategy(Strategy):
239
237
  strategy_wallet_signing_callback=self.strategy_wallet_signing_callback,
240
238
  )
241
239
 
242
- self.register_adapters(
243
- [
244
- balance,
245
- token_adapter,
246
- ledger_adapter,
247
- brap_adapter,
248
- moonwell_adapter,
249
- ]
250
- )
251
-
252
240
  self.balance_adapter = balance
253
241
  self.token_adapter = token_adapter
254
242
  self.ledger_adapter = ledger_adapter
@@ -377,11 +365,9 @@ class MoonwellWstethLoopStrategy(Strategy):
377
365
  int((weth_pos or {}).get("borrow_balance", 0) or 0) if weth_pos_ok else 0
378
366
  )
379
367
 
380
- # Gas reserve
381
368
  gas_keep_wei = int(self._gas_keep_wei())
382
369
  eth_usable_wei = max(0, int(wallet_eth) - int(gas_keep_wei))
383
370
 
384
- # USD conversions
385
371
  def _usd(raw: int, price: float, dec: int) -> float:
386
372
  if raw <= 0 or not price or price <= 0:
387
373
  return 0.0
@@ -1558,7 +1544,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1558
1544
  if self.token_adapter is None:
1559
1545
  raise RuntimeError("Token adapter not initialized.")
1560
1546
 
1561
- # Pre-fetch token info
1562
1547
  for token_id in [USDC_TOKEN_ID, WETH_TOKEN_ID, WSTETH_TOKEN_ID, ETH_TOKEN_ID]:
1563
1548
  try:
1564
1549
  success, info = await self.token_adapter.get_token(token_id)
@@ -1722,7 +1707,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1722
1707
  logger.info(
1723
1708
  f"Swap succeeded on attempt {i + 1} with slippage {slippage * 100:.1f}%"
1724
1709
  )
1725
- # Ensure result is a dict with to_amount
1726
1710
  if isinstance(result, dict):
1727
1711
  return result
1728
1712
  return {"to_amount": result if isinstance(result, int) else 0}
@@ -1748,7 +1732,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1748
1732
  f"failed with slippage {slippage * 100:.1f}%: {e}"
1749
1733
  )
1750
1734
  if i < max_retries - 1:
1751
- # Exponential backoff: 1s, 2s, 4s
1752
1735
  await asyncio.sleep(2**i)
1753
1736
 
1754
1737
  logger.error(
@@ -1838,7 +1821,6 @@ class MoonwellWstethLoopStrategy(Strategy):
1838
1821
  if ("429" in err or "Too Many Requests" in err) and attempt < (
1839
1822
  max_retries - 1
1840
1823
  ):
1841
- # Backoff: 1s, 2s
1842
1824
  await asyncio.sleep(2**attempt)
1843
1825
  continue
1844
1826
  logger.warning(
@@ -1944,6 +1926,34 @@ class MoonwellWstethLoopStrategy(Strategy):
1944
1926
  token_id=USDC_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
1945
1927
  )
1946
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
+
1947
1957
  async def _validate_gas_balance(self) -> tuple[bool, str]:
1948
1958
  gas_balance = await self._get_gas_balance()
1949
1959
  main_gas = await self._get_balance_raw(
@@ -1982,7 +1992,7 @@ class MoonwellWstethLoopStrategy(Strategy):
1982
1992
  return (True, "USDC deposit amount validated", usdc_amount)
1983
1993
 
1984
1994
  async def _check_quote_profitability(self) -> tuple[bool, str]:
1985
- quote = await self.quote()
1995
+ quote = await self._quote()
1986
1996
  if quote.get("apy", 0) < 0:
1987
1997
  return (
1988
1998
  False,
@@ -2027,7 +2037,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2027
2037
  if exclude is None:
2028
2038
  exclude = set()
2029
2039
 
2030
- # Always exclude gas token and target
2031
2040
  exclude.add(ETH_TOKEN_ID)
2032
2041
  exclude.add(target_token_id)
2033
2042
 
@@ -2082,7 +2091,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2082
2091
  return (True, f"Swept {swept_count} tokens totaling ${total_swept_usd:.2f}")
2083
2092
 
2084
2093
  async def _claim_and_reinvest_rewards(self) -> tuple[bool, str]:
2085
- # Claim rewards if above threshold
2086
2094
  claimed_ok, claimed = await self.moonwell_adapter.claim_rewards(
2087
2095
  min_rewards_usd=self.MIN_REWARD_CLAIM_USD
2088
2096
  )
@@ -2109,7 +2117,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2109
2117
  )
2110
2118
  return (True, f"WELL value ${well_value_usd:.2f} below threshold")
2111
2119
 
2112
- # Swap WELL → USDC
2113
2120
  logger.info(
2114
2121
  f"Swapping {well_balance / 10**well_decimals:.4f} WELL "
2115
2122
  f"(${well_value_usd:.2f}) to USDC"
@@ -2177,7 +2184,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2177
2184
  if not success:
2178
2185
  return (False, message)
2179
2186
 
2180
- # Transfer USDC to vault wallet
2181
2187
  success, message = await self._transfer_usdc_to_vault(usdc_amount)
2182
2188
  if not success:
2183
2189
  return (False, message)
@@ -2232,7 +2238,7 @@ class MoonwellWstethLoopStrategy(Strategy):
2232
2238
  logger.warning(f"Failed to fetch stETH APY: {e}")
2233
2239
  return None
2234
2240
 
2235
- async def quote(self) -> dict:
2241
+ async def _quote(self) -> dict:
2236
2242
  (
2237
2243
  usdc_apy_result,
2238
2244
  weth_apy_result,
@@ -2354,7 +2360,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2354
2360
  weth_bal = int(weth_after)
2355
2361
 
2356
2362
  if eth_delta > 0 and usable_eth > 0:
2357
- # Borrow arrived as native ETH - wrap it first
2358
2363
  wrap_amt = min(int(safe_borrow_amt), int(usable_eth))
2359
2364
  logger.info(
2360
2365
  f"Borrow arrived as native ETH, wrapping {wrap_amt / 10**18:.6f} ETH to WETH"
@@ -2482,7 +2487,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2482
2487
  min(to_amount_wei, wsteth_bal) if wsteth_bal > 0 else to_amount_wei
2483
2488
  )
2484
2489
 
2485
- # If swap produced 0 wstETH, rollback the borrow
2486
2490
  if lend_amt_wei <= 0:
2487
2491
  logger.warning("Swap resulted in 0 wstETH. Rolling back borrow...")
2488
2492
  try:
@@ -2822,7 +2826,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2822
2826
  borrow_bal = weth_pos[1].get("borrow_balance", 0)
2823
2827
  current_borrowed_value = (borrow_bal / 10**18) * weth_price
2824
2828
 
2825
- # Lend USDC and enable as collateral
2826
2829
  success, msg = await self.moonwell_adapter.lend(
2827
2830
  mtoken=M_USDC,
2828
2831
  underlying_token=USDC,
@@ -2898,7 +2901,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2898
2901
  )
2899
2902
  logger.info("Entered M_WETH market to enable borrowing")
2900
2903
 
2901
- # Use provided collateral factors or fetch them
2902
2904
  if collateral_factors is not None:
2903
2905
  cf_u, cf_w = collateral_factors
2904
2906
  else:
@@ -2906,7 +2908,6 @@ class MoonwellWstethLoopStrategy(Strategy):
2906
2908
 
2907
2909
  max_safe_f = self._max_safe_F(cf_w)
2908
2910
 
2909
- # Guard against division by zero/negative denominator
2910
2911
  denominator = self.TARGET_HEALTH_FACTOR + 0.001 - cf_w
2911
2912
  if denominator <= 0:
2912
2913
  logger.warning(
@@ -3057,7 +3058,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3057
3058
 
3058
3059
  snap, _ = await self._accounting_snapshot(collateral_factors=collateral_factors)
3059
3060
 
3060
- # Log current state
3061
3061
  logger.info("-" * 40)
3062
3062
  logger.info("CURRENT STATE:")
3063
3063
  logger.info(f" Health Factor: {snap.hf:.3f}")
@@ -3540,7 +3540,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3540
3540
 
3541
3541
  transferred_items = []
3542
3542
 
3543
- # Transfer USDC to main wallet
3544
3543
  usdc_balance = await self._get_balance_raw(
3545
3544
  token_id=USDC_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
3546
3545
  )
@@ -3562,7 +3561,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3562
3561
  transferred_items.append(f"{usdc_amount:.2f} USDC")
3563
3562
  logger.info(f"USDC transfer successful: {usdc_amount:.2f} USDC")
3564
3563
 
3565
- # Transfer ETH (minus small reserve for tx fees) to main wallet
3566
3564
  gas_balance = await self._get_balance_raw(
3567
3565
  token_id=ETH_TOKEN_ID, wallet_address=self._get_strategy_wallet_address()
3568
3566
  )
@@ -3591,7 +3589,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3591
3589
  async def _unlend_remaining_positions(self) -> tuple[bool, str]:
3592
3590
  logger.info("UNLEND: Redeeming remaining Moonwell positions...")
3593
3591
 
3594
- # Unlend remaining wstETH
3595
3592
  wsteth_pos = await self.moonwell_adapter.get_pos(mtoken=M_WSTETH)
3596
3593
  if wsteth_pos[0]:
3597
3594
  mtoken_bal = wsteth_pos[1].get("mtoken_balance", 0)
@@ -3603,7 +3600,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3603
3600
  )
3604
3601
  if not ok:
3605
3602
  return (False, f"Failed to unlend wstETH: {msg}")
3606
- # Swap to USDC with retries
3607
3603
  wsteth_bal = await self._get_balance_raw(
3608
3604
  token_id=WSTETH_TOKEN_ID,
3609
3605
  wallet_address=self._get_strategy_wallet_address(),
@@ -3617,7 +3613,6 @@ class MoonwellWstethLoopStrategy(Strategy):
3617
3613
  if swap_result is None:
3618
3614
  return (False, "Failed to swap wstETH to USDC after retries")
3619
3615
 
3620
- # Unlend remaining USDC
3621
3616
  usdc_pos = await self.moonwell_adapter.get_pos(mtoken=M_USDC)
3622
3617
  if usdc_pos[0]:
3623
3618
  mtoken_bal = usdc_pos[1].get("mtoken_balance", 0)
@@ -3630,10 +3625,8 @@ class MoonwellWstethLoopStrategy(Strategy):
3630
3625
  if not ok:
3631
3626
  return (False, f"Failed to unlend USDC: {msg}")
3632
3627
 
3633
- # Claim any remaining rewards
3634
3628
  await self.moonwell_adapter.claim_rewards(min_rewards_usd=0)
3635
3629
 
3636
- # Sweep any remaining tokens to USDC
3637
3630
  ok, msg = await self._sweep_token_balances(
3638
3631
  target_token_id=USDC_TOKEN_ID,
3639
3632
  exclude={ETH_TOKEN_ID, WELL_TOKEN_ID},
@@ -3664,6 +3657,27 @@ class MoonwellWstethLoopStrategy(Strategy):
3664
3657
  totals_usd = dict(snap.totals_usd)
3665
3658
 
3666
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
+ }
3667
3681
  hf = (1 / ltv) if ltv and ltv > 0 and not (ltv != ltv) else None
3668
3682
 
3669
3683
  gas_balance = await self._get_gas_balance()
@@ -3681,12 +3695,21 @@ class MoonwellWstethLoopStrategy(Strategy):
3681
3695
 
3682
3696
  peg_diff = await self.get_peg_diff()
3683
3697
 
3684
- 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
3685
3704
 
3686
- quote = await self.quote()
3705
+ quote = await self._quote()
3687
3706
 
3688
3707
  strategy_status = {
3689
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,
3690
3713
  "credit_remaining": f"{credit_remaining * 100:.2f}%",
3691
3714
  "LTV": ltv,
3692
3715
  "health_factor": hf,
@@ -56,7 +56,6 @@ def strategy():
56
56
 
57
57
  @pytest.fixture
58
58
  def mock_adapter_responses(strategy):
59
- # Mock balance adapter
60
59
  strategy.balance_adapter.get_balance = AsyncMock(return_value=(True, 1000000))
61
60
  strategy.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
62
61
  return_value=(True, "success")
@@ -65,7 +64,6 @@ def mock_adapter_responses(strategy):
65
64
  return_value=(True, "success")
66
65
  )
67
66
 
68
- # Mock token adapter
69
67
  strategy.token_adapter.get_token = AsyncMock(
70
68
  return_value=(True, {"decimals": 18, "symbol": "TEST"})
71
69
  )
@@ -73,7 +71,6 @@ def mock_adapter_responses(strategy):
73
71
  return_value=(True, {"current_price": 1.0})
74
72
  )
75
73
 
76
- # Mock moonwell adapter
77
74
  strategy.moonwell_adapter.get_pos = AsyncMock(
78
75
  return_value=(
79
76
  True,
@@ -102,7 +99,6 @@ def mock_adapter_responses(strategy):
102
99
  strategy.moonwell_adapter.set_collateral = AsyncMock(return_value=(True, "success"))
103
100
  strategy.moonwell_adapter.claim_rewards = AsyncMock(return_value={})
104
101
 
105
- # Mock brap adapter
106
102
  strategy.brap_adapter.swap_from_token_ids = AsyncMock(
107
103
  return_value=(True, {"to_amount": 1000000000000000000})
108
104
  )
@@ -116,8 +112,8 @@ async def test_smoke(strategy, mock_adapter_responses):
116
112
  examples = load_strategy_examples(Path(__file__))
117
113
  smoke_data = examples["smoke"]
118
114
 
119
- # Mock quote to return positive APY
120
- with patch.object(strategy, "quote", new_callable=AsyncMock) as mock_quote:
115
+ # Mock _quote to return positive APY
116
+ with patch.object(strategy, "_quote", new_callable=AsyncMock) as mock_quote:
121
117
  mock_quote.return_value = {"apy": 0.1, "data": {}}
122
118
 
123
119
  # Status test
@@ -166,7 +162,7 @@ async def test_canonical_usage(strategy, mock_adapter_responses):
166
162
 
167
163
  for example_name, example_data in canonical.items():
168
164
  # Mock methods for canonical usage tests
169
- with patch.object(strategy, "quote", new_callable=AsyncMock) as mock_quote:
165
+ with patch.object(strategy, "_quote", new_callable=AsyncMock) as mock_quote:
170
166
  mock_quote.return_value = {"apy": 0.1, "data": {}}
171
167
 
172
168
  if "deposit" in example_data:
@@ -226,7 +222,7 @@ async def test_status_returns_status_dict(strategy, mock_adapter_responses):
226
222
  ) as mock_peg:
227
223
  mock_peg.return_value = 0.001
228
224
  with patch.object(
229
- strategy, "quote", new_callable=AsyncMock
225
+ strategy, "_quote", new_callable=AsyncMock
230
226
  ) as mock_quote:
231
227
  mock_quote.return_value = {"apy": 0.1, "data": {}}
232
228
 
@@ -288,7 +284,7 @@ async def test_quote_returns_apy_info(strategy, mock_adapter_responses):
288
284
  return_value=mock_response
289
285
  )
290
286
 
291
- quote = await strategy.quote()
287
+ quote = await strategy._quote()
292
288
 
293
289
  assert "apy" in quote
294
290
  assert "information" in quote or "data" in quote
@@ -624,7 +620,6 @@ async def test_atomic_deposit_iteration_swaps_from_eth_when_borrow_surfaces_as_e
624
620
  strategy.moonwell_adapter.borrow = AsyncMock(side_effect=borrow_side_effect)
625
621
 
626
622
  async def wrap_eth_side_effect(*, amount: int):
627
- # Wrap ETH to WETH
628
623
  balances[ETH_TOKEN_ID] -= int(amount)
629
624
  balances[WETH_TOKEN_ID] += int(amount)
630
625
  return (True, {"block_number": 12346})
@@ -634,10 +629,8 @@ async def test_atomic_deposit_iteration_swaps_from_eth_when_borrow_surfaces_as_e
634
629
  async def swap_side_effect(
635
630
  *, from_token_id: str, to_token_id: str, amount: int, **_
636
631
  ):
637
- # After wrapping, the swap should be WETH→wstETH
638
632
  assert from_token_id == WETH_TOKEN_ID
639
633
  assert to_token_id == WSTETH_TOKEN_ID
640
- # Simulate receiving wstETH
641
634
  balances[WSTETH_TOKEN_ID] += 123
642
635
  return {"to_amount": 123}
643
636
 
@@ -897,7 +890,6 @@ async def test_partial_liquidate_prefers_wsteth_when_excess(strategy):
897
890
  strategy.token_adapter.get_token = AsyncMock(side_effect=mock_get_token)
898
891
  strategy.token_adapter.get_token_price = AsyncMock(side_effect=mock_get_price)
899
892
 
900
- # Wallet balances (raw)
901
893
  balances: dict[str, int] = {USDC_TOKEN_ID: 0, WSTETH_TOKEN_ID: 0}
902
894
 
903
895
  async def mock_get_balance_raw(*, token_id: str, wallet_address: str, **_):
@@ -915,7 +907,6 @@ async def test_partial_liquidate_prefers_wsteth_when_excess(strategy):
915
907
  snap.wsteth_price = 2000.0
916
908
  snap.wsteth_dec = 18
917
909
 
918
- # Collateral factors
919
910
  strategy.moonwell_adapter.get_collateral_factor = AsyncMock(
920
911
  return_value=(True, 0.8)
921
912
  )
@@ -987,7 +978,6 @@ async def test_partial_liquidate_prefers_wsteth_when_excess(strategy):
987
978
 
988
979
  @pytest.mark.asyncio
989
980
  async def test_partial_liquidate_uses_usdc_collateral_when_no_wsteth_excess(strategy):
990
- # Token metadata
991
981
  strategy.token_adapter.get_token = AsyncMock(
992
982
  side_effect=lambda token_id: (
993
983
  True,