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.
- wayfinder_paths/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- 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/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -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/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- 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 +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- 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/adapter.py +14 -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/__init__.py +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- 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/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- 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/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- 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/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- 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 +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- 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.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -1,63 +1,35 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
-
import time
|
|
5
4
|
from typing import Any, Literal
|
|
6
5
|
|
|
6
|
+
from aiocache import Cache
|
|
7
7
|
from eth_utils import to_checksum_address
|
|
8
|
+
from eth_utils.abi import collapse_if_tuple
|
|
8
9
|
|
|
10
|
+
from wayfinder_paths.adapters.multicall_adapter.adapter import MulticallAdapter
|
|
9
11
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
10
12
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
13
|
+
from wayfinder_paths.core.constants.base import MANTISSA, MAX_UINT256, SECONDS_PER_YEAR
|
|
14
|
+
from wayfinder_paths.core.constants.chains import CHAIN_ID_BASE
|
|
15
|
+
from wayfinder_paths.core.constants.contracts import (
|
|
16
|
+
BASE_WETH,
|
|
17
|
+
MOONWELL_COMPTROLLER,
|
|
18
|
+
MOONWELL_M_USDC,
|
|
19
|
+
MOONWELL_REWARD_DISTRIBUTOR,
|
|
20
|
+
MOONWELL_WELL_TOKEN,
|
|
21
|
+
)
|
|
11
22
|
from wayfinder_paths.core.constants.moonwell_abi import (
|
|
12
23
|
COMPTROLLER_ABI,
|
|
13
24
|
MTOKEN_ABI,
|
|
14
25
|
REWARD_DISTRIBUTOR_ABI,
|
|
15
26
|
WETH_ABI,
|
|
16
27
|
)
|
|
17
|
-
from wayfinder_paths.core.utils.tokens import
|
|
18
|
-
|
|
19
|
-
get_token_allowance,
|
|
20
|
-
)
|
|
21
|
-
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
28
|
+
from wayfinder_paths.core.utils.tokens import ensure_allowance
|
|
29
|
+
from wayfinder_paths.core.utils.transaction import encode_call, send_transaction
|
|
22
30
|
from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
MOONWELL_DEFAULTS = {
|
|
26
|
-
# mToken addresses
|
|
27
|
-
"m_usdc": "0xEdc817A28E8B93B03976FBd4a3dDBc9f7D176c22",
|
|
28
|
-
"m_weth": "0x628ff693426583D9a7FB391E54366292F509D457",
|
|
29
|
-
"m_wsteth": "0x627Fe393Bc6EdDA28e99AE648fD6fF362514304b",
|
|
30
|
-
# Underlying token addresses
|
|
31
|
-
"usdc": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
32
|
-
"weth": "0x4200000000000000000000000000000000000006",
|
|
33
|
-
"wsteth": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452",
|
|
34
|
-
# Protocol addresses
|
|
35
|
-
"reward_distributor": "0xe9005b078701e2a0948d2eac43010d35870ad9d2",
|
|
36
|
-
"comptroller": "0xfbb21d0380bee3312b33c4353c8936a0f13ef26c",
|
|
37
|
-
# WELL token address on Base
|
|
38
|
-
"well_token": "0xA88594D404727625A9437C3f886C7643872296AE",
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# Base chain ID
|
|
42
|
-
BASE_CHAIN_ID = 8453
|
|
43
|
-
|
|
44
|
-
# Mantissa for collateral factor calculations (1e18)
|
|
45
|
-
MANTISSA = 10**18
|
|
46
|
-
|
|
47
|
-
# Seconds per year for APY calculations
|
|
48
|
-
SECONDS_PER_YEAR = 365 * 24 * 60 * 60
|
|
49
|
-
|
|
50
|
-
# Collateral factor cache TTL (1 hour - rarely changes, governance controlled)
|
|
51
|
-
CF_CACHE_TTL = 3600
|
|
52
|
-
|
|
53
|
-
# Default retry settings for rate-limited RPCs
|
|
54
|
-
DEFAULT_MAX_RETRIES = 5
|
|
55
|
-
DEFAULT_BASE_DELAY = 3.0
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _is_rate_limit_error(error: Exception | str) -> bool:
|
|
59
|
-
error_str = str(error)
|
|
60
|
-
return "429" in error_str or "Too Many Requests" in error_str
|
|
32
|
+
CHAIN_NAME = "base"
|
|
61
33
|
|
|
62
34
|
|
|
63
35
|
def _timestamp_rate_to_apy(rate: float) -> float:
|
|
@@ -67,6 +39,69 @@ def _timestamp_rate_to_apy(rate: float) -> float:
|
|
|
67
39
|
class MoonwellAdapter(BaseAdapter):
|
|
68
40
|
adapter_type = "MOONWELL"
|
|
69
41
|
|
|
42
|
+
# ---------------------------
|
|
43
|
+
# Multicall decoding helpers
|
|
44
|
+
# ---------------------------
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _chunks(seq: list[Any], n: int) -> list[list[Any]]:
|
|
48
|
+
return [seq[i : i + n] for i in range(0, len(seq), n)]
|
|
49
|
+
|
|
50
|
+
@staticmethod
|
|
51
|
+
def _fn_abi(
|
|
52
|
+
contract: Any, fn_name: str, *, inputs_len: int | None = None
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
for item in contract.abi or []:
|
|
55
|
+
if item.get("type") != "function":
|
|
56
|
+
continue
|
|
57
|
+
if item.get("name") != fn_name:
|
|
58
|
+
continue
|
|
59
|
+
if inputs_len is not None and len(item.get("inputs") or []) != inputs_len:
|
|
60
|
+
continue
|
|
61
|
+
return item
|
|
62
|
+
raise ValueError(f"Function ABI not found: {fn_name} (inputs_len={inputs_len})")
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _decode(web3: Any, fn_abi: dict[str, Any], data: bytes) -> tuple[Any, ...]:
|
|
66
|
+
output_types = [
|
|
67
|
+
collapse_if_tuple(o)
|
|
68
|
+
for o in (fn_abi.get("outputs") or [])
|
|
69
|
+
if isinstance(o, dict)
|
|
70
|
+
]
|
|
71
|
+
if not output_types:
|
|
72
|
+
return ()
|
|
73
|
+
return tuple(web3.codec.decode(output_types, data))
|
|
74
|
+
|
|
75
|
+
async def _multicall_chunked(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
multicall: MulticallAdapter,
|
|
79
|
+
calls: list[Any],
|
|
80
|
+
chunk_size: int,
|
|
81
|
+
) -> list[bytes]:
|
|
82
|
+
"""
|
|
83
|
+
Execute multicall in chunks.
|
|
84
|
+
|
|
85
|
+
If a chunk reverts, fall back to executing calls one-by-one so we can salvage
|
|
86
|
+
partial results (returning b"" for failed calls).
|
|
87
|
+
"""
|
|
88
|
+
out: list[bytes] = []
|
|
89
|
+
for chunk in self._chunks(calls, max(1, int(chunk_size))):
|
|
90
|
+
if not chunk:
|
|
91
|
+
continue
|
|
92
|
+
try:
|
|
93
|
+
res = await multicall.aggregate(chunk)
|
|
94
|
+
out.extend(list(res.return_data))
|
|
95
|
+
continue
|
|
96
|
+
except Exception: # noqa: BLE001 - fall back to individual calls
|
|
97
|
+
for call in chunk:
|
|
98
|
+
try:
|
|
99
|
+
r = await multicall.aggregate([call])
|
|
100
|
+
out.append(r.return_data[0] if r.return_data else b"")
|
|
101
|
+
except Exception: # noqa: BLE001
|
|
102
|
+
out.append(b"")
|
|
103
|
+
return out
|
|
104
|
+
|
|
70
105
|
def __init__(
|
|
71
106
|
self,
|
|
72
107
|
config: dict[str, Any] | None = None,
|
|
@@ -75,49 +110,20 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
75
110
|
) -> None:
|
|
76
111
|
super().__init__("moonwell_adapter", config)
|
|
77
112
|
cfg = config or {}
|
|
78
|
-
adapter_cfg = cfg.get("moonwell_adapter") or {}
|
|
79
113
|
|
|
80
114
|
self.token_client = token_client
|
|
81
115
|
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
82
116
|
|
|
83
|
-
|
|
84
|
-
self.chain_id =
|
|
85
|
-
self.chain_name =
|
|
117
|
+
# Chain configuration (Base-only for now)
|
|
118
|
+
self.chain_id = CHAIN_ID_BASE
|
|
119
|
+
self.chain_name = CHAIN_NAME
|
|
120
|
+
self.comptroller_address = MOONWELL_COMPTROLLER
|
|
121
|
+
self.reward_distributor_address = MOONWELL_REWARD_DISTRIBUTOR
|
|
122
|
+
self.m_usdc = MOONWELL_M_USDC # Sample mtoken for ABI extraction
|
|
86
123
|
|
|
87
|
-
|
|
88
|
-
self.
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
self.reward_distributor_address = self._checksum(
|
|
92
|
-
adapter_cfg.get("reward_distributor")
|
|
93
|
-
or MOONWELL_DEFAULTS["reward_distributor"]
|
|
94
|
-
)
|
|
95
|
-
self.well_token = self._checksum(
|
|
96
|
-
adapter_cfg.get("well_token") or MOONWELL_DEFAULTS["well_token"]
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Token addresses
|
|
100
|
-
self.m_usdc = self._checksum(
|
|
101
|
-
adapter_cfg.get("m_usdc") or MOONWELL_DEFAULTS["m_usdc"]
|
|
102
|
-
)
|
|
103
|
-
self.m_weth = self._checksum(
|
|
104
|
-
adapter_cfg.get("m_weth") or MOONWELL_DEFAULTS["m_weth"]
|
|
105
|
-
)
|
|
106
|
-
self.m_wsteth = self._checksum(
|
|
107
|
-
adapter_cfg.get("m_wsteth") or MOONWELL_DEFAULTS["m_wsteth"]
|
|
108
|
-
)
|
|
109
|
-
self.usdc = self._checksum(adapter_cfg.get("usdc") or MOONWELL_DEFAULTS["usdc"])
|
|
110
|
-
self.weth = self._checksum(adapter_cfg.get("weth") or MOONWELL_DEFAULTS["weth"])
|
|
111
|
-
self.wsteth = self._checksum(
|
|
112
|
-
adapter_cfg.get("wsteth") or MOONWELL_DEFAULTS["wsteth"]
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
# Collateral factor cache: mtoken -> (value, timestamp)
|
|
116
|
-
self._cf_cache: dict[str, tuple[float, float]] = {}
|
|
117
|
-
|
|
118
|
-
# ------------------------------------------------------------------ #
|
|
119
|
-
# Public API - Lending Operations #
|
|
120
|
-
# ------------------------------------------------------------------ #
|
|
124
|
+
strategy_wallet = cfg.get("strategy_wallet") or {}
|
|
125
|
+
self.strategy_wallet_address = to_checksum_address(strategy_wallet["address"])
|
|
126
|
+
self._cache = Cache(Cache.MEMORY)
|
|
121
127
|
|
|
122
128
|
async def lend(
|
|
123
129
|
self,
|
|
@@ -126,33 +132,38 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
126
132
|
underlying_token: str,
|
|
127
133
|
amount: int,
|
|
128
134
|
) -> tuple[bool, Any]:
|
|
129
|
-
strategy = self.
|
|
135
|
+
strategy = self.strategy_wallet_address
|
|
130
136
|
amount = int(amount)
|
|
131
137
|
if amount <= 0:
|
|
132
138
|
return False, "amount must be positive"
|
|
133
139
|
|
|
134
|
-
mtoken =
|
|
135
|
-
underlying_token =
|
|
140
|
+
mtoken = to_checksum_address(mtoken)
|
|
141
|
+
underlying_token = to_checksum_address(underlying_token)
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
approved = await self._ensure_allowance(
|
|
143
|
+
approved = await ensure_allowance(
|
|
139
144
|
token_address=underlying_token,
|
|
140
145
|
owner=strategy,
|
|
141
146
|
spender=mtoken,
|
|
142
147
|
amount=amount,
|
|
148
|
+
chain_id=CHAIN_ID_BASE,
|
|
149
|
+
signing_callback=self.strategy_wallet_signing_callback,
|
|
150
|
+
approval_amount=MAX_UINT256,
|
|
143
151
|
)
|
|
144
152
|
if not approved[0]:
|
|
145
153
|
return approved
|
|
146
154
|
|
|
147
|
-
|
|
148
|
-
tx = await self._encode_call(
|
|
155
|
+
transaction = await encode_call(
|
|
149
156
|
target=mtoken,
|
|
150
157
|
abi=MTOKEN_ABI,
|
|
151
158
|
fn_name="mint",
|
|
152
159
|
args=[amount],
|
|
153
160
|
from_address=strategy,
|
|
161
|
+
chain_id=CHAIN_ID_BASE,
|
|
154
162
|
)
|
|
155
|
-
|
|
163
|
+
txn_hash = await send_transaction(
|
|
164
|
+
transaction, self.strategy_wallet_signing_callback
|
|
165
|
+
)
|
|
166
|
+
return (True, txn_hash)
|
|
156
167
|
|
|
157
168
|
async def unlend(
|
|
158
169
|
self,
|
|
@@ -160,26 +171,25 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
160
171
|
mtoken: str,
|
|
161
172
|
amount: int,
|
|
162
173
|
) -> tuple[bool, Any]:
|
|
163
|
-
strategy = self.
|
|
174
|
+
strategy = self.strategy_wallet_address
|
|
164
175
|
amount = int(amount)
|
|
165
176
|
if amount <= 0:
|
|
166
177
|
return False, "amount must be positive"
|
|
167
178
|
|
|
168
|
-
mtoken =
|
|
179
|
+
mtoken = to_checksum_address(mtoken)
|
|
169
180
|
|
|
170
|
-
|
|
171
|
-
tx = await self._encode_call(
|
|
181
|
+
transaction = await encode_call(
|
|
172
182
|
target=mtoken,
|
|
173
183
|
abi=MTOKEN_ABI,
|
|
174
184
|
fn_name="redeem",
|
|
175
185
|
args=[amount],
|
|
176
186
|
from_address=strategy,
|
|
187
|
+
chain_id=CHAIN_ID_BASE,
|
|
177
188
|
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
# ------------------------------------------------------------------ #
|
|
189
|
+
txn_hash = await send_transaction(
|
|
190
|
+
transaction, self.strategy_wallet_signing_callback
|
|
191
|
+
)
|
|
192
|
+
return (True, txn_hash)
|
|
183
193
|
|
|
184
194
|
async def borrow(
|
|
185
195
|
self,
|
|
@@ -187,86 +197,25 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
187
197
|
mtoken: str,
|
|
188
198
|
amount: int,
|
|
189
199
|
) -> tuple[bool, Any]:
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
strategy = self._strategy_address()
|
|
200
|
+
strategy = self.strategy_wallet_address
|
|
193
201
|
amount = int(amount)
|
|
194
202
|
if amount <= 0:
|
|
195
203
|
return False, "amount must be positive"
|
|
196
204
|
|
|
197
|
-
mtoken =
|
|
205
|
+
mtoken = to_checksum_address(mtoken)
|
|
198
206
|
|
|
199
|
-
|
|
200
|
-
try:
|
|
201
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
202
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
203
|
-
|
|
204
|
-
borrow_before = await mtoken_contract.functions.borrowBalanceStored(
|
|
205
|
-
strategy
|
|
206
|
-
).call(block_identifier="pending")
|
|
207
|
-
|
|
208
|
-
# Simulate borrow to check for errors before submitting
|
|
209
|
-
try:
|
|
210
|
-
borrow_return = await mtoken_contract.functions.borrow(amount).call(
|
|
211
|
-
{"from": strategy}, block_identifier="pending"
|
|
212
|
-
)
|
|
213
|
-
if borrow_return != 0:
|
|
214
|
-
logger.warning(
|
|
215
|
-
f"Borrow simulation returned error code {borrow_return}. "
|
|
216
|
-
"Codes: 3=COMPTROLLER_REJECTION, 9=INVALID_ACCOUNT_PAIR, "
|
|
217
|
-
"14=INSUFFICIENT_LIQUIDITY"
|
|
218
|
-
)
|
|
219
|
-
except Exception as call_err:
|
|
220
|
-
logger.debug(f"Borrow simulation failed: {call_err}")
|
|
221
|
-
except Exception as e:
|
|
222
|
-
logger.warning(f"Failed to get pre-borrow balance: {e}")
|
|
223
|
-
|
|
224
|
-
tx = await self._encode_call(
|
|
207
|
+
transaction = await encode_call(
|
|
225
208
|
target=mtoken,
|
|
226
209
|
abi=MTOKEN_ABI,
|
|
227
210
|
fn_name="borrow",
|
|
228
211
|
args=[amount],
|
|
229
212
|
from_address=strategy,
|
|
213
|
+
chain_id=CHAIN_ID_BASE,
|
|
230
214
|
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# Verify the borrow actually succeeded by checking balance increased
|
|
237
|
-
try:
|
|
238
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
239
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
240
|
-
borrow_after = await mtoken_contract.functions.borrowBalanceStored(
|
|
241
|
-
strategy
|
|
242
|
-
).call(block_identifier="pending")
|
|
243
|
-
|
|
244
|
-
# Borrow balance should have increased by approximately the amount
|
|
245
|
-
# Allow for some interest accrual
|
|
246
|
-
expected_increase = amount * 0.99
|
|
247
|
-
actual_increase = borrow_after - borrow_before
|
|
248
|
-
|
|
249
|
-
if actual_increase < expected_increase:
|
|
250
|
-
from loguru import logger
|
|
251
|
-
|
|
252
|
-
logger.error(
|
|
253
|
-
f"Borrow verification failed: balance only increased by "
|
|
254
|
-
f"{actual_increase} (expected ~{amount}). "
|
|
255
|
-
f"Moonwell likely returned an error code. "
|
|
256
|
-
f"Before: {borrow_before}, After: {borrow_after}"
|
|
257
|
-
)
|
|
258
|
-
return (
|
|
259
|
-
False,
|
|
260
|
-
f"Borrow failed: balance did not increase as expected. "
|
|
261
|
-
f"Before: {borrow_before}, After: {borrow_after}, Expected: +{amount}",
|
|
262
|
-
)
|
|
263
|
-
except Exception as e:
|
|
264
|
-
from loguru import logger
|
|
265
|
-
|
|
266
|
-
logger.warning(f"Could not verify borrow balance: {e}")
|
|
267
|
-
# Continue with the original result if verification fails
|
|
268
|
-
|
|
269
|
-
return result
|
|
215
|
+
txn_hash = await send_transaction(
|
|
216
|
+
transaction, self.strategy_wallet_signing_callback
|
|
217
|
+
)
|
|
218
|
+
return (True, txn_hash)
|
|
270
219
|
|
|
271
220
|
async def repay(
|
|
272
221
|
self,
|
|
@@ -276,75 +225,73 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
276
225
|
amount: int,
|
|
277
226
|
repay_full: bool = False,
|
|
278
227
|
) -> tuple[bool, Any]:
|
|
279
|
-
strategy = self.
|
|
228
|
+
strategy = self.strategy_wallet_address
|
|
280
229
|
amount = int(amount)
|
|
281
230
|
if amount <= 0:
|
|
282
231
|
return False, "amount must be positive"
|
|
283
232
|
|
|
284
|
-
mtoken =
|
|
285
|
-
underlying_token =
|
|
233
|
+
mtoken = to_checksum_address(mtoken)
|
|
234
|
+
underlying_token = to_checksum_address(underlying_token)
|
|
286
235
|
|
|
287
|
-
|
|
288
|
-
# When repay_full=True, approve the amount we have, Moonwell will use only what's needed
|
|
289
|
-
approved = await self._ensure_allowance(
|
|
236
|
+
approved = await ensure_allowance(
|
|
290
237
|
token_address=underlying_token,
|
|
291
238
|
owner=strategy,
|
|
292
239
|
spender=mtoken,
|
|
293
240
|
amount=amount,
|
|
241
|
+
chain_id=CHAIN_ID_BASE,
|
|
242
|
+
signing_callback=self.strategy_wallet_signing_callback,
|
|
243
|
+
approval_amount=MAX_UINT256,
|
|
294
244
|
)
|
|
295
245
|
if not approved[0]:
|
|
296
246
|
return approved
|
|
297
247
|
|
|
298
|
-
#
|
|
299
|
-
repay_amount =
|
|
248
|
+
# max uint256 avoids balance calculation race conditions
|
|
249
|
+
repay_amount = MAX_UINT256 if repay_full else amount
|
|
300
250
|
|
|
301
|
-
|
|
251
|
+
transaction = await encode_call(
|
|
302
252
|
target=mtoken,
|
|
303
253
|
abi=MTOKEN_ABI,
|
|
304
254
|
fn_name="repayBorrow",
|
|
305
255
|
args=[repay_amount],
|
|
306
256
|
from_address=strategy,
|
|
257
|
+
chain_id=CHAIN_ID_BASE,
|
|
307
258
|
)
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
# ------------------------------------------------------------------ #
|
|
259
|
+
txn_hash = await send_transaction(
|
|
260
|
+
transaction, self.strategy_wallet_signing_callback
|
|
261
|
+
)
|
|
262
|
+
return (True, txn_hash)
|
|
313
263
|
|
|
314
264
|
async def set_collateral(
|
|
315
265
|
self,
|
|
316
266
|
*,
|
|
317
267
|
mtoken: str,
|
|
318
268
|
) -> tuple[bool, Any]:
|
|
319
|
-
strategy = self.
|
|
320
|
-
mtoken =
|
|
269
|
+
strategy = self.strategy_wallet_address
|
|
270
|
+
mtoken = to_checksum_address(mtoken)
|
|
321
271
|
|
|
322
|
-
|
|
323
|
-
target=
|
|
272
|
+
transaction = await encode_call(
|
|
273
|
+
target=MOONWELL_COMPTROLLER,
|
|
324
274
|
abi=COMPTROLLER_ABI,
|
|
325
275
|
fn_name="enterMarkets",
|
|
326
276
|
args=[[mtoken]],
|
|
327
277
|
from_address=strategy,
|
|
278
|
+
chain_id=CHAIN_ID_BASE,
|
|
279
|
+
)
|
|
280
|
+
txn_hash = await send_transaction(
|
|
281
|
+
transaction, self.strategy_wallet_signing_callback
|
|
328
282
|
)
|
|
329
|
-
result = await self._send_tx(tx)
|
|
330
|
-
|
|
331
|
-
if not result[0]:
|
|
332
|
-
return result
|
|
333
283
|
|
|
334
|
-
# Verify the market was actually entered
|
|
335
284
|
try:
|
|
336
|
-
async with web3_from_chain_id(
|
|
285
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
337
286
|
comptroller = web3.eth.contract(
|
|
338
|
-
address=
|
|
287
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
339
288
|
)
|
|
340
289
|
is_member = await comptroller.functions.checkMembership(
|
|
341
290
|
strategy, mtoken
|
|
342
291
|
).call(block_identifier="pending")
|
|
343
292
|
|
|
344
293
|
if not is_member:
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
logger.error(
|
|
294
|
+
self.logger.error(
|
|
348
295
|
f"set_collateral verification failed: account {strategy} "
|
|
349
296
|
f"is not a member of market {mtoken} after enterMarkets call"
|
|
350
297
|
)
|
|
@@ -353,11 +300,9 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
353
300
|
f"enterMarkets succeeded but account is not a member of market {mtoken}",
|
|
354
301
|
)
|
|
355
302
|
except Exception as e:
|
|
356
|
-
|
|
303
|
+
self.logger.warning(f"Could not verify market membership: {e}")
|
|
357
304
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return result
|
|
305
|
+
return (True, txn_hash)
|
|
361
306
|
|
|
362
307
|
async def is_market_entered(
|
|
363
308
|
self,
|
|
@@ -366,16 +311,20 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
366
311
|
account: str | None = None,
|
|
367
312
|
) -> tuple[bool, bool | str]:
|
|
368
313
|
try:
|
|
369
|
-
acct =
|
|
370
|
-
|
|
314
|
+
acct = (
|
|
315
|
+
to_checksum_address(account)
|
|
316
|
+
if account
|
|
317
|
+
else self.strategy_wallet_address
|
|
318
|
+
)
|
|
319
|
+
mtoken = to_checksum_address(mtoken)
|
|
371
320
|
|
|
372
|
-
async with web3_from_chain_id(
|
|
321
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
373
322
|
comptroller = web3.eth.contract(
|
|
374
|
-
address=
|
|
323
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
375
324
|
)
|
|
376
325
|
is_member = await comptroller.functions.checkMembership(
|
|
377
326
|
acct, mtoken
|
|
378
|
-
).call()
|
|
327
|
+
).call(block_identifier="pending")
|
|
379
328
|
return True, bool(is_member)
|
|
380
329
|
except Exception as exc:
|
|
381
330
|
return False, str(exc)
|
|
@@ -385,32 +334,31 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
385
334
|
*,
|
|
386
335
|
mtoken: str,
|
|
387
336
|
) -> tuple[bool, Any]:
|
|
388
|
-
strategy = self.
|
|
389
|
-
mtoken =
|
|
337
|
+
strategy = self.strategy_wallet_address
|
|
338
|
+
mtoken = to_checksum_address(mtoken)
|
|
390
339
|
|
|
391
|
-
|
|
392
|
-
target=
|
|
340
|
+
transaction = await encode_call(
|
|
341
|
+
target=MOONWELL_COMPTROLLER,
|
|
393
342
|
abi=COMPTROLLER_ABI,
|
|
394
343
|
fn_name="exitMarket",
|
|
395
344
|
args=[mtoken],
|
|
396
345
|
from_address=strategy,
|
|
346
|
+
chain_id=CHAIN_ID_BASE,
|
|
397
347
|
)
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
# ------------------------------------------------------------------ #
|
|
348
|
+
txn_hash = await send_transaction(
|
|
349
|
+
transaction, self.strategy_wallet_signing_callback
|
|
350
|
+
)
|
|
351
|
+
return (True, txn_hash)
|
|
403
352
|
|
|
404
353
|
async def claim_rewards(
|
|
405
354
|
self,
|
|
406
355
|
*,
|
|
407
356
|
min_rewards_usd: float = 0.0,
|
|
408
357
|
) -> tuple[bool, dict[str, int] | str]:
|
|
409
|
-
strategy = self.
|
|
358
|
+
strategy = self.strategy_wallet_address
|
|
410
359
|
|
|
411
360
|
rewards = await self._get_outstanding_rewards(strategy)
|
|
412
361
|
|
|
413
|
-
# Skip if no rewards to claim
|
|
414
362
|
if not rewards:
|
|
415
363
|
return True, {}
|
|
416
364
|
|
|
@@ -419,25 +367,22 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
419
367
|
if total_usd < min_rewards_usd:
|
|
420
368
|
return True, {}
|
|
421
369
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
target=self.comptroller_address,
|
|
370
|
+
transaction = await encode_call(
|
|
371
|
+
target=MOONWELL_COMPTROLLER,
|
|
425
372
|
abi=COMPTROLLER_ABI,
|
|
426
373
|
fn_name="claimReward",
|
|
427
374
|
args=[strategy],
|
|
428
375
|
from_address=strategy,
|
|
376
|
+
chain_id=CHAIN_ID_BASE,
|
|
429
377
|
)
|
|
430
|
-
|
|
431
|
-
if not result[0]:
|
|
432
|
-
return result
|
|
433
|
-
|
|
378
|
+
await send_transaction(transaction, self.strategy_wallet_signing_callback)
|
|
434
379
|
return True, rewards
|
|
435
380
|
|
|
436
381
|
async def _get_outstanding_rewards(self, account: str) -> dict[str, int]:
|
|
437
382
|
try:
|
|
438
|
-
async with web3_from_chain_id(
|
|
383
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
439
384
|
contract = web3.eth.contract(
|
|
440
|
-
address=
|
|
385
|
+
address=MOONWELL_REWARD_DISTRIBUTOR, abi=REWARD_DISTRIBUTOR_ABI
|
|
441
386
|
)
|
|
442
387
|
|
|
443
388
|
all_rewards = await contract.functions.getOutstandingRewardsForUser(
|
|
@@ -446,15 +391,12 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
446
391
|
|
|
447
392
|
rewards: dict[str, int] = {}
|
|
448
393
|
for mtoken_data in all_rewards:
|
|
449
|
-
# mtoken_data is (mToken, [(rewardToken, totalReward, supplySide, borrowSide)])
|
|
450
394
|
if len(mtoken_data) >= 2:
|
|
451
|
-
|
|
452
|
-
for reward_info in token_rewards:
|
|
395
|
+
for reward_info in mtoken_data[1]:
|
|
453
396
|
if len(reward_info) >= 2:
|
|
454
|
-
token_addr = reward_info
|
|
455
|
-
total_reward = reward_info[1]
|
|
397
|
+
token_addr, total_reward, *_ = reward_info
|
|
456
398
|
if total_reward > 0:
|
|
457
|
-
key = f"{
|
|
399
|
+
key = f"{CHAIN_NAME}_{token_addr}"
|
|
458
400
|
rewards[key] = rewards.get(key, 0) + total_reward
|
|
459
401
|
return rewards
|
|
460
402
|
except Exception:
|
|
@@ -466,81 +408,384 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
466
408
|
|
|
467
409
|
total_usd = 0.0
|
|
468
410
|
for token_key, amount in rewards.items():
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
total_usd += (amount / (10**decimals)) * price
|
|
475
|
-
except Exception:
|
|
476
|
-
pass
|
|
411
|
+
token_data = await self.token_client.get_token_details(token_key)
|
|
412
|
+
if token_data:
|
|
413
|
+
price = token_data.get("price_usd") or token_data.get("price", 0)
|
|
414
|
+
decimals = token_data.get("decimals", 18)
|
|
415
|
+
total_usd += (amount / (10**decimals)) * price
|
|
477
416
|
return total_usd
|
|
478
417
|
|
|
479
418
|
# ------------------------------------------------------------------ #
|
|
480
419
|
# Public API - Position & Market Data #
|
|
481
420
|
# ------------------------------------------------------------------ #
|
|
482
421
|
|
|
483
|
-
async def
|
|
422
|
+
async def get_full_user_state(
|
|
484
423
|
self,
|
|
485
424
|
*,
|
|
486
|
-
mtoken: str,
|
|
487
425
|
account: str | None = None,
|
|
426
|
+
include_rewards: bool = True,
|
|
488
427
|
include_usd: bool = False,
|
|
489
|
-
|
|
490
|
-
|
|
428
|
+
include_apy: bool = False,
|
|
429
|
+
include_zero_positions: bool = False,
|
|
430
|
+
multicall_chunk_size: int = 240,
|
|
431
|
+
block_identifier: int | str | None = None, # multicall ignores block id
|
|
491
432
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
433
|
+
"""
|
|
434
|
+
Full Moonwell state snapshot using Multicall3.aggregate.
|
|
435
|
+
|
|
436
|
+
This minimizes RPC roundtrips by batching:
|
|
437
|
+
- Comptroller: getAllMarkets(), getAssetsIn(account), getAccountLiquidity(account)
|
|
438
|
+
- RewardDistributor (optional): getOutstandingRewardsForUser(account)
|
|
439
|
+
- Per market: balanceOf, exchangeRateStored, borrowBalanceStored, underlying,
|
|
440
|
+
decimals, comptroller.markets
|
|
441
|
+
"""
|
|
442
|
+
_ = block_identifier # reserved for future per-call block pinning
|
|
443
|
+
acct = to_checksum_address(account) if account else self.strategy_wallet_address
|
|
495
444
|
|
|
496
|
-
|
|
497
|
-
|
|
445
|
+
try:
|
|
446
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
447
|
+
multicall = MulticallAdapter(chain_id=self.chain_id, web3=web3)
|
|
498
448
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
449
|
+
comptroller = web3.eth.contract(
|
|
450
|
+
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
451
|
+
)
|
|
452
|
+
rewards_contract = web3.eth.contract(
|
|
453
|
+
address=self.reward_distributor_address,
|
|
454
|
+
abi=REWARD_DISTRIBUTOR_ABI,
|
|
455
|
+
)
|
|
507
456
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
|
|
457
|
+
# --- Stage 1: global reads (batched)
|
|
458
|
+
calls_stage1: list[Any] = [
|
|
459
|
+
multicall.build_call(
|
|
460
|
+
self.comptroller_address,
|
|
461
|
+
comptroller.encode_abi("getAllMarkets", args=[]),
|
|
462
|
+
),
|
|
463
|
+
multicall.build_call(
|
|
464
|
+
self.comptroller_address,
|
|
465
|
+
comptroller.encode_abi("getAssetsIn", args=[acct]),
|
|
466
|
+
),
|
|
467
|
+
multicall.build_call(
|
|
468
|
+
self.comptroller_address,
|
|
469
|
+
comptroller.encode_abi("getAccountLiquidity", args=[acct]),
|
|
470
|
+
),
|
|
471
|
+
]
|
|
472
|
+
if include_rewards:
|
|
473
|
+
calls_stage1.append(
|
|
474
|
+
multicall.build_call(
|
|
475
|
+
self.reward_distributor_address,
|
|
476
|
+
rewards_contract.encode_abi(
|
|
477
|
+
"getOutstandingRewardsForUser", args=[acct]
|
|
478
|
+
),
|
|
479
|
+
)
|
|
520
480
|
)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
481
|
+
|
|
482
|
+
ret1 = await self._multicall_chunked(
|
|
483
|
+
multicall=multicall,
|
|
484
|
+
calls=calls_stage1,
|
|
485
|
+
chunk_size=multicall_chunk_size,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# decode stage1
|
|
489
|
+
abi_all = self._fn_abi(comptroller, "getAllMarkets", inputs_len=0)
|
|
490
|
+
abi_assets = self._fn_abi(comptroller, "getAssetsIn", inputs_len=1)
|
|
491
|
+
abi_liq = self._fn_abi(comptroller, "getAccountLiquidity", inputs_len=1)
|
|
492
|
+
|
|
493
|
+
all_markets = (
|
|
494
|
+
self._decode(web3, abi_all, ret1[0] or b"")[0]
|
|
495
|
+
if ret1 and ret1[0]
|
|
496
|
+
else []
|
|
497
|
+
)
|
|
498
|
+
assets_in = (
|
|
499
|
+
self._decode(web3, abi_assets, ret1[1] or b"")[0]
|
|
500
|
+
if len(ret1) > 1 and ret1[1]
|
|
501
|
+
else []
|
|
502
|
+
)
|
|
503
|
+
liq_tuple = (
|
|
504
|
+
self._decode(web3, abi_liq, ret1[2] or b"")
|
|
505
|
+
if len(ret1) > 2 and ret1[2]
|
|
506
|
+
else (0, 0, 0)
|
|
507
|
+
)
|
|
508
|
+
error, liquidity, shortfall = (
|
|
509
|
+
int(liq_tuple[0]),
|
|
510
|
+
int(liq_tuple[1]),
|
|
511
|
+
int(liq_tuple[2]),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
entered = {str(a).lower() for a in (assets_in or [])}
|
|
515
|
+
|
|
516
|
+
rewards: dict[str, int] = {}
|
|
517
|
+
if include_rewards:
|
|
518
|
+
raw_rewards = ret1[3] if len(ret1) > 3 else b""
|
|
519
|
+
if raw_rewards:
|
|
520
|
+
abi_rewards = self._fn_abi(
|
|
521
|
+
rewards_contract,
|
|
522
|
+
"getOutstandingRewardsForUser",
|
|
523
|
+
inputs_len=1,
|
|
524
|
+
)
|
|
525
|
+
decoded = self._decode(web3, abi_rewards, raw_rewards)
|
|
526
|
+
try:
|
|
527
|
+
all_rewards = decoded[0]
|
|
528
|
+
for mtoken_data in all_rewards:
|
|
529
|
+
if len(mtoken_data) < 2:
|
|
530
|
+
continue
|
|
531
|
+
token_rewards = mtoken_data[1] or []
|
|
532
|
+
for reward_info in token_rewards:
|
|
533
|
+
if len(reward_info) < 2:
|
|
534
|
+
continue
|
|
535
|
+
token_addr = reward_info[0]
|
|
536
|
+
total_reward = int(reward_info[1])
|
|
537
|
+
if total_reward <= 0:
|
|
538
|
+
continue
|
|
539
|
+
key = f"{self.chain_name}_{token_addr}"
|
|
540
|
+
rewards[key] = rewards.get(key, 0) + total_reward
|
|
541
|
+
except Exception: # noqa: BLE001
|
|
542
|
+
rewards = await self._get_outstanding_rewards(acct)
|
|
543
|
+
else:
|
|
544
|
+
rewards = await self._get_outstanding_rewards(acct)
|
|
545
|
+
|
|
546
|
+
# --- Stage 2: per-market reads (batched)
|
|
547
|
+
market_calls: list[Any] = []
|
|
548
|
+
market_meta: list[str] = []
|
|
549
|
+
|
|
550
|
+
for m in all_markets or []:
|
|
551
|
+
mtoken = to_checksum_address(str(m))
|
|
552
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
553
|
+
market_meta.append(mtoken)
|
|
554
|
+
|
|
555
|
+
market_calls.extend(
|
|
556
|
+
[
|
|
557
|
+
multicall.build_call(
|
|
558
|
+
mtoken,
|
|
559
|
+
mtoken_contract.encode_abi("balanceOf", args=[acct]),
|
|
560
|
+
),
|
|
561
|
+
multicall.build_call(
|
|
562
|
+
mtoken,
|
|
563
|
+
mtoken_contract.encode_abi(
|
|
564
|
+
"exchangeRateStored", args=[]
|
|
565
|
+
),
|
|
566
|
+
),
|
|
567
|
+
multicall.build_call(
|
|
568
|
+
mtoken,
|
|
569
|
+
mtoken_contract.encode_abi(
|
|
570
|
+
"borrowBalanceStored", args=[acct]
|
|
571
|
+
),
|
|
572
|
+
),
|
|
573
|
+
multicall.build_call(
|
|
574
|
+
mtoken,
|
|
575
|
+
mtoken_contract.encode_abi("underlying", args=[]),
|
|
576
|
+
),
|
|
577
|
+
multicall.build_call(
|
|
578
|
+
mtoken,
|
|
579
|
+
mtoken_contract.encode_abi("decimals", args=[]),
|
|
580
|
+
),
|
|
581
|
+
multicall.build_call(
|
|
582
|
+
self.comptroller_address,
|
|
583
|
+
comptroller.encode_abi("markets", args=[mtoken]),
|
|
584
|
+
),
|
|
585
|
+
]
|
|
525
586
|
)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
587
|
+
|
|
588
|
+
ret2 = await self._multicall_chunked(
|
|
589
|
+
multicall=multicall,
|
|
590
|
+
calls=market_calls,
|
|
591
|
+
chunk_size=multicall_chunk_size,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
sample_mtoken = web3.eth.contract(address=self.m_usdc, abi=MTOKEN_ABI)
|
|
595
|
+
abi_bal = self._fn_abi(sample_mtoken, "balanceOf", inputs_len=1)
|
|
596
|
+
abi_exch = self._fn_abi(
|
|
597
|
+
sample_mtoken, "exchangeRateStored", inputs_len=0
|
|
598
|
+
)
|
|
599
|
+
abi_borrow = self._fn_abi(
|
|
600
|
+
sample_mtoken, "borrowBalanceStored", inputs_len=1
|
|
601
|
+
)
|
|
602
|
+
abi_under = self._fn_abi(sample_mtoken, "underlying", inputs_len=0)
|
|
603
|
+
abi_dec = self._fn_abi(sample_mtoken, "decimals", inputs_len=0)
|
|
604
|
+
abi_mkts = self._fn_abi(comptroller, "markets", inputs_len=1)
|
|
605
|
+
|
|
606
|
+
positions: list[dict[str, Any]] = []
|
|
607
|
+
|
|
608
|
+
stride = 6
|
|
609
|
+
for i, mtoken in enumerate(market_meta):
|
|
610
|
+
base = i * stride
|
|
611
|
+
if base + (stride - 1) >= len(ret2):
|
|
612
|
+
break
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
bal_c = (
|
|
616
|
+
int(self._decode(web3, abi_bal, ret2[base + 0])[0])
|
|
617
|
+
if ret2[base + 0]
|
|
618
|
+
else 0
|
|
619
|
+
)
|
|
620
|
+
exch = (
|
|
621
|
+
int(self._decode(web3, abi_exch, ret2[base + 1])[0])
|
|
622
|
+
if ret2[base + 1]
|
|
623
|
+
else 0
|
|
624
|
+
)
|
|
625
|
+
borrow = (
|
|
626
|
+
int(self._decode(web3, abi_borrow, ret2[base + 2])[0])
|
|
627
|
+
if ret2[base + 2]
|
|
628
|
+
else 0
|
|
629
|
+
)
|
|
630
|
+
underlying = (
|
|
631
|
+
to_checksum_address(
|
|
632
|
+
str(self._decode(web3, abi_under, ret2[base + 3])[0])
|
|
633
|
+
)
|
|
634
|
+
if ret2[base + 3]
|
|
635
|
+
else None
|
|
636
|
+
)
|
|
637
|
+
mdec = (
|
|
638
|
+
int(self._decode(web3, abi_dec, ret2[base + 4])[0])
|
|
639
|
+
if ret2[base + 4]
|
|
640
|
+
else 18
|
|
641
|
+
)
|
|
642
|
+
mkts = (
|
|
643
|
+
self._decode(web3, abi_mkts, ret2[base + 5])
|
|
644
|
+
if ret2[base + 5]
|
|
645
|
+
else (False, 0)
|
|
646
|
+
)
|
|
647
|
+
is_listed = bool(mkts[0])
|
|
648
|
+
collateral_factor = float(int(mkts[1])) / MANTISSA
|
|
649
|
+
except Exception: # noqa: BLE001 - skip malformed markets
|
|
533
650
|
continue
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
651
|
+
|
|
652
|
+
supplied_underlying = (bal_c * exch) // MANTISSA if exch else 0
|
|
653
|
+
|
|
654
|
+
has_supply = bal_c > 0
|
|
655
|
+
has_borrow = borrow > 0
|
|
656
|
+
if not include_zero_positions and not (has_supply or has_borrow):
|
|
657
|
+
continue
|
|
658
|
+
|
|
659
|
+
row: dict[str, Any] = {
|
|
660
|
+
"mtoken": mtoken,
|
|
661
|
+
"underlying": underlying,
|
|
662
|
+
"enteredAsCollateral": mtoken.lower() in entered,
|
|
663
|
+
"isListed": is_listed,
|
|
664
|
+
"collateralFactor": collateral_factor,
|
|
665
|
+
"mTokenDecimals": int(mdec),
|
|
666
|
+
"mTokenBalance": int(bal_c),
|
|
667
|
+
"exchangeRate": int(exch),
|
|
668
|
+
"suppliedUnderlying": int(supplied_underlying),
|
|
669
|
+
"borrowedUnderlying": int(borrow),
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if include_apy:
|
|
673
|
+
try:
|
|
674
|
+
ok_s, apy_s = await self.get_apy(
|
|
675
|
+
mtoken=mtoken,
|
|
676
|
+
apy_type="supply",
|
|
677
|
+
include_rewards=True,
|
|
678
|
+
)
|
|
679
|
+
row["apySupply"] = apy_s if ok_s else None
|
|
680
|
+
except Exception: # noqa: BLE001
|
|
681
|
+
row["apySupply"] = None
|
|
682
|
+
|
|
683
|
+
try:
|
|
684
|
+
ok_b, apy_b = await self.get_apy(
|
|
685
|
+
mtoken=mtoken,
|
|
686
|
+
apy_type="borrow",
|
|
687
|
+
include_rewards=True,
|
|
688
|
+
)
|
|
689
|
+
row["apyBorrow"] = apy_b if ok_b else None
|
|
690
|
+
except Exception: # noqa: BLE001
|
|
691
|
+
row["apyBorrow"] = None
|
|
692
|
+
|
|
693
|
+
positions.append(row)
|
|
694
|
+
|
|
695
|
+
out: dict[str, Any] = {
|
|
696
|
+
"protocol": "moonwell",
|
|
697
|
+
"chainId": int(self.chain_id),
|
|
698
|
+
"account": acct,
|
|
699
|
+
"accountLiquidity": {
|
|
700
|
+
"error": error,
|
|
701
|
+
"liquidity": int(liquidity),
|
|
702
|
+
"shortfall": int(shortfall),
|
|
703
|
+
},
|
|
704
|
+
"positions": positions,
|
|
705
|
+
"rewards": rewards,
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if include_usd and self.token_client:
|
|
709
|
+
total_supplied_usd = 0.0
|
|
710
|
+
total_borrowed_usd = 0.0
|
|
711
|
+
|
|
712
|
+
for r in positions:
|
|
713
|
+
u = r.get("underlying")
|
|
714
|
+
if not u:
|
|
715
|
+
continue
|
|
716
|
+
key = f"{self.chain_name}_{u}"
|
|
717
|
+
td = await self.token_client.get_token_details(key)
|
|
718
|
+
if not td:
|
|
719
|
+
continue
|
|
720
|
+
price = td.get("price_usd") or td.get("price")
|
|
721
|
+
dec = int(td.get("decimals", 18))
|
|
722
|
+
if price is None:
|
|
723
|
+
continue
|
|
724
|
+
total_supplied_usd += (
|
|
725
|
+
r["suppliedUnderlying"] / (10**dec)
|
|
726
|
+
) * float(price)
|
|
727
|
+
total_borrowed_usd += (
|
|
728
|
+
r["borrowedUnderlying"] / (10**dec)
|
|
729
|
+
) * float(price)
|
|
730
|
+
|
|
731
|
+
out["totalsUsd"] = {
|
|
732
|
+
"supplied": total_supplied_usd,
|
|
733
|
+
"borrowed": total_borrowed_usd,
|
|
734
|
+
"net": total_supplied_usd - total_borrowed_usd,
|
|
735
|
+
}
|
|
736
|
+
if include_rewards and rewards:
|
|
737
|
+
out["rewardsUsd"] = await self._calculate_rewards_usd(rewards)
|
|
738
|
+
|
|
739
|
+
return True, out
|
|
740
|
+
|
|
741
|
+
except Exception as exc: # noqa: BLE001
|
|
742
|
+
return False, str(exc)
|
|
743
|
+
|
|
744
|
+
async def get_pos(
|
|
745
|
+
self,
|
|
746
|
+
*,
|
|
747
|
+
mtoken: str,
|
|
748
|
+
account: str | None = None,
|
|
749
|
+
include_usd: bool = False,
|
|
750
|
+
block_identifier: int | str | None = None,
|
|
751
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
752
|
+
mtoken = to_checksum_address(mtoken)
|
|
753
|
+
account = (
|
|
754
|
+
to_checksum_address(account) if account else self.strategy_wallet_address
|
|
755
|
+
)
|
|
756
|
+
block_id = block_identifier if block_identifier is not None else "pending"
|
|
757
|
+
|
|
758
|
+
try:
|
|
759
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
760
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
761
|
+
rewards_contract = web3.eth.contract(
|
|
762
|
+
address=MOONWELL_REWARD_DISTRIBUTOR,
|
|
763
|
+
abi=REWARD_DISTRIBUTOR_ABI,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
bal = await mtoken_contract.functions.balanceOf(account).call(
|
|
767
|
+
block_identifier=block_id
|
|
768
|
+
)
|
|
769
|
+
exch = await mtoken_contract.functions.exchangeRateStored().call(
|
|
770
|
+
block_identifier=block_id
|
|
771
|
+
)
|
|
772
|
+
borrow = await mtoken_contract.functions.borrowBalanceStored(
|
|
773
|
+
account
|
|
774
|
+
).call(block_identifier=block_id)
|
|
775
|
+
underlying = await mtoken_contract.functions.underlying().call(
|
|
776
|
+
block_identifier=block_id
|
|
777
|
+
)
|
|
778
|
+
rewards = await rewards_contract.functions.getOutstandingRewardsForUser(
|
|
779
|
+
mtoken, account
|
|
780
|
+
).call(block_identifier=block_id)
|
|
781
|
+
except Exception as exc:
|
|
782
|
+
return False, str(exc)
|
|
538
783
|
|
|
539
784
|
try:
|
|
540
785
|
reward_balances = self._process_rewards(rewards)
|
|
541
786
|
|
|
542
|
-
mtoken_key = f"{
|
|
543
|
-
underlying_key = f"{
|
|
787
|
+
mtoken_key = f"{CHAIN_NAME}_{mtoken}"
|
|
788
|
+
underlying_key = f"{CHAIN_NAME}_{underlying}"
|
|
544
789
|
|
|
545
790
|
balances: dict[str, int] = {mtoken_key: bal}
|
|
546
791
|
balances.update(reward_balances)
|
|
@@ -559,7 +804,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
559
804
|
|
|
560
805
|
if include_usd and self.token_client:
|
|
561
806
|
usd_balances = await self._calculate_usd_balances(
|
|
562
|
-
balances, underlying_key
|
|
807
|
+
balances, underlying_key
|
|
563
808
|
)
|
|
564
809
|
result["usd_balances"] = usd_balances
|
|
565
810
|
|
|
@@ -571,39 +816,31 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
571
816
|
result: dict[str, int] = {}
|
|
572
817
|
for reward_info in rewards:
|
|
573
818
|
if len(reward_info) >= 2:
|
|
574
|
-
token_addr = reward_info
|
|
575
|
-
total_reward = reward_info[1]
|
|
819
|
+
token_addr, total_reward, *_ = reward_info
|
|
576
820
|
if total_reward > 0:
|
|
577
|
-
key = f"{
|
|
821
|
+
key = f"{CHAIN_NAME}_{token_addr}"
|
|
578
822
|
result[key] = total_reward
|
|
579
823
|
return result
|
|
580
824
|
|
|
581
825
|
async def _calculate_usd_balances(
|
|
582
|
-
self, balances: dict[str, int], underlying_key: str
|
|
826
|
+
self, balances: dict[str, int], underlying_key: str
|
|
583
827
|
) -> dict[str, float | None]:
|
|
584
828
|
if not self.token_client:
|
|
585
829
|
return {}
|
|
586
830
|
|
|
587
|
-
tokens = set(balances.keys()) | {underlying_key}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
token_key
|
|
593
|
-
)
|
|
594
|
-
except Exception:
|
|
595
|
-
token_data[token_key] = None
|
|
831
|
+
tokens = list(set(balances.keys()) | {underlying_key})
|
|
832
|
+
token_details = await asyncio.gather(
|
|
833
|
+
*[self.token_client.get_token_details(key) for key in tokens]
|
|
834
|
+
)
|
|
835
|
+
token_data = dict(zip(tokens, token_details, strict=True))
|
|
596
836
|
|
|
597
837
|
usd_balances: dict[str, float | None] = {}
|
|
598
838
|
for token_key, bal in balances.items():
|
|
599
839
|
data = token_data.get(token_key)
|
|
600
|
-
if data:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
usd_balances[token_key] = (bal / (10**decimals)) * price
|
|
605
|
-
else:
|
|
606
|
-
usd_balances[token_key] = None
|
|
840
|
+
if data and (price := data.get("price_usd") or data.get("price")):
|
|
841
|
+
usd_balances[token_key] = (
|
|
842
|
+
bal / (10 ** data.get("decimals", 18))
|
|
843
|
+
) * price
|
|
607
844
|
else:
|
|
608
845
|
usd_balances[token_key] = None
|
|
609
846
|
|
|
@@ -613,48 +850,34 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
613
850
|
self,
|
|
614
851
|
*,
|
|
615
852
|
mtoken: str,
|
|
616
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
617
853
|
) -> tuple[bool, float | str]:
|
|
618
|
-
mtoken =
|
|
619
|
-
|
|
620
|
-
now = time.time()
|
|
621
|
-
if mtoken in self._cf_cache:
|
|
622
|
-
cached_value, cached_time = self._cf_cache[mtoken]
|
|
623
|
-
if now - cached_time < CF_CACHE_TTL:
|
|
624
|
-
return True, cached_value
|
|
625
|
-
|
|
626
|
-
last_error = ""
|
|
627
|
-
for attempt in range(max_retries):
|
|
628
|
-
try:
|
|
629
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
630
|
-
contract = web3.eth.contract(
|
|
631
|
-
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
632
|
-
)
|
|
854
|
+
mtoken = to_checksum_address(mtoken)
|
|
633
855
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
)
|
|
638
|
-
is_listed, collateral_factor_mantissa = result
|
|
856
|
+
cache_key = f"cf_{mtoken}"
|
|
857
|
+
if cached := await self._cache.get(cache_key):
|
|
858
|
+
return True, cached
|
|
639
859
|
|
|
640
|
-
|
|
641
|
-
|
|
860
|
+
try:
|
|
861
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
862
|
+
contract = web3.eth.contract(
|
|
863
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
864
|
+
)
|
|
642
865
|
|
|
643
|
-
|
|
866
|
+
# markets() returns (isListed, collateralFactorMantissa)
|
|
867
|
+
result = await contract.functions.markets(mtoken).call(
|
|
868
|
+
block_identifier="pending"
|
|
869
|
+
)
|
|
870
|
+
is_listed, collateral_factor_mantissa = result
|
|
644
871
|
|
|
645
|
-
|
|
646
|
-
|
|
872
|
+
if not is_listed:
|
|
873
|
+
return False, f"Market {mtoken} is not listed"
|
|
647
874
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
last_error = str(exc)
|
|
651
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
652
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
653
|
-
await asyncio.sleep(wait_time)
|
|
654
|
-
continue
|
|
655
|
-
return False, last_error
|
|
875
|
+
collateral_factor = collateral_factor_mantissa / MANTISSA
|
|
876
|
+
await self._cache.set(cache_key, collateral_factor, ttl=3600)
|
|
656
877
|
|
|
657
|
-
|
|
878
|
+
return True, collateral_factor
|
|
879
|
+
except Exception as exc:
|
|
880
|
+
return False, str(exc)
|
|
658
881
|
|
|
659
882
|
async def get_apy(
|
|
660
883
|
self,
|
|
@@ -662,68 +885,53 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
662
885
|
mtoken: str,
|
|
663
886
|
apy_type: Literal["supply", "borrow"] = "supply",
|
|
664
887
|
include_rewards: bool = True,
|
|
665
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
666
888
|
) -> tuple[bool, float | str]:
|
|
667
|
-
mtoken =
|
|
889
|
+
mtoken = to_checksum_address(mtoken)
|
|
668
890
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
abi=REWARD_DISTRIBUTOR_ABI,
|
|
677
|
-
)
|
|
891
|
+
try:
|
|
892
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
893
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
894
|
+
reward_distributor = web3.eth.contract(
|
|
895
|
+
address=MOONWELL_REWARD_DISTRIBUTOR,
|
|
896
|
+
abi=REWARD_DISTRIBUTOR_ABI,
|
|
897
|
+
)
|
|
678
898
|
|
|
679
|
-
|
|
680
|
-
|
|
899
|
+
if apy_type == "supply":
|
|
900
|
+
rate_per_timestamp = (
|
|
901
|
+
await mtoken_contract.functions.supplyRatePerTimestamp().call(
|
|
681
902
|
block_identifier="pending"
|
|
682
903
|
)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
)
|
|
693
|
-
else:
|
|
694
|
-
rate_per_timestamp = await mtoken_contract.functions.borrowRatePerTimestamp().call(
|
|
904
|
+
)
|
|
905
|
+
mkt_config = await reward_distributor.functions.getAllMarketConfigs(
|
|
906
|
+
mtoken
|
|
907
|
+
).call(block_identifier="pending")
|
|
908
|
+
total_value = await mtoken_contract.functions.totalSupply().call(
|
|
909
|
+
block_identifier="pending"
|
|
910
|
+
)
|
|
911
|
+
else:
|
|
912
|
+
rate_per_timestamp = (
|
|
913
|
+
await mtoken_contract.functions.borrowRatePerTimestamp().call(
|
|
695
914
|
block_identifier="pending"
|
|
696
915
|
)
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
block_identifier="pending"
|
|
705
|
-
)
|
|
706
|
-
)
|
|
707
|
-
|
|
708
|
-
rate = rate_per_timestamp / MANTISSA
|
|
709
|
-
apy = _timestamp_rate_to_apy(rate)
|
|
916
|
+
)
|
|
917
|
+
mkt_config = await reward_distributor.functions.getAllMarketConfigs(
|
|
918
|
+
mtoken
|
|
919
|
+
).call(block_identifier="pending")
|
|
920
|
+
total_value = await mtoken_contract.functions.totalBorrows().call(
|
|
921
|
+
block_identifier="pending"
|
|
922
|
+
)
|
|
710
923
|
|
|
711
|
-
|
|
712
|
-
rewards_apr = await self._calculate_rewards_apr(
|
|
713
|
-
mtoken, mkt_config, total_value, apy_type
|
|
714
|
-
)
|
|
715
|
-
apy += rewards_apr
|
|
924
|
+
apy = _timestamp_rate_to_apy(rate_per_timestamp / MANTISSA)
|
|
716
925
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
await asyncio.sleep(wait_time)
|
|
723
|
-
continue
|
|
724
|
-
return False, last_error
|
|
926
|
+
if include_rewards and self.token_client and total_value > 0:
|
|
927
|
+
rewards_apr = await self._calculate_rewards_apr(
|
|
928
|
+
mtoken, mkt_config, total_value, apy_type
|
|
929
|
+
)
|
|
930
|
+
apy += rewards_apr
|
|
725
931
|
|
|
726
|
-
|
|
932
|
+
return True, apy
|
|
933
|
+
except Exception as exc:
|
|
934
|
+
return False, str(exc)
|
|
727
935
|
|
|
728
936
|
async def _calculate_rewards_apr(
|
|
729
937
|
self,
|
|
@@ -736,10 +944,12 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
736
944
|
return 0.0
|
|
737
945
|
|
|
738
946
|
try:
|
|
739
|
-
# Find WELL token config
|
|
740
947
|
well_config = None
|
|
741
948
|
for config in mkt_config:
|
|
742
|
-
if
|
|
949
|
+
if (
|
|
950
|
+
len(config) >= 6
|
|
951
|
+
and config[1].lower() == MOONWELL_WELL_TOKEN.lower()
|
|
952
|
+
):
|
|
743
953
|
well_config = config
|
|
744
954
|
break
|
|
745
955
|
|
|
@@ -758,18 +968,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
758
968
|
if well_rate == 0:
|
|
759
969
|
return 0.0
|
|
760
970
|
|
|
761
|
-
async with web3_from_chain_id(
|
|
971
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
762
972
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
763
973
|
underlying_addr = await mtoken_contract.functions.underlying().call(
|
|
764
974
|
block_identifier="pending"
|
|
765
975
|
)
|
|
766
976
|
|
|
767
|
-
well_key = f"{self.chain_name}_{self.well_token}"
|
|
768
|
-
underlying_key = f"{self.chain_name}_{underlying_addr}"
|
|
769
|
-
|
|
770
977
|
well_data, underlying_data = await asyncio.gather(
|
|
771
|
-
self.token_client.get_token_details(
|
|
772
|
-
|
|
978
|
+
self.token_client.get_token_details(
|
|
979
|
+
f"{CHAIN_NAME}_{MOONWELL_WELL_TOKEN}"
|
|
980
|
+
),
|
|
981
|
+
self.token_client.get_token_details(f"{CHAIN_NAME}_{underlying_addr}"),
|
|
773
982
|
)
|
|
774
983
|
|
|
775
984
|
well_price = (
|
|
@@ -809,42 +1018,34 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
809
1018
|
self,
|
|
810
1019
|
*,
|
|
811
1020
|
account: str | None = None,
|
|
812
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
813
1021
|
) -> tuple[bool, int | str]:
|
|
814
|
-
account =
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
for attempt in range(max_retries):
|
|
818
|
-
try:
|
|
819
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
820
|
-
contract = web3.eth.contract(
|
|
821
|
-
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
822
|
-
)
|
|
1022
|
+
account = (
|
|
1023
|
+
to_checksum_address(account) if account else self.strategy_wallet_address
|
|
1024
|
+
)
|
|
823
1025
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
block_identifier="pending"
|
|
830
|
-
)
|
|
1026
|
+
try:
|
|
1027
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
1028
|
+
contract = web3.eth.contract(
|
|
1029
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
1030
|
+
)
|
|
831
1031
|
|
|
832
|
-
|
|
833
|
-
|
|
1032
|
+
(
|
|
1033
|
+
error,
|
|
1034
|
+
liquidity,
|
|
1035
|
+
shortfall,
|
|
1036
|
+
) = await contract.functions.getAccountLiquidity(account).call(
|
|
1037
|
+
block_identifier="pending"
|
|
1038
|
+
)
|
|
834
1039
|
|
|
835
|
-
|
|
836
|
-
|
|
1040
|
+
if error != 0:
|
|
1041
|
+
return False, f"Comptroller error: {error}"
|
|
837
1042
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
last_error = str(exc)
|
|
841
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
842
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
843
|
-
await asyncio.sleep(wait_time)
|
|
844
|
-
continue
|
|
845
|
-
return False, last_error
|
|
1043
|
+
if shortfall > 0:
|
|
1044
|
+
return False, f"Account has shortfall: {shortfall}"
|
|
846
1045
|
|
|
847
|
-
|
|
1046
|
+
return True, liquidity
|
|
1047
|
+
except Exception as exc:
|
|
1048
|
+
return False, str(exc)
|
|
848
1049
|
|
|
849
1050
|
async def max_withdrawable_mtoken(
|
|
850
1051
|
self,
|
|
@@ -852,13 +1053,15 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
852
1053
|
mtoken: str,
|
|
853
1054
|
account: str | None = None,
|
|
854
1055
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
855
|
-
mtoken =
|
|
856
|
-
account =
|
|
1056
|
+
mtoken = to_checksum_address(mtoken)
|
|
1057
|
+
account = (
|
|
1058
|
+
to_checksum_address(account) if account else self.strategy_wallet_address
|
|
1059
|
+
)
|
|
857
1060
|
|
|
858
1061
|
try:
|
|
859
|
-
async with web3_from_chain_id(
|
|
1062
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
860
1063
|
comptroller = web3.eth.contract(
|
|
861
|
-
address=
|
|
1064
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
862
1065
|
)
|
|
863
1066
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
864
1067
|
|
|
@@ -894,13 +1097,11 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
894
1097
|
|
|
895
1098
|
u_dec = 18
|
|
896
1099
|
if self.token_client:
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
except Exception:
|
|
903
|
-
pass
|
|
1100
|
+
u_data = await self.token_client.get_token_details(
|
|
1101
|
+
f"{CHAIN_NAME}_{u_addr}"
|
|
1102
|
+
)
|
|
1103
|
+
if u_data:
|
|
1104
|
+
u_dec = u_data.get("decimals", 18)
|
|
904
1105
|
|
|
905
1106
|
# Binary search: largest cTokens you can redeem without shortfall
|
|
906
1107
|
lo, hi = 0, int(bal_raw)
|
|
@@ -949,125 +1150,26 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
949
1150
|
except Exception as exc:
|
|
950
1151
|
return False, str(exc)
|
|
951
1152
|
|
|
952
|
-
# ------------------------------------------------------------------ #
|
|
953
|
-
# Public API - ETH Wrapping #
|
|
954
|
-
# ------------------------------------------------------------------ #
|
|
955
|
-
|
|
956
1153
|
async def wrap_eth(
|
|
957
1154
|
self,
|
|
958
1155
|
*,
|
|
959
1156
|
amount: int,
|
|
960
1157
|
) -> tuple[bool, Any]:
|
|
961
|
-
strategy = self.
|
|
1158
|
+
strategy = self.strategy_wallet_address
|
|
962
1159
|
amount = int(amount)
|
|
963
1160
|
if amount <= 0:
|
|
964
1161
|
return False, "amount must be positive"
|
|
965
1162
|
|
|
966
|
-
|
|
967
|
-
target=
|
|
1163
|
+
transaction = await encode_call(
|
|
1164
|
+
target=BASE_WETH,
|
|
968
1165
|
abi=WETH_ABI,
|
|
969
1166
|
fn_name="deposit",
|
|
970
1167
|
args=[],
|
|
971
1168
|
from_address=strategy,
|
|
1169
|
+
chain_id=CHAIN_ID_BASE,
|
|
972
1170
|
value=amount,
|
|
973
1171
|
)
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
# ------------------------------------------------------------------ #
|
|
977
|
-
# Helpers #
|
|
978
|
-
# ------------------------------------------------------------------ #
|
|
979
|
-
|
|
980
|
-
# Max uint256 for unlimited approvals
|
|
981
|
-
MAX_UINT256 = 2**256 - 1
|
|
982
|
-
|
|
983
|
-
async def _send_tx(self, tx: dict[str, Any]) -> tuple[bool, Any]:
|
|
984
|
-
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
985
|
-
return True, txn_hash
|
|
986
|
-
|
|
987
|
-
async def _ensure_allowance(
|
|
988
|
-
self,
|
|
989
|
-
*,
|
|
990
|
-
token_address: str,
|
|
991
|
-
owner: str,
|
|
992
|
-
spender: str,
|
|
993
|
-
amount: int,
|
|
994
|
-
) -> tuple[bool, Any]:
|
|
995
|
-
allowance = await get_token_allowance(
|
|
996
|
-
token_address, self.chain_id, owner, spender
|
|
997
|
-
)
|
|
998
|
-
if allowance >= amount:
|
|
999
|
-
return True, {}
|
|
1000
|
-
|
|
1001
|
-
# Approve for max uint256 to avoid precision/timing issues
|
|
1002
|
-
approve_tx = await build_approve_transaction(
|
|
1003
|
-
from_address=owner,
|
|
1004
|
-
chain_id=self.chain_id,
|
|
1005
|
-
token_address=token_address,
|
|
1006
|
-
spender_address=spender,
|
|
1007
|
-
amount=self.MAX_UINT256,
|
|
1172
|
+
txn_hash = await send_transaction(
|
|
1173
|
+
transaction, self.strategy_wallet_signing_callback
|
|
1008
1174
|
)
|
|
1009
|
-
|
|
1010
|
-
result = await self._send_tx(approve_tx)
|
|
1011
|
-
|
|
1012
|
-
# Small delay after approval to ensure state is propagated on providers/chains
|
|
1013
|
-
# where we don't wait for additional confirmations by default.
|
|
1014
|
-
if result[0]:
|
|
1015
|
-
confirmations = 0
|
|
1016
|
-
if isinstance(result[1], dict):
|
|
1017
|
-
try:
|
|
1018
|
-
confirmations = int(result[1].get("confirmations") or 0)
|
|
1019
|
-
except (TypeError, ValueError):
|
|
1020
|
-
confirmations = 0
|
|
1021
|
-
if confirmations == 0:
|
|
1022
|
-
await asyncio.sleep(1.0)
|
|
1023
|
-
|
|
1024
|
-
return result
|
|
1025
|
-
|
|
1026
|
-
async def _encode_call(
|
|
1027
|
-
self,
|
|
1028
|
-
*,
|
|
1029
|
-
target: str,
|
|
1030
|
-
abi: list[dict[str, Any]],
|
|
1031
|
-
fn_name: str,
|
|
1032
|
-
args: list[Any],
|
|
1033
|
-
from_address: str,
|
|
1034
|
-
value: int = 0,
|
|
1035
|
-
) -> dict[str, Any]:
|
|
1036
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
1037
|
-
contract = web3.eth.contract(address=target, abi=abi)
|
|
1038
|
-
|
|
1039
|
-
try:
|
|
1040
|
-
tx_data = await getattr(contract.functions, fn_name)(
|
|
1041
|
-
*args
|
|
1042
|
-
).build_transaction({"from": from_address})
|
|
1043
|
-
data = tx_data["data"]
|
|
1044
|
-
except ValueError as exc:
|
|
1045
|
-
raise ValueError(f"Failed to encode {fn_name}: {exc}") from exc
|
|
1046
|
-
|
|
1047
|
-
tx: dict[str, Any] = {
|
|
1048
|
-
"chainId": int(self.chain_id),
|
|
1049
|
-
"from": to_checksum_address(from_address),
|
|
1050
|
-
"to": to_checksum_address(target),
|
|
1051
|
-
"data": data,
|
|
1052
|
-
"value": int(value),
|
|
1053
|
-
}
|
|
1054
|
-
return tx
|
|
1055
|
-
|
|
1056
|
-
def _strategy_address(self) -> str:
|
|
1057
|
-
addr = None
|
|
1058
|
-
if isinstance(self.strategy_wallet, dict):
|
|
1059
|
-
addr = self.strategy_wallet.get("address") or (
|
|
1060
|
-
(self.strategy_wallet.get("evm") or {}).get("address")
|
|
1061
|
-
)
|
|
1062
|
-
elif isinstance(self.strategy_wallet, str):
|
|
1063
|
-
addr = self.strategy_wallet
|
|
1064
|
-
if not addr:
|
|
1065
|
-
raise ValueError(
|
|
1066
|
-
"strategy_wallet address is required for Moonwell operations"
|
|
1067
|
-
)
|
|
1068
|
-
return to_checksum_address(addr)
|
|
1069
|
-
|
|
1070
|
-
def _checksum(self, address: str | None) -> str:
|
|
1071
|
-
if not address:
|
|
1072
|
-
raise ValueError("Missing required contract address in Moonwell config")
|
|
1073
|
-
return to_checksum_address(address)
|
|
1175
|
+
return (True, txn_hash)
|