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.
- wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
120
|
-
with patch.object(strategy, "
|
|
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, "
|
|
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, "
|
|
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.
|
|
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,
|