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
|
@@ -1,23 +1,21 @@
|
|
|
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
|
|
11
13
|
from wayfinder_paths.core.constants.base import MANTISSA, MAX_UINT256, SECONDS_PER_YEAR
|
|
12
14
|
from wayfinder_paths.core.constants.chains import CHAIN_ID_BASE
|
|
13
15
|
from wayfinder_paths.core.constants.contracts import (
|
|
14
|
-
BASE_USDC,
|
|
15
16
|
BASE_WETH,
|
|
16
|
-
BASE_WSTETH,
|
|
17
17
|
MOONWELL_COMPTROLLER,
|
|
18
18
|
MOONWELL_M_USDC,
|
|
19
|
-
MOONWELL_M_WETH,
|
|
20
|
-
MOONWELL_M_WSTETH,
|
|
21
19
|
MOONWELL_REWARD_DISTRIBUTOR,
|
|
22
20
|
MOONWELL_WELL_TOKEN,
|
|
23
21
|
)
|
|
@@ -28,30 +26,10 @@ from wayfinder_paths.core.constants.moonwell_abi import (
|
|
|
28
26
|
WETH_ABI,
|
|
29
27
|
)
|
|
30
28
|
from wayfinder_paths.core.utils.tokens import ensure_allowance
|
|
31
|
-
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
29
|
+
from wayfinder_paths.core.utils.transaction import encode_call, send_transaction
|
|
32
30
|
from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
"m_usdc": MOONWELL_M_USDC,
|
|
36
|
-
"m_weth": MOONWELL_M_WETH,
|
|
37
|
-
"m_wsteth": MOONWELL_M_WSTETH,
|
|
38
|
-
"usdc": BASE_USDC,
|
|
39
|
-
"weth": BASE_WETH,
|
|
40
|
-
"wsteth": BASE_WSTETH,
|
|
41
|
-
"reward_distributor": MOONWELL_REWARD_DISTRIBUTOR,
|
|
42
|
-
"comptroller": MOONWELL_COMPTROLLER,
|
|
43
|
-
"well_token": MOONWELL_WELL_TOKEN,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
BASE_CHAIN_ID = CHAIN_ID_BASE
|
|
47
|
-
CF_CACHE_TTL = 3600
|
|
48
|
-
DEFAULT_MAX_RETRIES = 5
|
|
49
|
-
DEFAULT_BASE_DELAY = 3.0
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _is_rate_limit_error(error: Exception | str) -> bool:
|
|
53
|
-
error_str = str(error)
|
|
54
|
-
return "429" in error_str or "Too Many Requests" in error_str
|
|
32
|
+
CHAIN_NAME = "base"
|
|
55
33
|
|
|
56
34
|
|
|
57
35
|
def _timestamp_rate_to_apy(rate: float) -> float:
|
|
@@ -61,6 +39,69 @@ def _timestamp_rate_to_apy(rate: float) -> float:
|
|
|
61
39
|
class MoonwellAdapter(BaseAdapter):
|
|
62
40
|
adapter_type = "MOONWELL"
|
|
63
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
|
+
|
|
64
105
|
def __init__(
|
|
65
106
|
self,
|
|
66
107
|
config: dict[str, Any] | None = None,
|
|
@@ -69,53 +110,20 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
69
110
|
) -> None:
|
|
70
111
|
super().__init__("moonwell_adapter", config)
|
|
71
112
|
cfg = config or {}
|
|
72
|
-
adapter_cfg = cfg.get("moonwell_adapter") or {}
|
|
73
113
|
|
|
74
114
|
self.token_client = token_client
|
|
75
115
|
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
76
116
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
self.
|
|
82
|
-
self.
|
|
83
|
-
self.chain_name = "base"
|
|
84
|
-
|
|
85
|
-
# Protocol addresses (with config overrides)
|
|
86
|
-
self.comptroller_address = to_checksum_address(
|
|
87
|
-
adapter_cfg.get("comptroller") or MOONWELL_DEFAULTS["comptroller"]
|
|
88
|
-
)
|
|
89
|
-
self.reward_distributor_address = to_checksum_address(
|
|
90
|
-
adapter_cfg.get("reward_distributor")
|
|
91
|
-
or MOONWELL_DEFAULTS["reward_distributor"]
|
|
92
|
-
)
|
|
93
|
-
self.well_token = to_checksum_address(
|
|
94
|
-
adapter_cfg.get("well_token") or MOONWELL_DEFAULTS["well_token"]
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
# Token addresses
|
|
98
|
-
self.m_usdc = to_checksum_address(
|
|
99
|
-
adapter_cfg.get("m_usdc") or MOONWELL_DEFAULTS["m_usdc"]
|
|
100
|
-
)
|
|
101
|
-
self.m_weth = to_checksum_address(
|
|
102
|
-
adapter_cfg.get("m_weth") or MOONWELL_DEFAULTS["m_weth"]
|
|
103
|
-
)
|
|
104
|
-
self.m_wsteth = to_checksum_address(
|
|
105
|
-
adapter_cfg.get("m_wsteth") or MOONWELL_DEFAULTS["m_wsteth"]
|
|
106
|
-
)
|
|
107
|
-
self.usdc = to_checksum_address(
|
|
108
|
-
adapter_cfg.get("usdc") or MOONWELL_DEFAULTS["usdc"]
|
|
109
|
-
)
|
|
110
|
-
self.weth = to_checksum_address(
|
|
111
|
-
adapter_cfg.get("weth") or MOONWELL_DEFAULTS["weth"]
|
|
112
|
-
)
|
|
113
|
-
self.wsteth = to_checksum_address(
|
|
114
|
-
adapter_cfg.get("wsteth") or MOONWELL_DEFAULTS["wsteth"]
|
|
115
|
-
)
|
|
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
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
self.
|
|
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)
|
|
119
127
|
|
|
120
128
|
async def lend(
|
|
121
129
|
self,
|
|
@@ -132,28 +140,29 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
132
140
|
mtoken = to_checksum_address(mtoken)
|
|
133
141
|
underlying_token = to_checksum_address(underlying_token)
|
|
134
142
|
|
|
135
|
-
# Approve mToken to spend underlying tokens
|
|
136
143
|
approved = await ensure_allowance(
|
|
137
144
|
token_address=underlying_token,
|
|
138
145
|
owner=strategy,
|
|
139
146
|
spender=mtoken,
|
|
140
147
|
amount=amount,
|
|
141
|
-
chain_id=
|
|
148
|
+
chain_id=CHAIN_ID_BASE,
|
|
142
149
|
signing_callback=self.strategy_wallet_signing_callback,
|
|
143
150
|
approval_amount=MAX_UINT256,
|
|
144
151
|
)
|
|
145
152
|
if not approved[0]:
|
|
146
153
|
return approved
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
tx = await self._encode_call(
|
|
155
|
+
transaction = await encode_call(
|
|
150
156
|
target=mtoken,
|
|
151
157
|
abi=MTOKEN_ABI,
|
|
152
158
|
fn_name="mint",
|
|
153
159
|
args=[amount],
|
|
154
160
|
from_address=strategy,
|
|
161
|
+
chain_id=CHAIN_ID_BASE,
|
|
162
|
+
)
|
|
163
|
+
txn_hash = await send_transaction(
|
|
164
|
+
transaction, self.strategy_wallet_signing_callback
|
|
155
165
|
)
|
|
156
|
-
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
157
166
|
return (True, txn_hash)
|
|
158
167
|
|
|
159
168
|
async def unlend(
|
|
@@ -169,15 +178,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
169
178
|
|
|
170
179
|
mtoken = to_checksum_address(mtoken)
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
tx = await self._encode_call(
|
|
181
|
+
transaction = await encode_call(
|
|
174
182
|
target=mtoken,
|
|
175
183
|
abi=MTOKEN_ABI,
|
|
176
184
|
fn_name="redeem",
|
|
177
185
|
args=[amount],
|
|
178
186
|
from_address=strategy,
|
|
187
|
+
chain_id=CHAIN_ID_BASE,
|
|
188
|
+
)
|
|
189
|
+
txn_hash = await send_transaction(
|
|
190
|
+
transaction, self.strategy_wallet_signing_callback
|
|
179
191
|
)
|
|
180
|
-
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
181
192
|
return (True, txn_hash)
|
|
182
193
|
|
|
183
194
|
async def borrow(
|
|
@@ -193,68 +204,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
193
204
|
|
|
194
205
|
mtoken = to_checksum_address(mtoken)
|
|
195
206
|
|
|
196
|
-
|
|
197
|
-
try:
|
|
198
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
199
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
200
|
-
|
|
201
|
-
borrow_before = await mtoken_contract.functions.borrowBalanceStored(
|
|
202
|
-
strategy
|
|
203
|
-
).call(block_identifier="pending")
|
|
204
|
-
|
|
205
|
-
# Simulate borrow to check for errors before submitting
|
|
206
|
-
try:
|
|
207
|
-
borrow_return = await mtoken_contract.functions.borrow(amount).call(
|
|
208
|
-
{"from": strategy}, block_identifier="pending"
|
|
209
|
-
)
|
|
210
|
-
if borrow_return != 0:
|
|
211
|
-
self.logger.warning(
|
|
212
|
-
f"Borrow simulation returned error code {borrow_return}. "
|
|
213
|
-
"Codes: 3=COMPTROLLER_REJECTION, 9=INVALID_ACCOUNT_PAIR, "
|
|
214
|
-
"14=INSUFFICIENT_LIQUIDITY"
|
|
215
|
-
)
|
|
216
|
-
except Exception as call_err:
|
|
217
|
-
self.logger.debug(f"Borrow simulation failed: {call_err}")
|
|
218
|
-
except Exception as e:
|
|
219
|
-
self.logger.warning(f"Failed to get pre-borrow balance: {e}")
|
|
220
|
-
|
|
221
|
-
tx = await self._encode_call(
|
|
207
|
+
transaction = await encode_call(
|
|
222
208
|
target=mtoken,
|
|
223
209
|
abi=MTOKEN_ABI,
|
|
224
210
|
fn_name="borrow",
|
|
225
211
|
args=[amount],
|
|
226
212
|
from_address=strategy,
|
|
213
|
+
chain_id=CHAIN_ID_BASE,
|
|
214
|
+
)
|
|
215
|
+
txn_hash = await send_transaction(
|
|
216
|
+
transaction, self.strategy_wallet_signing_callback
|
|
227
217
|
)
|
|
228
|
-
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
229
|
-
|
|
230
|
-
# Verify the borrow actually succeeded by checking balance increased
|
|
231
|
-
try:
|
|
232
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
233
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
234
|
-
borrow_after = await mtoken_contract.functions.borrowBalanceStored(
|
|
235
|
-
strategy
|
|
236
|
-
).call(block_identifier="pending")
|
|
237
|
-
|
|
238
|
-
# Borrow balance should have increased by approximately the amount
|
|
239
|
-
# Allow for some interest accrual
|
|
240
|
-
expected_increase = amount * 0.99
|
|
241
|
-
actual_increase = borrow_after - borrow_before
|
|
242
|
-
|
|
243
|
-
if actual_increase < expected_increase:
|
|
244
|
-
self.logger.error(
|
|
245
|
-
f"Borrow verification failed: balance only increased by "
|
|
246
|
-
f"{actual_increase} (expected ~{amount}). "
|
|
247
|
-
f"Moonwell likely returned an error code. "
|
|
248
|
-
f"Before: {borrow_before}, After: {borrow_after}"
|
|
249
|
-
)
|
|
250
|
-
return (
|
|
251
|
-
False,
|
|
252
|
-
f"Borrow failed: balance did not increase as expected. "
|
|
253
|
-
f"Before: {borrow_before}, After: {borrow_after}, Expected: +{amount}",
|
|
254
|
-
)
|
|
255
|
-
except Exception as e:
|
|
256
|
-
self.logger.warning(f"Could not verify borrow balance: {e}")
|
|
257
|
-
|
|
258
218
|
return (True, txn_hash)
|
|
259
219
|
|
|
260
220
|
async def repay(
|
|
@@ -273,31 +233,32 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
273
233
|
mtoken = to_checksum_address(mtoken)
|
|
274
234
|
underlying_token = to_checksum_address(underlying_token)
|
|
275
235
|
|
|
276
|
-
# Approve mToken to spend underlying tokens for repayment
|
|
277
|
-
# When repay_full=True, approve the amount we have, Moonwell will use only what's needed
|
|
278
236
|
approved = await ensure_allowance(
|
|
279
237
|
token_address=underlying_token,
|
|
280
238
|
owner=strategy,
|
|
281
239
|
spender=mtoken,
|
|
282
240
|
amount=amount,
|
|
283
|
-
chain_id=
|
|
241
|
+
chain_id=CHAIN_ID_BASE,
|
|
284
242
|
signing_callback=self.strategy_wallet_signing_callback,
|
|
285
243
|
approval_amount=MAX_UINT256,
|
|
286
244
|
)
|
|
287
245
|
if not approved[0]:
|
|
288
246
|
return approved
|
|
289
247
|
|
|
290
|
-
#
|
|
248
|
+
# max uint256 avoids balance calculation race conditions
|
|
291
249
|
repay_amount = MAX_UINT256 if repay_full else amount
|
|
292
250
|
|
|
293
|
-
|
|
251
|
+
transaction = await encode_call(
|
|
294
252
|
target=mtoken,
|
|
295
253
|
abi=MTOKEN_ABI,
|
|
296
254
|
fn_name="repayBorrow",
|
|
297
255
|
args=[repay_amount],
|
|
298
256
|
from_address=strategy,
|
|
257
|
+
chain_id=CHAIN_ID_BASE,
|
|
258
|
+
)
|
|
259
|
+
txn_hash = await send_transaction(
|
|
260
|
+
transaction, self.strategy_wallet_signing_callback
|
|
299
261
|
)
|
|
300
|
-
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
301
262
|
return (True, txn_hash)
|
|
302
263
|
|
|
303
264
|
async def set_collateral(
|
|
@@ -308,20 +269,22 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
308
269
|
strategy = self.strategy_wallet_address
|
|
309
270
|
mtoken = to_checksum_address(mtoken)
|
|
310
271
|
|
|
311
|
-
|
|
312
|
-
target=
|
|
272
|
+
transaction = await encode_call(
|
|
273
|
+
target=MOONWELL_COMPTROLLER,
|
|
313
274
|
abi=COMPTROLLER_ABI,
|
|
314
275
|
fn_name="enterMarkets",
|
|
315
276
|
args=[[mtoken]],
|
|
316
277
|
from_address=strategy,
|
|
278
|
+
chain_id=CHAIN_ID_BASE,
|
|
279
|
+
)
|
|
280
|
+
txn_hash = await send_transaction(
|
|
281
|
+
transaction, self.strategy_wallet_signing_callback
|
|
317
282
|
)
|
|
318
|
-
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
319
283
|
|
|
320
|
-
# Verify the market was actually entered
|
|
321
284
|
try:
|
|
322
|
-
async with web3_from_chain_id(
|
|
285
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
323
286
|
comptroller = web3.eth.contract(
|
|
324
|
-
address=
|
|
287
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
325
288
|
)
|
|
326
289
|
is_member = await comptroller.functions.checkMembership(
|
|
327
290
|
strategy, mtoken
|
|
@@ -355,9 +318,9 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
355
318
|
)
|
|
356
319
|
mtoken = to_checksum_address(mtoken)
|
|
357
320
|
|
|
358
|
-
async with web3_from_chain_id(
|
|
321
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
359
322
|
comptroller = web3.eth.contract(
|
|
360
|
-
address=
|
|
323
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
361
324
|
)
|
|
362
325
|
is_member = await comptroller.functions.checkMembership(
|
|
363
326
|
acct, mtoken
|
|
@@ -374,14 +337,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
374
337
|
strategy = self.strategy_wallet_address
|
|
375
338
|
mtoken = to_checksum_address(mtoken)
|
|
376
339
|
|
|
377
|
-
|
|
378
|
-
target=
|
|
340
|
+
transaction = await encode_call(
|
|
341
|
+
target=MOONWELL_COMPTROLLER,
|
|
379
342
|
abi=COMPTROLLER_ABI,
|
|
380
343
|
fn_name="exitMarket",
|
|
381
344
|
args=[mtoken],
|
|
382
345
|
from_address=strategy,
|
|
346
|
+
chain_id=CHAIN_ID_BASE,
|
|
347
|
+
)
|
|
348
|
+
txn_hash = await send_transaction(
|
|
349
|
+
transaction, self.strategy_wallet_signing_callback
|
|
383
350
|
)
|
|
384
|
-
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
385
351
|
return (True, txn_hash)
|
|
386
352
|
|
|
387
353
|
async def claim_rewards(
|
|
@@ -393,7 +359,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
393
359
|
|
|
394
360
|
rewards = await self._get_outstanding_rewards(strategy)
|
|
395
361
|
|
|
396
|
-
# Skip if no rewards to claim
|
|
397
362
|
if not rewards:
|
|
398
363
|
return True, {}
|
|
399
364
|
|
|
@@ -402,22 +367,22 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
402
367
|
if total_usd < min_rewards_usd:
|
|
403
368
|
return True, {}
|
|
404
369
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
target=self.comptroller_address,
|
|
370
|
+
transaction = await encode_call(
|
|
371
|
+
target=MOONWELL_COMPTROLLER,
|
|
408
372
|
abi=COMPTROLLER_ABI,
|
|
409
373
|
fn_name="claimReward",
|
|
410
374
|
args=[strategy],
|
|
411
375
|
from_address=strategy,
|
|
376
|
+
chain_id=CHAIN_ID_BASE,
|
|
412
377
|
)
|
|
413
|
-
await send_transaction(
|
|
378
|
+
await send_transaction(transaction, self.strategy_wallet_signing_callback)
|
|
414
379
|
return True, rewards
|
|
415
380
|
|
|
416
381
|
async def _get_outstanding_rewards(self, account: str) -> dict[str, int]:
|
|
417
382
|
try:
|
|
418
|
-
async with web3_from_chain_id(
|
|
383
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
419
384
|
contract = web3.eth.contract(
|
|
420
|
-
address=
|
|
385
|
+
address=MOONWELL_REWARD_DISTRIBUTOR, abi=REWARD_DISTRIBUTOR_ABI
|
|
421
386
|
)
|
|
422
387
|
|
|
423
388
|
all_rewards = await contract.functions.getOutstandingRewardsForUser(
|
|
@@ -426,15 +391,12 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
426
391
|
|
|
427
392
|
rewards: dict[str, int] = {}
|
|
428
393
|
for mtoken_data in all_rewards:
|
|
429
|
-
# mtoken_data is (mToken, [(rewardToken, totalReward, supplySide, borrowSide)])
|
|
430
394
|
if len(mtoken_data) >= 2:
|
|
431
|
-
|
|
432
|
-
for reward_info in token_rewards:
|
|
395
|
+
for reward_info in mtoken_data[1]:
|
|
433
396
|
if len(reward_info) >= 2:
|
|
434
|
-
token_addr = reward_info
|
|
435
|
-
total_reward = reward_info[1]
|
|
397
|
+
token_addr, total_reward, *_ = reward_info
|
|
436
398
|
if total_reward > 0:
|
|
437
|
-
key = f"{
|
|
399
|
+
key = f"{CHAIN_NAME}_{token_addr}"
|
|
438
400
|
rewards[key] = rewards.get(key, 0) + total_reward
|
|
439
401
|
return rewards
|
|
440
402
|
except Exception:
|
|
@@ -446,23 +408,345 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
446
408
|
|
|
447
409
|
total_usd = 0.0
|
|
448
410
|
for token_key, amount in rewards.items():
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
total_usd += (amount / (10**decimals)) * price
|
|
455
|
-
except Exception:
|
|
456
|
-
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
|
|
457
416
|
return total_usd
|
|
458
417
|
|
|
418
|
+
# ------------------------------------------------------------------ #
|
|
419
|
+
# Public API - Position & Market Data #
|
|
420
|
+
# ------------------------------------------------------------------ #
|
|
421
|
+
|
|
422
|
+
async def get_full_user_state(
|
|
423
|
+
self,
|
|
424
|
+
*,
|
|
425
|
+
account: str | None = None,
|
|
426
|
+
include_rewards: bool = True,
|
|
427
|
+
include_usd: bool = False,
|
|
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
|
|
432
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
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
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
447
|
+
multicall = MulticallAdapter(chain_id=self.chain_id, web3=web3)
|
|
448
|
+
|
|
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
|
+
)
|
|
456
|
+
|
|
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
|
+
)
|
|
480
|
+
)
|
|
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
|
+
]
|
|
586
|
+
)
|
|
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
|
|
650
|
+
continue
|
|
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
|
+
|
|
459
744
|
async def get_pos(
|
|
460
745
|
self,
|
|
461
746
|
*,
|
|
462
747
|
mtoken: str,
|
|
463
748
|
account: str | None = None,
|
|
464
749
|
include_usd: bool = False,
|
|
465
|
-
max_retries: int = 3,
|
|
466
750
|
block_identifier: int | str | None = None,
|
|
467
751
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
468
752
|
mtoken = to_checksum_address(mtoken)
|
|
@@ -471,54 +755,37 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
471
755
|
)
|
|
472
756
|
block_id = block_identifier if block_identifier is not None else "pending"
|
|
473
757
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
rewards_contract = web3.eth.contract(
|
|
482
|
-
address=self.reward_distributor_address,
|
|
483
|
-
abi=REWARD_DISTRIBUTOR_ABI,
|
|
484
|
-
)
|
|
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
|
+
)
|
|
485
765
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
)
|
|
504
|
-
break
|
|
505
|
-
except Exception as exc:
|
|
506
|
-
last_error = str(exc)
|
|
507
|
-
if "429" in last_error or "Too Many Requests" in last_error:
|
|
508
|
-
if attempt < max_retries - 1:
|
|
509
|
-
wait_time = 2 ** (attempt + 1)
|
|
510
|
-
await asyncio.sleep(wait_time)
|
|
511
|
-
continue
|
|
512
|
-
return False, last_error
|
|
513
|
-
else:
|
|
514
|
-
# All retries exhausted
|
|
515
|
-
return False, last_error
|
|
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)
|
|
516
783
|
|
|
517
784
|
try:
|
|
518
785
|
reward_balances = self._process_rewards(rewards)
|
|
519
786
|
|
|
520
|
-
mtoken_key = f"{
|
|
521
|
-
underlying_key = f"{
|
|
787
|
+
mtoken_key = f"{CHAIN_NAME}_{mtoken}"
|
|
788
|
+
underlying_key = f"{CHAIN_NAME}_{underlying}"
|
|
522
789
|
|
|
523
790
|
balances: dict[str, int] = {mtoken_key: bal}
|
|
524
791
|
balances.update(reward_balances)
|
|
@@ -537,7 +804,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
537
804
|
|
|
538
805
|
if include_usd and self.token_client:
|
|
539
806
|
usd_balances = await self._calculate_usd_balances(
|
|
540
|
-
balances, underlying_key
|
|
807
|
+
balances, underlying_key
|
|
541
808
|
)
|
|
542
809
|
result["usd_balances"] = usd_balances
|
|
543
810
|
|
|
@@ -549,39 +816,31 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
549
816
|
result: dict[str, int] = {}
|
|
550
817
|
for reward_info in rewards:
|
|
551
818
|
if len(reward_info) >= 2:
|
|
552
|
-
token_addr = reward_info
|
|
553
|
-
total_reward = reward_info[1]
|
|
819
|
+
token_addr, total_reward, *_ = reward_info
|
|
554
820
|
if total_reward > 0:
|
|
555
|
-
key = f"{
|
|
821
|
+
key = f"{CHAIN_NAME}_{token_addr}"
|
|
556
822
|
result[key] = total_reward
|
|
557
823
|
return result
|
|
558
824
|
|
|
559
825
|
async def _calculate_usd_balances(
|
|
560
|
-
self, balances: dict[str, int], underlying_key: str
|
|
826
|
+
self, balances: dict[str, int], underlying_key: str
|
|
561
827
|
) -> dict[str, float | None]:
|
|
562
828
|
if not self.token_client:
|
|
563
829
|
return {}
|
|
564
830
|
|
|
565
|
-
tokens = set(balances.keys()) | {underlying_key}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
token_key
|
|
571
|
-
)
|
|
572
|
-
except Exception:
|
|
573
|
-
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))
|
|
574
836
|
|
|
575
837
|
usd_balances: dict[str, float | None] = {}
|
|
576
838
|
for token_key, bal in balances.items():
|
|
577
839
|
data = token_data.get(token_key)
|
|
578
|
-
if data:
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
usd_balances[token_key] = (bal / (10**decimals)) * price
|
|
583
|
-
else:
|
|
584
|
-
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
|
|
585
844
|
else:
|
|
586
845
|
usd_balances[token_key] = None
|
|
587
846
|
|
|
@@ -591,48 +850,34 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
591
850
|
self,
|
|
592
851
|
*,
|
|
593
852
|
mtoken: str,
|
|
594
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
595
853
|
) -> tuple[bool, float | str]:
|
|
596
854
|
mtoken = to_checksum_address(mtoken)
|
|
597
855
|
|
|
598
|
-
|
|
599
|
-
if
|
|
600
|
-
|
|
601
|
-
if now - cached_time < CF_CACHE_TTL:
|
|
602
|
-
return True, cached_value
|
|
603
|
-
|
|
604
|
-
last_error = ""
|
|
605
|
-
for attempt in range(max_retries):
|
|
606
|
-
try:
|
|
607
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
608
|
-
contract = web3.eth.contract(
|
|
609
|
-
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
# markets() returns (isListed, collateralFactorMantissa)
|
|
613
|
-
result = await contract.functions.markets(mtoken).call(
|
|
614
|
-
block_identifier="pending"
|
|
615
|
-
)
|
|
616
|
-
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
|
|
617
859
|
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
)
|
|
620
865
|
|
|
621
|
-
|
|
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
|
|
622
871
|
|
|
623
|
-
|
|
624
|
-
|
|
872
|
+
if not is_listed:
|
|
873
|
+
return False, f"Market {mtoken} is not listed"
|
|
625
874
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
last_error = str(exc)
|
|
629
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
630
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
631
|
-
await asyncio.sleep(wait_time)
|
|
632
|
-
continue
|
|
633
|
-
return False, last_error
|
|
875
|
+
collateral_factor = collateral_factor_mantissa / MANTISSA
|
|
876
|
+
await self._cache.set(cache_key, collateral_factor, ttl=3600)
|
|
634
877
|
|
|
635
|
-
|
|
878
|
+
return True, collateral_factor
|
|
879
|
+
except Exception as exc:
|
|
880
|
+
return False, str(exc)
|
|
636
881
|
|
|
637
882
|
async def get_apy(
|
|
638
883
|
self,
|
|
@@ -640,68 +885,53 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
640
885
|
mtoken: str,
|
|
641
886
|
apy_type: Literal["supply", "borrow"] = "supply",
|
|
642
887
|
include_rewards: bool = True,
|
|
643
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
644
888
|
) -> tuple[bool, float | str]:
|
|
645
889
|
mtoken = to_checksum_address(mtoken)
|
|
646
890
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
abi=REWARD_DISTRIBUTOR_ABI,
|
|
655
|
-
)
|
|
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
|
+
)
|
|
656
898
|
|
|
657
|
-
|
|
658
|
-
|
|
899
|
+
if apy_type == "supply":
|
|
900
|
+
rate_per_timestamp = (
|
|
901
|
+
await mtoken_contract.functions.supplyRatePerTimestamp().call(
|
|
659
902
|
block_identifier="pending"
|
|
660
903
|
)
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
)
|
|
671
|
-
else:
|
|
672
|
-
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(
|
|
673
914
|
block_identifier="pending"
|
|
674
915
|
)
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
block_identifier="pending"
|
|
683
|
-
)
|
|
684
|
-
)
|
|
685
|
-
|
|
686
|
-
rate = rate_per_timestamp / MANTISSA
|
|
687
|
-
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
|
+
)
|
|
688
923
|
|
|
689
|
-
|
|
690
|
-
rewards_apr = await self._calculate_rewards_apr(
|
|
691
|
-
mtoken, mkt_config, total_value, apy_type
|
|
692
|
-
)
|
|
693
|
-
apy += rewards_apr
|
|
924
|
+
apy = _timestamp_rate_to_apy(rate_per_timestamp / MANTISSA)
|
|
694
925
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
await asyncio.sleep(wait_time)
|
|
701
|
-
continue
|
|
702
|
-
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
|
|
703
931
|
|
|
704
|
-
|
|
932
|
+
return True, apy
|
|
933
|
+
except Exception as exc:
|
|
934
|
+
return False, str(exc)
|
|
705
935
|
|
|
706
936
|
async def _calculate_rewards_apr(
|
|
707
937
|
self,
|
|
@@ -714,10 +944,12 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
714
944
|
return 0.0
|
|
715
945
|
|
|
716
946
|
try:
|
|
717
|
-
# Find WELL token config
|
|
718
947
|
well_config = None
|
|
719
948
|
for config in mkt_config:
|
|
720
|
-
if
|
|
949
|
+
if (
|
|
950
|
+
len(config) >= 6
|
|
951
|
+
and config[1].lower() == MOONWELL_WELL_TOKEN.lower()
|
|
952
|
+
):
|
|
721
953
|
well_config = config
|
|
722
954
|
break
|
|
723
955
|
|
|
@@ -736,18 +968,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
736
968
|
if well_rate == 0:
|
|
737
969
|
return 0.0
|
|
738
970
|
|
|
739
|
-
async with web3_from_chain_id(
|
|
971
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
740
972
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
741
973
|
underlying_addr = await mtoken_contract.functions.underlying().call(
|
|
742
974
|
block_identifier="pending"
|
|
743
975
|
)
|
|
744
976
|
|
|
745
|
-
well_key = f"{self.chain_name}_{self.well_token}"
|
|
746
|
-
underlying_key = f"{self.chain_name}_{underlying_addr}"
|
|
747
|
-
|
|
748
977
|
well_data, underlying_data = await asyncio.gather(
|
|
749
|
-
self.token_client.get_token_details(
|
|
750
|
-
|
|
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}"),
|
|
751
982
|
)
|
|
752
983
|
|
|
753
984
|
well_price = (
|
|
@@ -787,44 +1018,34 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
787
1018
|
self,
|
|
788
1019
|
*,
|
|
789
1020
|
account: str | None = None,
|
|
790
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
791
1021
|
) -> tuple[bool, int | str]:
|
|
792
1022
|
account = (
|
|
793
1023
|
to_checksum_address(account) if account else self.strategy_wallet_address
|
|
794
1024
|
)
|
|
795
1025
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
802
|
-
)
|
|
803
|
-
|
|
804
|
-
(
|
|
805
|
-
error,
|
|
806
|
-
liquidity,
|
|
807
|
-
shortfall,
|
|
808
|
-
) = await contract.functions.getAccountLiquidity(account).call(
|
|
809
|
-
block_identifier="pending"
|
|
810
|
-
)
|
|
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
|
+
)
|
|
811
1031
|
|
|
812
|
-
|
|
813
|
-
|
|
1032
|
+
(
|
|
1033
|
+
error,
|
|
1034
|
+
liquidity,
|
|
1035
|
+
shortfall,
|
|
1036
|
+
) = await contract.functions.getAccountLiquidity(account).call(
|
|
1037
|
+
block_identifier="pending"
|
|
1038
|
+
)
|
|
814
1039
|
|
|
815
|
-
|
|
816
|
-
|
|
1040
|
+
if error != 0:
|
|
1041
|
+
return False, f"Comptroller error: {error}"
|
|
817
1042
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
last_error = str(exc)
|
|
821
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
822
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
823
|
-
await asyncio.sleep(wait_time)
|
|
824
|
-
continue
|
|
825
|
-
return False, last_error
|
|
1043
|
+
if shortfall > 0:
|
|
1044
|
+
return False, f"Account has shortfall: {shortfall}"
|
|
826
1045
|
|
|
827
|
-
|
|
1046
|
+
return True, liquidity
|
|
1047
|
+
except Exception as exc:
|
|
1048
|
+
return False, str(exc)
|
|
828
1049
|
|
|
829
1050
|
async def max_withdrawable_mtoken(
|
|
830
1051
|
self,
|
|
@@ -838,9 +1059,9 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
838
1059
|
)
|
|
839
1060
|
|
|
840
1061
|
try:
|
|
841
|
-
async with web3_from_chain_id(
|
|
1062
|
+
async with web3_from_chain_id(CHAIN_ID_BASE) as web3:
|
|
842
1063
|
comptroller = web3.eth.contract(
|
|
843
|
-
address=
|
|
1064
|
+
address=MOONWELL_COMPTROLLER, abi=COMPTROLLER_ABI
|
|
844
1065
|
)
|
|
845
1066
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
846
1067
|
|
|
@@ -876,13 +1097,11 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
876
1097
|
|
|
877
1098
|
u_dec = 18
|
|
878
1099
|
if self.token_client:
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
except Exception:
|
|
885
|
-
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)
|
|
886
1105
|
|
|
887
1106
|
# Binary search: largest cTokens you can redeem without shortfall
|
|
888
1107
|
lo, hi = 0, int(bal_raw)
|
|
@@ -941,43 +1160,16 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
941
1160
|
if amount <= 0:
|
|
942
1161
|
return False, "amount must be positive"
|
|
943
1162
|
|
|
944
|
-
|
|
945
|
-
target=
|
|
1163
|
+
transaction = await encode_call(
|
|
1164
|
+
target=BASE_WETH,
|
|
946
1165
|
abi=WETH_ABI,
|
|
947
1166
|
fn_name="deposit",
|
|
948
1167
|
args=[],
|
|
949
1168
|
from_address=strategy,
|
|
1169
|
+
chain_id=CHAIN_ID_BASE,
|
|
950
1170
|
value=amount,
|
|
951
1171
|
)
|
|
952
|
-
txn_hash = await send_transaction(
|
|
1172
|
+
txn_hash = await send_transaction(
|
|
1173
|
+
transaction, self.strategy_wallet_signing_callback
|
|
1174
|
+
)
|
|
953
1175
|
return (True, txn_hash)
|
|
954
|
-
|
|
955
|
-
async def _encode_call(
|
|
956
|
-
self,
|
|
957
|
-
*,
|
|
958
|
-
target: str,
|
|
959
|
-
abi: list[dict[str, Any]],
|
|
960
|
-
fn_name: str,
|
|
961
|
-
args: list[Any],
|
|
962
|
-
from_address: str,
|
|
963
|
-
value: int = 0,
|
|
964
|
-
) -> dict[str, Any]:
|
|
965
|
-
async with web3_from_chain_id(self.chain_id) as web3:
|
|
966
|
-
contract = web3.eth.contract(address=target, abi=abi)
|
|
967
|
-
|
|
968
|
-
try:
|
|
969
|
-
tx_data = await getattr(contract.functions, fn_name)(
|
|
970
|
-
*args
|
|
971
|
-
).build_transaction({"from": from_address})
|
|
972
|
-
data = tx_data["data"]
|
|
973
|
-
except ValueError as exc:
|
|
974
|
-
raise ValueError(f"Failed to encode {fn_name}: {exc}") from exc
|
|
975
|
-
|
|
976
|
-
tx: dict[str, Any] = {
|
|
977
|
-
"chainId": int(self.chain_id),
|
|
978
|
-
"from": to_checksum_address(from_address),
|
|
979
|
-
"to": to_checksum_address(target),
|
|
980
|
-
"data": data,
|
|
981
|
-
"value": int(value),
|
|
982
|
-
}
|
|
983
|
-
return tx
|