wayfinder-paths 0.1.15__py3-none-any.whl → 0.1.16__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/README.md +19 -20
- wayfinder_paths/adapters/balance_adapter/adapter.py +66 -37
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +2 -8
- wayfinder_paths/adapters/brap_adapter/README.md +22 -19
- wayfinder_paths/adapters/brap_adapter/adapter.py +33 -34
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +2 -18
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -56
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +1 -8
- wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +301 -662
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +275 -179
- wayfinder_paths/core/config.py +8 -47
- wayfinder_paths/core/constants/base.py +0 -1
- wayfinder_paths/core/constants/erc20_abi.py +13 -13
- wayfinder_paths/core/strategies/Strategy.py +6 -2
- wayfinder_paths/core/utils/erc20_service.py +100 -0
- wayfinder_paths/core/utils/evm_helpers.py +1 -1
- wayfinder_paths/core/utils/transaction.py +191 -0
- wayfinder_paths/core/utils/web3.py +66 -0
- wayfinder_paths/run_strategy.py +37 -6
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +200 -224
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +128 -151
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +52 -78
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +0 -1
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +39 -64
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +42 -85
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
- wayfinder_paths/templates/strategy/README.md +1 -5
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +35 -44
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/sdk_example.py +0 -125
- wayfinder_paths/core/engine/__init__.py +0 -5
- wayfinder_paths/core/services/__init__.py +0 -0
- wayfinder_paths/core/services/base.py +0 -131
- wayfinder_paths/core/services/local_evm_txn.py +0 -350
- wayfinder_paths/core/services/local_token_txn.py +0 -238
- wayfinder_paths/core/services/web3_service.py +0 -43
- wayfinder_paths/core/wallets/README.md +0 -88
- wayfinder_paths/core/wallets/WalletManager.py +0 -56
- wayfinder_paths/core/wallets/__init__.py +0 -7
- wayfinder_paths/scripts/run_strategy.py +0 -152
- wayfinder_paths/strategies/config.py +0 -85
- {wayfinder_paths-0.1.15.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
|
@@ -15,15 +15,18 @@ from eth_utils import to_checksum_address
|
|
|
15
15
|
|
|
16
16
|
from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
|
|
17
17
|
from wayfinder_paths.core.clients.TokenClient import TokenClient
|
|
18
|
-
from wayfinder_paths.core.constants.base import DEFAULT_TRANSACTION_TIMEOUT
|
|
19
|
-
from wayfinder_paths.core.constants.erc20_abi import ERC20_ABI
|
|
20
18
|
from wayfinder_paths.core.constants.moonwell_abi import (
|
|
21
19
|
COMPTROLLER_ABI,
|
|
22
20
|
MTOKEN_ABI,
|
|
23
21
|
REWARD_DISTRIBUTOR_ABI,
|
|
24
22
|
WETH_ABI,
|
|
25
23
|
)
|
|
26
|
-
from wayfinder_paths.core.
|
|
24
|
+
from wayfinder_paths.core.utils.erc20_service import (
|
|
25
|
+
build_approve_transaction,
|
|
26
|
+
get_token_allowance,
|
|
27
|
+
)
|
|
28
|
+
from wayfinder_paths.core.utils.transaction import send_transaction
|
|
29
|
+
from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
27
30
|
|
|
28
31
|
# Moonwell Base chain addresses
|
|
29
32
|
MOONWELL_DEFAULTS = {
|
|
@@ -58,11 +61,6 @@ CF_CACHE_TTL = 3600
|
|
|
58
61
|
DEFAULT_MAX_RETRIES = 5
|
|
59
62
|
DEFAULT_BASE_DELAY = 3.0 # seconds
|
|
60
63
|
|
|
61
|
-
# Compound-style Failure(uint256,uint256,uint256) topic0
|
|
62
|
-
FAILURE_EVENT_TOPIC0 = (
|
|
63
|
-
"0x45b96fe442630264581b197e84bbada861235052c5a1aadfff9ea4e40a969aa0"
|
|
64
|
-
)
|
|
65
|
-
|
|
66
64
|
|
|
67
65
|
def _is_rate_limit_error(error: Exception | str) -> bool:
|
|
68
66
|
"""Check if an error is a rate limit (429) error."""
|
|
@@ -70,38 +68,6 @@ def _is_rate_limit_error(error: Exception | str) -> bool:
|
|
|
70
68
|
return "429" in error_str or "Too Many Requests" in error_str
|
|
71
69
|
|
|
72
70
|
|
|
73
|
-
async def _retry_with_backoff(
|
|
74
|
-
coro_factory,
|
|
75
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
76
|
-
base_delay: float = DEFAULT_BASE_DELAY,
|
|
77
|
-
):
|
|
78
|
-
"""Retry an async operation with exponential backoff on rate limit errors.
|
|
79
|
-
|
|
80
|
-
Args:
|
|
81
|
-
coro_factory: A callable that returns a new coroutine each time.
|
|
82
|
-
max_retries: Maximum number of retry attempts.
|
|
83
|
-
base_delay: Base delay in seconds (doubles each retry).
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
The result of the coroutine if successful.
|
|
87
|
-
|
|
88
|
-
Raises:
|
|
89
|
-
The last exception if all retries fail.
|
|
90
|
-
"""
|
|
91
|
-
last_error = None
|
|
92
|
-
for attempt in range(max_retries):
|
|
93
|
-
try:
|
|
94
|
-
return await coro_factory()
|
|
95
|
-
except Exception as exc:
|
|
96
|
-
last_error = exc
|
|
97
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
98
|
-
wait_time = base_delay * (2**attempt)
|
|
99
|
-
await asyncio.sleep(wait_time)
|
|
100
|
-
continue
|
|
101
|
-
raise
|
|
102
|
-
raise last_error
|
|
103
|
-
|
|
104
|
-
|
|
105
71
|
def _timestamp_rate_to_apy(rate: float) -> float:
|
|
106
72
|
"""Convert a per-second rate to APY."""
|
|
107
73
|
return (1 + rate) ** SECONDS_PER_YEAR - 1
|
|
@@ -115,20 +81,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
115
81
|
def __init__(
|
|
116
82
|
self,
|
|
117
83
|
config: dict[str, Any] | None = None,
|
|
118
|
-
web3_service: Web3Service | None = None,
|
|
119
84
|
token_client: TokenClient | None = None,
|
|
120
85
|
simulation: bool = False,
|
|
86
|
+
strategy_wallet_signing_callback=None,
|
|
121
87
|
) -> None:
|
|
122
88
|
super().__init__("moonwell_adapter", config)
|
|
123
89
|
cfg = config or {}
|
|
124
90
|
adapter_cfg = cfg.get("moonwell_adapter") or {}
|
|
125
91
|
|
|
126
|
-
self.web3 = web3_service
|
|
127
92
|
self.simulation = simulation
|
|
128
93
|
self.token_client = token_client
|
|
129
|
-
self.
|
|
130
|
-
web3_service.token_transactions if web3_service else None
|
|
131
|
-
)
|
|
94
|
+
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
132
95
|
|
|
133
96
|
self.strategy_wallet = cfg.get("strategy_wallet") or {}
|
|
134
97
|
self.chain_id = adapter_cfg.get("chain_id", BASE_CHAIN_ID)
|
|
@@ -169,79 +132,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
169
132
|
# Public API - Lending Operations #
|
|
170
133
|
# ------------------------------------------------------------------ #
|
|
171
134
|
|
|
172
|
-
def _tx_pinned_block(self, result: Any) -> int | None:
|
|
173
|
-
if isinstance(result, dict):
|
|
174
|
-
return (
|
|
175
|
-
result.get("confirmed_block_number")
|
|
176
|
-
or result.get("block_number")
|
|
177
|
-
or (result.get("receipt", {}) or {}).get("blockNumber")
|
|
178
|
-
)
|
|
179
|
-
return None
|
|
180
|
-
|
|
181
|
-
def _as_bytes(self, value: Any) -> bytes | None:
|
|
182
|
-
if value is None:
|
|
183
|
-
return None
|
|
184
|
-
if isinstance(value, (bytes, bytearray)):
|
|
185
|
-
return bytes(value)
|
|
186
|
-
if hasattr(value, "hex"):
|
|
187
|
-
try:
|
|
188
|
-
hex_str = value.hex()
|
|
189
|
-
if isinstance(hex_str, str):
|
|
190
|
-
if hex_str.startswith("0x"):
|
|
191
|
-
return bytes.fromhex(hex_str[2:])
|
|
192
|
-
return bytes.fromhex(hex_str)
|
|
193
|
-
except Exception:
|
|
194
|
-
return None
|
|
195
|
-
if isinstance(value, str):
|
|
196
|
-
v = value
|
|
197
|
-
if v.startswith("0x"):
|
|
198
|
-
v = v[2:]
|
|
199
|
-
try:
|
|
200
|
-
return bytes.fromhex(v)
|
|
201
|
-
except Exception:
|
|
202
|
-
return None
|
|
203
|
-
return None
|
|
204
|
-
|
|
205
|
-
def _failure_event_details(
|
|
206
|
-
self, result: Any, contract_address: str
|
|
207
|
-
) -> dict[str, int] | None:
|
|
208
|
-
if not isinstance(result, dict):
|
|
209
|
-
return None
|
|
210
|
-
|
|
211
|
-
receipt = (
|
|
212
|
-
result.get("receipt") if isinstance(result.get("receipt"), dict) else {}
|
|
213
|
-
)
|
|
214
|
-
logs = receipt.get("logs") if isinstance(receipt, dict) else None
|
|
215
|
-
if not isinstance(logs, list):
|
|
216
|
-
return None
|
|
217
|
-
|
|
218
|
-
addr_l = str(contract_address or "").lower()
|
|
219
|
-
for log in logs:
|
|
220
|
-
if not isinstance(log, dict):
|
|
221
|
-
continue
|
|
222
|
-
if str(log.get("address") or "").lower() != addr_l:
|
|
223
|
-
continue
|
|
224
|
-
topics = log.get("topics") or []
|
|
225
|
-
if not topics:
|
|
226
|
-
continue
|
|
227
|
-
topic0_bytes = self._as_bytes(topics[0])
|
|
228
|
-
if not topic0_bytes:
|
|
229
|
-
continue
|
|
230
|
-
if topic0_bytes.hex() != FAILURE_EVENT_TOPIC0.lower().removeprefix("0x"):
|
|
231
|
-
continue
|
|
232
|
-
|
|
233
|
-
data_b = self._as_bytes(log.get("data"))
|
|
234
|
-
if not data_b or len(data_b) < 96:
|
|
235
|
-
return {"error": 0, "info": 0, "detail": 0}
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
"error": int.from_bytes(data_b[0:32], "big"),
|
|
239
|
-
"info": int.from_bytes(data_b[32:64], "big"),
|
|
240
|
-
"detail": int.from_bytes(data_b[64:96], "big"),
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return None
|
|
244
|
-
|
|
245
135
|
async def lend(
|
|
246
136
|
self,
|
|
247
137
|
*,
|
|
@@ -249,11 +139,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
249
139
|
underlying_token: str,
|
|
250
140
|
amount: int,
|
|
251
141
|
) -> tuple[bool, Any]:
|
|
252
|
-
"""Supply tokens to Moonwell by minting mTokens.
|
|
253
|
-
|
|
254
|
-
Note: mint() returns an error code and can emit Failure without reverting.
|
|
255
|
-
We verify success by checking that the mToken balance increased.
|
|
256
|
-
"""
|
|
142
|
+
"""Supply tokens to Moonwell by minting mTokens."""
|
|
257
143
|
strategy = self._strategy_address()
|
|
258
144
|
amount = int(amount)
|
|
259
145
|
if amount <= 0:
|
|
@@ -262,17 +148,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
262
148
|
mtoken = self._checksum(mtoken)
|
|
263
149
|
underlying_token = self._checksum(underlying_token)
|
|
264
150
|
|
|
265
|
-
mtoken_bal_before = None
|
|
266
|
-
if self.web3 and not self.simulation:
|
|
267
|
-
try:
|
|
268
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
269
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
270
|
-
mtoken_bal_before = await mtoken_contract.functions.balanceOf(
|
|
271
|
-
strategy
|
|
272
|
-
).call()
|
|
273
|
-
except Exception:
|
|
274
|
-
mtoken_bal_before = None
|
|
275
|
-
|
|
276
151
|
# Approve mToken to spend underlying tokens
|
|
277
152
|
approved = await self._ensure_allowance(
|
|
278
153
|
token_address=underlying_token,
|
|
@@ -291,41 +166,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
291
166
|
args=[amount],
|
|
292
167
|
from_address=strategy,
|
|
293
168
|
)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if not result[0] or not self.web3 or self.simulation:
|
|
297
|
-
return result
|
|
298
|
-
|
|
299
|
-
if isinstance(result[1], dict):
|
|
300
|
-
failure = self._failure_event_details(result[1], mtoken)
|
|
301
|
-
if failure is not None:
|
|
302
|
-
return (
|
|
303
|
-
False,
|
|
304
|
-
f"Mint failed (Failure event): error={failure['error']} info={failure['info']} detail={failure['detail']}",
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
if mtoken_bal_before is None:
|
|
308
|
-
return result
|
|
309
|
-
|
|
310
|
-
try:
|
|
311
|
-
pinned_block = self._tx_pinned_block(result[1])
|
|
312
|
-
block_id = pinned_block if pinned_block is not None else "latest"
|
|
313
|
-
|
|
314
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
315
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
316
|
-
mtoken_bal_after = await mtoken_contract.functions.balanceOf(strategy).call(
|
|
317
|
-
block_identifier=block_id
|
|
318
|
-
)
|
|
319
|
-
if int(mtoken_bal_after) <= int(mtoken_bal_before):
|
|
320
|
-
return (
|
|
321
|
-
False,
|
|
322
|
-
f"Mint verification failed: mToken balance did not increase (before={mtoken_bal_before}, after={mtoken_bal_after})",
|
|
323
|
-
)
|
|
324
|
-
except Exception:
|
|
325
|
-
# If verification fails due to RPC/ABI issues, keep original result.
|
|
326
|
-
return result
|
|
327
|
-
|
|
328
|
-
return result
|
|
169
|
+
return await self._send_tx(tx)
|
|
329
170
|
|
|
330
171
|
async def unlend(
|
|
331
172
|
self,
|
|
@@ -333,13 +174,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
333
174
|
mtoken: str,
|
|
334
175
|
amount: int,
|
|
335
176
|
) -> tuple[bool, Any]:
|
|
336
|
-
"""Withdraw tokens from Moonwell by redeeming mTokens.
|
|
337
|
-
|
|
338
|
-
Note: redeem() returns an error code and can emit Failure without reverting.
|
|
339
|
-
We verify success by checking that either:
|
|
340
|
-
- underlying wallet balance increased, or
|
|
341
|
-
- mToken wallet balance decreased.
|
|
342
|
-
"""
|
|
177
|
+
"""Withdraw tokens from Moonwell by redeeming mTokens."""
|
|
343
178
|
strategy = self._strategy_address()
|
|
344
179
|
amount = int(amount)
|
|
345
180
|
if amount <= 0:
|
|
@@ -347,38 +182,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
347
182
|
|
|
348
183
|
mtoken = self._checksum(mtoken)
|
|
349
184
|
|
|
350
|
-
mtoken_bal_before = None
|
|
351
|
-
underlying_addr = None
|
|
352
|
-
underlying_bal_before = None
|
|
353
|
-
|
|
354
|
-
if self.web3 and not self.simulation:
|
|
355
|
-
try:
|
|
356
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
357
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
358
|
-
|
|
359
|
-
# Snapshot balances for verification.
|
|
360
|
-
mtoken_bal_before = await mtoken_contract.functions.balanceOf(
|
|
361
|
-
strategy
|
|
362
|
-
).call()
|
|
363
|
-
try:
|
|
364
|
-
underlying_addr = (
|
|
365
|
-
await mtoken_contract.functions.underlying().call()
|
|
366
|
-
)
|
|
367
|
-
except Exception:
|
|
368
|
-
underlying_addr = None
|
|
369
|
-
if underlying_addr:
|
|
370
|
-
underlying_contract = web3.eth.contract(
|
|
371
|
-
address=to_checksum_address(underlying_addr),
|
|
372
|
-
abi=ERC20_ABI,
|
|
373
|
-
)
|
|
374
|
-
underlying_bal_before = (
|
|
375
|
-
await underlying_contract.functions.balanceOf(strategy).call()
|
|
376
|
-
)
|
|
377
|
-
except Exception:
|
|
378
|
-
mtoken_bal_before = None
|
|
379
|
-
underlying_addr = None
|
|
380
|
-
underlying_bal_before = None
|
|
381
|
-
|
|
382
185
|
# Redeem mTokens for underlying
|
|
383
186
|
tx = await self._encode_call(
|
|
384
187
|
target=mtoken,
|
|
@@ -387,60 +190,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
387
190
|
args=[amount],
|
|
388
191
|
from_address=strategy,
|
|
389
192
|
)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if not result[0] or not self.web3 or self.simulation:
|
|
393
|
-
return result
|
|
394
|
-
|
|
395
|
-
if isinstance(result[1], dict):
|
|
396
|
-
failure = self._failure_event_details(result[1], mtoken)
|
|
397
|
-
if failure is not None:
|
|
398
|
-
return (
|
|
399
|
-
False,
|
|
400
|
-
f"Redeem failed (Failure event): error={failure['error']} info={failure['info']} detail={failure['detail']}",
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
if mtoken_bal_before is None:
|
|
404
|
-
return result
|
|
405
|
-
|
|
406
|
-
try:
|
|
407
|
-
pinned_block = self._tx_pinned_block(result[1])
|
|
408
|
-
block_id = pinned_block if pinned_block is not None else "latest"
|
|
409
|
-
|
|
410
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
411
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
412
|
-
mtoken_bal_after = await mtoken_contract.functions.balanceOf(strategy).call(
|
|
413
|
-
block_identifier=block_id
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
underlying_bal_after = None
|
|
417
|
-
if underlying_addr:
|
|
418
|
-
underlying_contract = web3.eth.contract(
|
|
419
|
-
address=to_checksum_address(underlying_addr),
|
|
420
|
-
abi=ERC20_ABI,
|
|
421
|
-
)
|
|
422
|
-
underlying_bal_after = await underlying_contract.functions.balanceOf(
|
|
423
|
-
strategy
|
|
424
|
-
).call(block_identifier=block_id)
|
|
425
|
-
|
|
426
|
-
mtoken_decreased = int(mtoken_bal_after) < int(mtoken_bal_before)
|
|
427
|
-
underlying_increased = (
|
|
428
|
-
underlying_bal_before is not None
|
|
429
|
-
and underlying_bal_after is not None
|
|
430
|
-
and int(underlying_bal_after) > int(underlying_bal_before)
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
if not mtoken_decreased and not underlying_increased:
|
|
434
|
-
return (
|
|
435
|
-
False,
|
|
436
|
-
"Redeem verification failed: no observed balance change "
|
|
437
|
-
f"(mtoken before={mtoken_bal_before}, after={mtoken_bal_after}; "
|
|
438
|
-
f"underlying before={underlying_bal_before}, after={underlying_bal_after})",
|
|
439
|
-
)
|
|
440
|
-
except Exception:
|
|
441
|
-
return result
|
|
442
|
-
|
|
443
|
-
return result
|
|
193
|
+
return await self._send_tx(tx)
|
|
444
194
|
|
|
445
195
|
# ------------------------------------------------------------------ #
|
|
446
196
|
# Public API - Borrowing Operations #
|
|
@@ -470,19 +220,18 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
470
220
|
|
|
471
221
|
# Get borrow balance before the transaction for verification
|
|
472
222
|
borrow_before = 0
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
223
|
+
try:
|
|
224
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
476
225
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
477
226
|
|
|
478
227
|
borrow_before = await mtoken_contract.functions.borrowBalanceStored(
|
|
479
228
|
strategy
|
|
480
|
-
).call()
|
|
229
|
+
).call(block_identifier="pending")
|
|
481
230
|
|
|
482
231
|
# Simulate borrow to check for errors before submitting
|
|
483
232
|
try:
|
|
484
233
|
borrow_return = await mtoken_contract.functions.borrow(amount).call(
|
|
485
|
-
{"from": strategy}
|
|
234
|
+
{"from": strategy}, block_identifier="pending"
|
|
486
235
|
)
|
|
487
236
|
if borrow_return != 0:
|
|
488
237
|
logger.warning(
|
|
@@ -492,9 +241,8 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
492
241
|
)
|
|
493
242
|
except Exception as call_err:
|
|
494
243
|
logger.debug(f"Borrow simulation failed: {call_err}")
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
logger.warning(f"Failed to get pre-borrow balance: {e}")
|
|
244
|
+
except Exception as e:
|
|
245
|
+
logger.warning(f"Failed to get pre-borrow balance: {e}")
|
|
498
246
|
|
|
499
247
|
tx = await self._encode_call(
|
|
500
248
|
target=mtoken,
|
|
@@ -503,28 +251,18 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
503
251
|
args=[amount],
|
|
504
252
|
from_address=strategy,
|
|
505
253
|
)
|
|
506
|
-
result = await self.
|
|
254
|
+
result = await self._send_tx(tx)
|
|
507
255
|
|
|
508
256
|
if not result[0]:
|
|
509
257
|
return result
|
|
510
258
|
|
|
511
259
|
# Verify the borrow actually succeeded by checking balance increased
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
pinned_block = None
|
|
515
|
-
if isinstance(result[1], dict):
|
|
516
|
-
pinned_block = (
|
|
517
|
-
result[1].get("confirmed_block_number")
|
|
518
|
-
or result[1].get("block_number")
|
|
519
|
-
or (result[1].get("receipt", {}) or {}).get("blockNumber")
|
|
520
|
-
)
|
|
521
|
-
block_id = pinned_block if pinned_block is not None else "latest"
|
|
522
|
-
|
|
523
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
260
|
+
try:
|
|
261
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
524
262
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
525
263
|
borrow_after = await mtoken_contract.functions.borrowBalanceStored(
|
|
526
264
|
strategy
|
|
527
|
-
).call(block_identifier=
|
|
265
|
+
).call(block_identifier="pending")
|
|
528
266
|
|
|
529
267
|
# Borrow balance should have increased by approximately the amount
|
|
530
268
|
# Allow for some interest accrual
|
|
@@ -545,11 +283,11 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
545
283
|
f"Borrow failed: balance did not increase as expected. "
|
|
546
284
|
f"Before: {borrow_before}, After: {borrow_after}, Expected: +{amount}",
|
|
547
285
|
)
|
|
548
|
-
|
|
549
|
-
|
|
286
|
+
except Exception as e:
|
|
287
|
+
from loguru import logger
|
|
550
288
|
|
|
551
|
-
|
|
552
|
-
|
|
289
|
+
logger.warning(f"Could not verify borrow balance: {e}")
|
|
290
|
+
# Continue with the original result if verification fails
|
|
553
291
|
|
|
554
292
|
return result
|
|
555
293
|
|
|
@@ -577,17 +315,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
577
315
|
mtoken = self._checksum(mtoken)
|
|
578
316
|
underlying_token = self._checksum(underlying_token)
|
|
579
317
|
|
|
580
|
-
borrow_before = None
|
|
581
|
-
if self.web3 and not self.simulation:
|
|
582
|
-
try:
|
|
583
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
584
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
585
|
-
borrow_before = await mtoken_contract.functions.borrowBalanceStored(
|
|
586
|
-
strategy
|
|
587
|
-
).call()
|
|
588
|
-
except Exception:
|
|
589
|
-
borrow_before = None
|
|
590
|
-
|
|
591
318
|
# Approve mToken to spend underlying tokens for repayment
|
|
592
319
|
# When repay_full=True, approve the amount we have, Moonwell will use only what's needed
|
|
593
320
|
approved = await self._ensure_allowance(
|
|
@@ -609,49 +336,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
609
336
|
args=[repay_amount],
|
|
610
337
|
from_address=strategy,
|
|
611
338
|
)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if not result[0] or not self.web3 or self.simulation:
|
|
615
|
-
return result
|
|
616
|
-
|
|
617
|
-
if isinstance(result[1], dict):
|
|
618
|
-
failure = self._failure_event_details(result[1], mtoken)
|
|
619
|
-
if failure is not None:
|
|
620
|
-
return (
|
|
621
|
-
False,
|
|
622
|
-
f"Repay failed (Failure event): error={failure['error']} info={failure['info']} detail={failure['detail']}",
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
if borrow_before is None or int(borrow_before) <= 0:
|
|
626
|
-
return result
|
|
627
|
-
|
|
628
|
-
try:
|
|
629
|
-
pinned_block = self._tx_pinned_block(result[1])
|
|
630
|
-
block_id = pinned_block if pinned_block is not None else "latest"
|
|
631
|
-
|
|
632
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
633
|
-
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
634
|
-
borrow_after = await mtoken_contract.functions.borrowBalanceStored(
|
|
635
|
-
strategy
|
|
636
|
-
).call(block_identifier=block_id)
|
|
637
|
-
|
|
638
|
-
if repay_full:
|
|
639
|
-
# Full repayment should clear the borrow balance (allow 1 wei dust).
|
|
640
|
-
if int(borrow_after) > 1:
|
|
641
|
-
return (
|
|
642
|
-
False,
|
|
643
|
-
f"Repay verification failed: repay_full did not clear debt (before={borrow_before}, after={borrow_after})",
|
|
644
|
-
)
|
|
645
|
-
else:
|
|
646
|
-
if int(borrow_after) >= int(borrow_before):
|
|
647
|
-
return (
|
|
648
|
-
False,
|
|
649
|
-
f"Repay verification failed: borrow balance did not decrease (before={borrow_before}, after={borrow_after})",
|
|
650
|
-
)
|
|
651
|
-
except Exception:
|
|
652
|
-
return result
|
|
653
|
-
|
|
654
|
-
return result
|
|
339
|
+
return await self._send_tx(tx)
|
|
655
340
|
|
|
656
341
|
# ------------------------------------------------------------------ #
|
|
657
342
|
# Public API - Collateral Management #
|
|
@@ -677,30 +362,20 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
677
362
|
args=[[mtoken]],
|
|
678
363
|
from_address=strategy,
|
|
679
364
|
)
|
|
680
|
-
result = await self.
|
|
365
|
+
result = await self._send_tx(tx)
|
|
681
366
|
|
|
682
367
|
if not result[0]:
|
|
683
368
|
return result
|
|
684
369
|
|
|
685
370
|
# Verify the market was actually entered
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
pinned_block = None
|
|
689
|
-
if isinstance(result[1], dict):
|
|
690
|
-
pinned_block = (
|
|
691
|
-
result[1].get("confirmed_block_number")
|
|
692
|
-
or result[1].get("block_number")
|
|
693
|
-
or (result[1].get("receipt", {}) or {}).get("blockNumber")
|
|
694
|
-
)
|
|
695
|
-
block_id = pinned_block if pinned_block is not None else "latest"
|
|
696
|
-
|
|
697
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
371
|
+
try:
|
|
372
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
698
373
|
comptroller = web3.eth.contract(
|
|
699
374
|
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
700
375
|
)
|
|
701
376
|
is_member = await comptroller.functions.checkMembership(
|
|
702
377
|
strategy, mtoken
|
|
703
|
-
).call(block_identifier=
|
|
378
|
+
).call(block_identifier="pending")
|
|
704
379
|
|
|
705
380
|
if not is_member:
|
|
706
381
|
from loguru import logger
|
|
@@ -713,10 +388,10 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
713
388
|
False,
|
|
714
389
|
f"enterMarkets succeeded but account is not a member of market {mtoken}",
|
|
715
390
|
)
|
|
716
|
-
|
|
717
|
-
|
|
391
|
+
except Exception as e:
|
|
392
|
+
from loguru import logger
|
|
718
393
|
|
|
719
|
-
|
|
394
|
+
logger.warning(f"Could not verify market membership: {e}")
|
|
720
395
|
|
|
721
396
|
return result
|
|
722
397
|
|
|
@@ -729,19 +404,19 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
729
404
|
"""Check whether an account has entered a given market (as collateral / borrowing market)."""
|
|
730
405
|
if self.simulation:
|
|
731
406
|
return True, True
|
|
732
|
-
if not self.web3:
|
|
733
|
-
return False, "web3 service not configured"
|
|
734
407
|
|
|
735
408
|
try:
|
|
736
409
|
acct = self._checksum(account) if account else self._strategy_address()
|
|
737
410
|
mtoken = self._checksum(mtoken)
|
|
738
411
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
412
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
413
|
+
comptroller = web3.eth.contract(
|
|
414
|
+
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
415
|
+
)
|
|
416
|
+
is_member = await comptroller.functions.checkMembership(
|
|
417
|
+
acct, mtoken
|
|
418
|
+
).call()
|
|
419
|
+
return True, bool(is_member)
|
|
745
420
|
except Exception as exc:
|
|
746
421
|
return False, str(exc)
|
|
747
422
|
|
|
@@ -761,7 +436,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
761
436
|
args=[mtoken],
|
|
762
437
|
from_address=strategy,
|
|
763
438
|
)
|
|
764
|
-
return await self.
|
|
439
|
+
return await self._send_tx(tx)
|
|
765
440
|
|
|
766
441
|
# ------------------------------------------------------------------ #
|
|
767
442
|
# Public API - Rewards #
|
|
@@ -796,7 +471,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
796
471
|
args=[strategy],
|
|
797
472
|
from_address=strategy,
|
|
798
473
|
)
|
|
799
|
-
result = await self.
|
|
474
|
+
result = await self._send_tx(tx)
|
|
800
475
|
if not result[0]:
|
|
801
476
|
return result
|
|
802
477
|
|
|
@@ -804,33 +479,30 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
804
479
|
|
|
805
480
|
async def _get_outstanding_rewards(self, account: str) -> dict[str, int]:
|
|
806
481
|
"""Get outstanding rewards for an account across all markets."""
|
|
807
|
-
if not self.web3:
|
|
808
|
-
return {}
|
|
809
|
-
|
|
810
482
|
try:
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
483
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
484
|
+
contract = web3.eth.contract(
|
|
485
|
+
address=self.reward_distributor_address, abi=REWARD_DISTRIBUTOR_ABI
|
|
486
|
+
)
|
|
815
487
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
488
|
+
# Call getOutstandingRewardsForUser(user)
|
|
489
|
+
all_rewards = await contract.functions.getOutstandingRewardsForUser(
|
|
490
|
+
account
|
|
491
|
+
).call(block_identifier="pending")
|
|
492
|
+
|
|
493
|
+
rewards: dict[str, int] = {}
|
|
494
|
+
for mtoken_data in all_rewards:
|
|
495
|
+
# mtoken_data is (mToken, [(rewardToken, totalReward, supplySide, borrowSide)])
|
|
496
|
+
if len(mtoken_data) >= 2:
|
|
497
|
+
token_rewards = mtoken_data[1] if len(mtoken_data) > 1 else []
|
|
498
|
+
for reward_info in token_rewards:
|
|
499
|
+
if len(reward_info) >= 2:
|
|
500
|
+
token_addr = reward_info[0]
|
|
501
|
+
total_reward = reward_info[1]
|
|
502
|
+
if total_reward > 0:
|
|
503
|
+
key = f"{self.chain_name}_{token_addr}"
|
|
504
|
+
rewards[key] = rewards.get(key, 0) + total_reward
|
|
505
|
+
return rewards
|
|
834
506
|
except Exception:
|
|
835
507
|
return {}
|
|
836
508
|
|
|
@@ -874,45 +546,45 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
874
546
|
block_identifier: Block to query at. Can be:
|
|
875
547
|
- int: specific block number (for pinning to tx block)
|
|
876
548
|
- "safe": OP Stack safe block (data posted to L1)
|
|
877
|
-
- None/"
|
|
549
|
+
- None/"pending": current head (default)
|
|
878
550
|
|
|
879
551
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
880
552
|
"""
|
|
881
|
-
if not self.web3:
|
|
882
|
-
return False, "web3 service not configured"
|
|
883
|
-
|
|
884
553
|
mtoken = self._checksum(mtoken)
|
|
885
554
|
account = self._checksum(account) if account else self._strategy_address()
|
|
886
|
-
block_id = block_identifier if block_identifier is not None else "
|
|
555
|
+
block_id = block_identifier if block_identifier is not None else "pending"
|
|
887
556
|
|
|
888
557
|
bal = exch = borrow = underlying = rewards = None
|
|
889
558
|
last_error = ""
|
|
890
559
|
|
|
891
560
|
for attempt in range(max_retries):
|
|
892
561
|
try:
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
562
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
563
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
564
|
+
rewards_contract = web3.eth.contract(
|
|
565
|
+
address=self.reward_distributor_address,
|
|
566
|
+
abi=REWARD_DISTRIBUTOR_ABI,
|
|
567
|
+
)
|
|
898
568
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
569
|
+
# Fetch data sequentially to avoid overwhelming rate-limited public RPCs
|
|
570
|
+
# (parallel fetch would make 5 simultaneous calls per position)
|
|
571
|
+
bal = await mtoken_contract.functions.balanceOf(account).call(
|
|
572
|
+
block_identifier=block_id
|
|
573
|
+
)
|
|
574
|
+
exch = await mtoken_contract.functions.exchangeRateStored().call(
|
|
575
|
+
block_identifier=block_id
|
|
576
|
+
)
|
|
577
|
+
borrow = await mtoken_contract.functions.borrowBalanceStored(
|
|
578
|
+
account
|
|
579
|
+
).call(block_identifier=block_id)
|
|
580
|
+
underlying = await mtoken_contract.functions.underlying().call(
|
|
581
|
+
block_identifier=block_id
|
|
582
|
+
)
|
|
583
|
+
rewards = (
|
|
584
|
+
await rewards_contract.functions.getOutstandingRewardsForUser(
|
|
585
|
+
mtoken, account
|
|
586
|
+
).call(block_identifier=block_id)
|
|
587
|
+
)
|
|
916
588
|
break # Success, exit retry loop
|
|
917
589
|
except Exception as exc:
|
|
918
590
|
last_error = str(exc)
|
|
@@ -1017,9 +689,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1017
689
|
Uses a 1-hour cache since collateral factors rarely change (governance controlled).
|
|
1018
690
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1019
691
|
"""
|
|
1020
|
-
if not self.web3:
|
|
1021
|
-
return False, "web3 service not configured"
|
|
1022
|
-
|
|
1023
692
|
mtoken = self._checksum(mtoken)
|
|
1024
693
|
|
|
1025
694
|
# Check cache first
|
|
@@ -1032,25 +701,27 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1032
701
|
last_error = ""
|
|
1033
702
|
for attempt in range(max_retries):
|
|
1034
703
|
try:
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
704
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
705
|
+
contract = web3.eth.contract(
|
|
706
|
+
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
707
|
+
)
|
|
1039
708
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
709
|
+
# markets() returns (isListed, collateralFactorMantissa)
|
|
710
|
+
result = await contract.functions.markets(mtoken).call(
|
|
711
|
+
block_identifier="pending"
|
|
712
|
+
)
|
|
713
|
+
is_listed, collateral_factor_mantissa = result
|
|
1043
714
|
|
|
1044
|
-
|
|
1045
|
-
|
|
715
|
+
if not is_listed:
|
|
716
|
+
return False, f"Market {mtoken} is not listed"
|
|
1046
717
|
|
|
1047
|
-
|
|
1048
|
-
|
|
718
|
+
# Convert from mantissa to decimal
|
|
719
|
+
collateral_factor = collateral_factor_mantissa / MANTISSA
|
|
1049
720
|
|
|
1050
|
-
|
|
1051
|
-
|
|
721
|
+
# Cache the result
|
|
722
|
+
self._cf_cache[mtoken] = (collateral_factor, now)
|
|
1052
723
|
|
|
1053
|
-
|
|
724
|
+
return True, collateral_factor
|
|
1054
725
|
except Exception as exc:
|
|
1055
726
|
last_error = str(exc)
|
|
1056
727
|
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
@@ -1073,50 +744,60 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1073
744
|
|
|
1074
745
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1075
746
|
"""
|
|
1076
|
-
if not self.web3:
|
|
1077
|
-
return False, "web3 service not configured"
|
|
1078
|
-
|
|
1079
747
|
mtoken = self._checksum(mtoken)
|
|
1080
748
|
|
|
1081
749
|
last_error = ""
|
|
1082
750
|
for attempt in range(max_retries):
|
|
1083
751
|
try:
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
# Get base rate (sequential to avoid rate limits)
|
|
1091
|
-
if apy_type == "supply":
|
|
1092
|
-
rate_per_timestamp = (
|
|
1093
|
-
await mtoken_contract.functions.supplyRatePerTimestamp().call()
|
|
1094
|
-
)
|
|
1095
|
-
mkt_config = await reward_distributor.functions.getAllMarketConfigs(
|
|
1096
|
-
mtoken
|
|
1097
|
-
).call()
|
|
1098
|
-
total_value = await mtoken_contract.functions.totalSupply().call()
|
|
1099
|
-
else:
|
|
1100
|
-
rate_per_timestamp = (
|
|
1101
|
-
await mtoken_contract.functions.borrowRatePerTimestamp().call()
|
|
1102
|
-
)
|
|
1103
|
-
mkt_config = await reward_distributor.functions.getAllMarketConfigs(
|
|
1104
|
-
mtoken
|
|
1105
|
-
).call()
|
|
1106
|
-
total_value = await mtoken_contract.functions.totalBorrows().call()
|
|
1107
|
-
|
|
1108
|
-
# Convert rate per second to APY
|
|
1109
|
-
rate = rate_per_timestamp / MANTISSA
|
|
1110
|
-
apy = _timestamp_rate_to_apy(rate)
|
|
1111
|
-
|
|
1112
|
-
# Add WELL rewards APY if requested and token_client available
|
|
1113
|
-
if include_rewards and self.token_client and total_value > 0:
|
|
1114
|
-
rewards_apr = await self._calculate_rewards_apr(
|
|
1115
|
-
mtoken, mkt_config, total_value, apy_type
|
|
752
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
753
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
754
|
+
reward_distributor = web3.eth.contract(
|
|
755
|
+
address=self.reward_distributor_address,
|
|
756
|
+
abi=REWARD_DISTRIBUTOR_ABI,
|
|
1116
757
|
)
|
|
1117
|
-
apy += rewards_apr
|
|
1118
758
|
|
|
1119
|
-
|
|
759
|
+
# Get base rate (sequential to avoid rate limits)
|
|
760
|
+
if apy_type == "supply":
|
|
761
|
+
rate_per_timestamp = await mtoken_contract.functions.supplyRatePerTimestamp().call(
|
|
762
|
+
block_identifier="pending"
|
|
763
|
+
)
|
|
764
|
+
mkt_config = (
|
|
765
|
+
await reward_distributor.functions.getAllMarketConfigs(
|
|
766
|
+
mtoken
|
|
767
|
+
).call(block_identifier="pending")
|
|
768
|
+
)
|
|
769
|
+
total_value = (
|
|
770
|
+
await mtoken_contract.functions.totalSupply().call(
|
|
771
|
+
block_identifier="pending"
|
|
772
|
+
)
|
|
773
|
+
)
|
|
774
|
+
else:
|
|
775
|
+
rate_per_timestamp = await mtoken_contract.functions.borrowRatePerTimestamp().call(
|
|
776
|
+
block_identifier="pending"
|
|
777
|
+
)
|
|
778
|
+
mkt_config = (
|
|
779
|
+
await reward_distributor.functions.getAllMarketConfigs(
|
|
780
|
+
mtoken
|
|
781
|
+
).call(block_identifier="pending")
|
|
782
|
+
)
|
|
783
|
+
total_value = (
|
|
784
|
+
await mtoken_contract.functions.totalBorrows().call(
|
|
785
|
+
block_identifier="pending"
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
# Convert rate per second to APY
|
|
790
|
+
rate = rate_per_timestamp / MANTISSA
|
|
791
|
+
apy = _timestamp_rate_to_apy(rate)
|
|
792
|
+
|
|
793
|
+
# Add WELL rewards APY if requested and token_client available
|
|
794
|
+
if include_rewards and self.token_client and total_value > 0:
|
|
795
|
+
rewards_apr = await self._calculate_rewards_apr(
|
|
796
|
+
mtoken, mkt_config, total_value, apy_type
|
|
797
|
+
)
|
|
798
|
+
apy += rewards_apr
|
|
799
|
+
|
|
800
|
+
return True, apy
|
|
1120
801
|
except Exception as exc:
|
|
1121
802
|
last_error = str(exc)
|
|
1122
803
|
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
@@ -1163,9 +844,11 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1163
844
|
return 0.0
|
|
1164
845
|
|
|
1165
846
|
# Get underlying token for decimals
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
847
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
848
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
849
|
+
underlying_addr = await mtoken_contract.functions.underlying().call(
|
|
850
|
+
block_identifier="pending"
|
|
851
|
+
)
|
|
1169
852
|
|
|
1170
853
|
# Get prices
|
|
1171
854
|
well_key = f"{self.chain_name}_{self.well_token}"
|
|
@@ -1221,33 +904,32 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1221
904
|
|
|
1222
905
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1223
906
|
"""
|
|
1224
|
-
if not self.web3:
|
|
1225
|
-
return False, "web3 service not configured"
|
|
1226
|
-
|
|
1227
907
|
account = self._checksum(account) if account else self._strategy_address()
|
|
1228
908
|
|
|
1229
909
|
last_error = ""
|
|
1230
910
|
for attempt in range(max_retries):
|
|
1231
911
|
try:
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
912
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
913
|
+
contract = web3.eth.contract(
|
|
914
|
+
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
915
|
+
)
|
|
1236
916
|
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
917
|
+
# getAccountLiquidity returns (error, liquidity, shortfall)
|
|
918
|
+
(
|
|
919
|
+
error,
|
|
920
|
+
liquidity,
|
|
921
|
+
shortfall,
|
|
922
|
+
) = await contract.functions.getAccountLiquidity(account).call(
|
|
923
|
+
block_identifier="pending"
|
|
924
|
+
)
|
|
1243
925
|
|
|
1244
|
-
|
|
1245
|
-
|
|
926
|
+
if error != 0:
|
|
927
|
+
return False, f"Comptroller error: {error}"
|
|
1246
928
|
|
|
1247
|
-
|
|
1248
|
-
|
|
929
|
+
if shortfall > 0:
|
|
930
|
+
return False, f"Account has shortfall: {shortfall}"
|
|
1249
931
|
|
|
1250
|
-
|
|
932
|
+
return True, liquidity
|
|
1251
933
|
except Exception as exc:
|
|
1252
934
|
last_error = str(exc)
|
|
1253
935
|
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
@@ -1265,93 +947,102 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1265
947
|
account: str | None = None,
|
|
1266
948
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
1267
949
|
"""Calculate max mTokens withdrawable without liquidation using binary search."""
|
|
1268
|
-
if not self.web3:
|
|
1269
|
-
return False, "web3 service not configured"
|
|
1270
|
-
|
|
1271
950
|
mtoken = self._checksum(mtoken)
|
|
1272
951
|
account = self._checksum(account) if account else self._strategy_address()
|
|
1273
952
|
|
|
1274
953
|
try:
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
954
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
955
|
+
comptroller = web3.eth.contract(
|
|
956
|
+
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
957
|
+
)
|
|
958
|
+
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
959
|
+
|
|
960
|
+
# Get all needed data in parallel
|
|
961
|
+
bal_raw, exch_raw, cash_raw, m_dec, u_addr = await asyncio.gather(
|
|
962
|
+
mtoken_contract.functions.balanceOf(account).call(
|
|
963
|
+
block_identifier="pending"
|
|
964
|
+
),
|
|
965
|
+
mtoken_contract.functions.exchangeRateStored().call(
|
|
966
|
+
block_identifier="pending"
|
|
967
|
+
),
|
|
968
|
+
mtoken_contract.functions.getCash().call(
|
|
969
|
+
block_identifier="pending"
|
|
970
|
+
),
|
|
971
|
+
mtoken_contract.functions.decimals().call(
|
|
972
|
+
block_identifier="pending"
|
|
973
|
+
),
|
|
974
|
+
mtoken_contract.functions.underlying().call(
|
|
975
|
+
block_identifier="pending"
|
|
976
|
+
),
|
|
977
|
+
)
|
|
978
|
+
|
|
979
|
+
if bal_raw == 0 or exch_raw == 0:
|
|
980
|
+
return True, {
|
|
981
|
+
"cTokens_raw": 0,
|
|
982
|
+
"cTokens": 0.0,
|
|
983
|
+
"underlying_raw": 0,
|
|
984
|
+
"underlying": 0.0,
|
|
985
|
+
"bounds_raw": {"collateral_cTokens": 0, "cash_cTokens": 0},
|
|
986
|
+
"exchangeRate_raw": int(exch_raw),
|
|
987
|
+
"mToken_decimals": int(m_dec),
|
|
988
|
+
"underlying_decimals": None,
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
# Get underlying decimals
|
|
992
|
+
u_dec = 18 # Default
|
|
993
|
+
if self.token_client:
|
|
994
|
+
try:
|
|
995
|
+
u_key = f"{self.chain_name}_{u_addr}"
|
|
996
|
+
u_data = await self.token_client.get_token_details(u_key)
|
|
997
|
+
if u_data:
|
|
998
|
+
u_dec = u_data.get("decimals", 18)
|
|
999
|
+
except Exception:
|
|
1000
|
+
pass
|
|
1001
|
+
|
|
1002
|
+
# Binary search: largest cTokens you can redeem without shortfall
|
|
1003
|
+
lo, hi = 0, int(bal_raw)
|
|
1004
|
+
while lo < hi:
|
|
1005
|
+
mid = (lo + hi + 1) // 2
|
|
1006
|
+
(
|
|
1007
|
+
err,
|
|
1008
|
+
_liq,
|
|
1009
|
+
short,
|
|
1010
|
+
) = await comptroller.functions.getHypotheticalAccountLiquidity(
|
|
1011
|
+
account, mtoken, mid, 0
|
|
1012
|
+
).call(block_identifier="pending")
|
|
1013
|
+
if err != 0:
|
|
1014
|
+
return False, f"Comptroller error {err}"
|
|
1015
|
+
if short == 0:
|
|
1016
|
+
lo = mid # Safe, try more
|
|
1017
|
+
else:
|
|
1018
|
+
hi = mid - 1
|
|
1019
|
+
|
|
1020
|
+
c_by_collateral = lo
|
|
1021
|
+
|
|
1022
|
+
# Pool cash bound (convert underlying cash -> cToken capacity)
|
|
1023
|
+
c_by_cash = (int(cash_raw) * MANTISSA) // int(exch_raw)
|
|
1024
|
+
|
|
1025
|
+
redeem_c_raw = min(c_by_collateral, int(c_by_cash))
|
|
1026
|
+
|
|
1027
|
+
# Final underlying you actually receive (mirror Solidity floor)
|
|
1028
|
+
under_raw = (redeem_c_raw * int(exch_raw)) // MANTISSA
|
|
1289
1029
|
|
|
1290
|
-
if bal_raw == 0 or exch_raw == 0:
|
|
1291
1030
|
return True, {
|
|
1292
|
-
"cTokens_raw":
|
|
1293
|
-
"cTokens":
|
|
1294
|
-
"underlying_raw":
|
|
1295
|
-
"underlying":
|
|
1296
|
-
"bounds_raw": {
|
|
1031
|
+
"cTokens_raw": int(redeem_c_raw),
|
|
1032
|
+
"cTokens": redeem_c_raw / (10 ** int(m_dec)),
|
|
1033
|
+
"underlying_raw": int(under_raw),
|
|
1034
|
+
"underlying": under_raw / (10 ** int(u_dec)),
|
|
1035
|
+
"bounds_raw": {
|
|
1036
|
+
"collateral_cTokens": int(c_by_collateral),
|
|
1037
|
+
"cash_cTokens": int(c_by_cash),
|
|
1038
|
+
},
|
|
1297
1039
|
"exchangeRate_raw": int(exch_raw),
|
|
1298
1040
|
"mToken_decimals": int(m_dec),
|
|
1299
|
-
"underlying_decimals":
|
|
1041
|
+
"underlying_decimals": int(u_dec),
|
|
1042
|
+
"conversion_factor": redeem_c_raw / under_raw
|
|
1043
|
+
if under_raw > 0
|
|
1044
|
+
else 0,
|
|
1300
1045
|
}
|
|
1301
|
-
|
|
1302
|
-
# Get underlying decimals
|
|
1303
|
-
u_dec = 18 # Default
|
|
1304
|
-
if self.token_client:
|
|
1305
|
-
try:
|
|
1306
|
-
u_key = f"{self.chain_name}_{u_addr}"
|
|
1307
|
-
u_data = await self.token_client.get_token_details(u_key)
|
|
1308
|
-
if u_data:
|
|
1309
|
-
u_dec = u_data.get("decimals", 18)
|
|
1310
|
-
except Exception:
|
|
1311
|
-
pass
|
|
1312
|
-
|
|
1313
|
-
# Binary search: largest cTokens you can redeem without shortfall
|
|
1314
|
-
lo, hi = 0, int(bal_raw)
|
|
1315
|
-
while lo < hi:
|
|
1316
|
-
mid = (lo + hi + 1) // 2
|
|
1317
|
-
(
|
|
1318
|
-
err,
|
|
1319
|
-
_liq,
|
|
1320
|
-
short,
|
|
1321
|
-
) = await comptroller.functions.getHypotheticalAccountLiquidity(
|
|
1322
|
-
account, mtoken, mid, 0
|
|
1323
|
-
).call()
|
|
1324
|
-
if err != 0:
|
|
1325
|
-
return False, f"Comptroller error {err}"
|
|
1326
|
-
if short == 0:
|
|
1327
|
-
lo = mid # Safe, try more
|
|
1328
|
-
else:
|
|
1329
|
-
hi = mid - 1
|
|
1330
|
-
|
|
1331
|
-
c_by_collateral = lo
|
|
1332
|
-
|
|
1333
|
-
# Pool cash bound (convert underlying cash -> cToken capacity)
|
|
1334
|
-
c_by_cash = (int(cash_raw) * MANTISSA) // int(exch_raw)
|
|
1335
|
-
|
|
1336
|
-
redeem_c_raw = min(c_by_collateral, int(c_by_cash))
|
|
1337
|
-
|
|
1338
|
-
# Final underlying you actually receive (mirror Solidity floor)
|
|
1339
|
-
under_raw = (redeem_c_raw * int(exch_raw)) // MANTISSA
|
|
1340
|
-
|
|
1341
|
-
return True, {
|
|
1342
|
-
"cTokens_raw": int(redeem_c_raw),
|
|
1343
|
-
"cTokens": redeem_c_raw / (10 ** int(m_dec)),
|
|
1344
|
-
"underlying_raw": int(under_raw),
|
|
1345
|
-
"underlying": under_raw / (10 ** int(u_dec)),
|
|
1346
|
-
"bounds_raw": {
|
|
1347
|
-
"collateral_cTokens": int(c_by_collateral),
|
|
1348
|
-
"cash_cTokens": int(c_by_cash),
|
|
1349
|
-
},
|
|
1350
|
-
"exchangeRate_raw": int(exch_raw),
|
|
1351
|
-
"mToken_decimals": int(m_dec),
|
|
1352
|
-
"underlying_decimals": int(u_dec),
|
|
1353
|
-
"conversion_factor": redeem_c_raw / under_raw if under_raw > 0 else 0,
|
|
1354
|
-
}
|
|
1355
1046
|
except Exception as exc:
|
|
1356
1047
|
return False, str(exc)
|
|
1357
1048
|
|
|
@@ -1378,7 +1069,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1378
1069
|
from_address=strategy,
|
|
1379
1070
|
value=amount,
|
|
1380
1071
|
)
|
|
1381
|
-
return await self.
|
|
1072
|
+
return await self._send_tx(tx)
|
|
1382
1073
|
|
|
1383
1074
|
# ------------------------------------------------------------------ #
|
|
1384
1075
|
# Helpers #
|
|
@@ -1387,6 +1078,13 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1387
1078
|
# Max uint256 for unlimited approvals
|
|
1388
1079
|
MAX_UINT256 = 2**256 - 1
|
|
1389
1080
|
|
|
1081
|
+
async def _send_tx(self, tx: dict[str, Any]) -> tuple[bool, Any]:
|
|
1082
|
+
"""Send transaction with simulation check."""
|
|
1083
|
+
if self.simulation:
|
|
1084
|
+
return True, {"simulation": tx}
|
|
1085
|
+
txn_hash = await send_transaction(tx, self.strategy_wallet_signing_callback)
|
|
1086
|
+
return True, txn_hash
|
|
1087
|
+
|
|
1390
1088
|
async def _ensure_allowance(
|
|
1391
1089
|
self,
|
|
1392
1090
|
*,
|
|
@@ -1398,29 +1096,27 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1398
1096
|
"""Ensure token allowance is sufficient, approving if needed.
|
|
1399
1097
|
|
|
1400
1098
|
Approves for max uint256 to avoid precision issues with exact amounts.
|
|
1099
|
+
In simulation mode, skips the allowance check and assumes approval needed.
|
|
1401
1100
|
"""
|
|
1402
|
-
if
|
|
1403
|
-
return
|
|
1101
|
+
if self.simulation:
|
|
1102
|
+
return True, {} # Skip allowance check in simulation
|
|
1404
1103
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
chain, token_address, owner, spender
|
|
1104
|
+
allowance = await get_token_allowance(
|
|
1105
|
+
token_address, self.chain_id, owner, spender
|
|
1408
1106
|
)
|
|
1409
|
-
if allowance
|
|
1107
|
+
if allowance >= amount:
|
|
1410
1108
|
return True, {}
|
|
1411
1109
|
|
|
1412
1110
|
# Approve for max uint256 to avoid precision/timing issues
|
|
1413
|
-
|
|
1111
|
+
approve_tx = await build_approve_transaction(
|
|
1112
|
+
from_address=owner,
|
|
1414
1113
|
chain_id=self.chain_id,
|
|
1415
1114
|
token_address=token_address,
|
|
1416
|
-
|
|
1417
|
-
spender=spender,
|
|
1115
|
+
spender_address=spender,
|
|
1418
1116
|
amount=self.MAX_UINT256,
|
|
1419
1117
|
)
|
|
1420
|
-
if not build_success:
|
|
1421
|
-
return False, approve_tx
|
|
1422
1118
|
|
|
1423
|
-
result = await self.
|
|
1119
|
+
result = await self._send_tx(approve_tx)
|
|
1424
1120
|
|
|
1425
1121
|
# Small delay after approval to ensure state is propagated on providers/chains
|
|
1426
1122
|
# where we don't wait for additional confirmations by default.
|
|
@@ -1436,60 +1132,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1436
1132
|
|
|
1437
1133
|
return result
|
|
1438
1134
|
|
|
1439
|
-
async def _execute(
|
|
1440
|
-
self, tx: dict[str, Any], max_retries: int = DEFAULT_MAX_RETRIES
|
|
1441
|
-
) -> tuple[bool, Any]:
|
|
1442
|
-
"""Execute a transaction (or return simulation data).
|
|
1443
|
-
|
|
1444
|
-
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1445
|
-
"""
|
|
1446
|
-
if self.simulation:
|
|
1447
|
-
return True, {"simulation": tx}
|
|
1448
|
-
if not self.web3:
|
|
1449
|
-
return False, "web3 service not configured"
|
|
1450
|
-
|
|
1451
|
-
last_error = None
|
|
1452
|
-
for attempt in range(max_retries):
|
|
1453
|
-
try:
|
|
1454
|
-
return await self.web3.broadcast_transaction(
|
|
1455
|
-
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
1456
|
-
)
|
|
1457
|
-
except Exception as exc:
|
|
1458
|
-
last_error = exc
|
|
1459
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
1460
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
1461
|
-
await asyncio.sleep(wait_time)
|
|
1462
|
-
continue
|
|
1463
|
-
return False, str(exc)
|
|
1464
|
-
|
|
1465
|
-
return False, str(last_error) if last_error else "Max retries exceeded"
|
|
1466
|
-
|
|
1467
|
-
async def _broadcast_transaction(
|
|
1468
|
-
self, tx: dict[str, Any], max_retries: int = DEFAULT_MAX_RETRIES
|
|
1469
|
-
) -> tuple[bool, Any]:
|
|
1470
|
-
"""Broadcast a pre-built transaction.
|
|
1471
|
-
|
|
1472
|
-
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1473
|
-
"""
|
|
1474
|
-
if not self.web3:
|
|
1475
|
-
return False, "web3 service not configured"
|
|
1476
|
-
|
|
1477
|
-
last_error = None
|
|
1478
|
-
for attempt in range(max_retries):
|
|
1479
|
-
try:
|
|
1480
|
-
return await self.web3.evm_transactions.broadcast_transaction(
|
|
1481
|
-
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
1482
|
-
)
|
|
1483
|
-
except Exception as exc:
|
|
1484
|
-
last_error = exc
|
|
1485
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
1486
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
1487
|
-
await asyncio.sleep(wait_time)
|
|
1488
|
-
continue
|
|
1489
|
-
return False, str(exc)
|
|
1490
|
-
|
|
1491
|
-
return False, str(last_error) if last_error else "Max retries exceeded"
|
|
1492
|
-
|
|
1493
1135
|
async def _encode_call(
|
|
1494
1136
|
self,
|
|
1495
1137
|
*,
|
|
@@ -1501,28 +1143,25 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1501
1143
|
value: int = 0,
|
|
1502
1144
|
) -> dict[str, Any]:
|
|
1503
1145
|
"""Encode a contract call without touching the network."""
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
1508
|
-
contract = web3.eth.contract(address=target, abi=abi)
|
|
1146
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
1147
|
+
contract = web3.eth.contract(address=target, abi=abi)
|
|
1509
1148
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1149
|
+
try:
|
|
1150
|
+
tx_data = await getattr(contract.functions, fn_name)(
|
|
1151
|
+
*args
|
|
1152
|
+
).build_transaction({"from": from_address})
|
|
1153
|
+
data = tx_data["data"]
|
|
1154
|
+
except ValueError as exc:
|
|
1155
|
+
raise ValueError(f"Failed to encode {fn_name}: {exc}") from exc
|
|
1156
|
+
|
|
1157
|
+
tx: dict[str, Any] = {
|
|
1158
|
+
"chainId": int(self.chain_id),
|
|
1159
|
+
"from": to_checksum_address(from_address),
|
|
1160
|
+
"to": to_checksum_address(target),
|
|
1161
|
+
"data": data,
|
|
1162
|
+
"value": int(value),
|
|
1163
|
+
}
|
|
1164
|
+
return tx
|
|
1526
1165
|
|
|
1527
1166
|
def _strategy_address(self) -> str:
|
|
1528
1167
|
"""Get the strategy wallet address."""
|