wayfinder-paths 0.1.14__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.
- wayfinder_paths/adapters/balance_adapter/README.md +19 -20
- wayfinder_paths/adapters/balance_adapter/adapter.py +91 -22
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +5 -11
- wayfinder_paths/adapters/brap_adapter/README.md +22 -19
- wayfinder_paths/adapters/brap_adapter/adapter.py +95 -45
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +8 -24
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +40 -42
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +8 -15
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/README.md +29 -31
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +326 -364
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +285 -189
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -4
- wayfinder_paths/core/config.py +8 -47
- wayfinder_paths/core/constants/base.py +0 -1
- wayfinder_paths/core/constants/erc20_abi.py +13 -24
- wayfinder_paths/core/engine/StrategyJob.py +3 -1
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +22 -4
- wayfinder_paths/core/utils/erc20_service.py +100 -0
- wayfinder_paths/core/utils/evm_helpers.py +1 -8
- wayfinder_paths/core/utils/transaction.py +191 -0
- wayfinder_paths/core/utils/web3.py +66 -0
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +42 -6
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +263 -220
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +132 -155
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +123 -80
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -12
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -6
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2270 -1328
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +282 -121
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +0 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +107 -85
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +0 -8
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +1 -5
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/METADATA +3 -41
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/RECORD +45 -54
- {wayfinder_paths-0.1.14.dist-info → wayfinder_paths-0.1.16.dist-info}/WHEEL +1 -1
- wayfinder_paths/abis/generic/erc20.json +0 -383
- 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 -130
- wayfinder_paths/core/services/local_evm_txn.py +0 -334
- wayfinder_paths/core/services/local_token_txn.py +0 -242
- 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.14.dist-info → wayfinder_paths-0.1.16.dist-info}/LICENSE +0 -0
|
@@ -15,14 +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
18
|
from wayfinder_paths.core.constants.moonwell_abi import (
|
|
20
19
|
COMPTROLLER_ABI,
|
|
21
20
|
MTOKEN_ABI,
|
|
22
21
|
REWARD_DISTRIBUTOR_ABI,
|
|
23
22
|
WETH_ABI,
|
|
24
23
|
)
|
|
25
|
-
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
|
|
26
30
|
|
|
27
31
|
# Moonwell Base chain addresses
|
|
28
32
|
MOONWELL_DEFAULTS = {
|
|
@@ -64,38 +68,6 @@ def _is_rate_limit_error(error: Exception | str) -> bool:
|
|
|
64
68
|
return "429" in error_str or "Too Many Requests" in error_str
|
|
65
69
|
|
|
66
70
|
|
|
67
|
-
async def _retry_with_backoff(
|
|
68
|
-
coro_factory,
|
|
69
|
-
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
70
|
-
base_delay: float = DEFAULT_BASE_DELAY,
|
|
71
|
-
):
|
|
72
|
-
"""Retry an async operation with exponential backoff on rate limit errors.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
coro_factory: A callable that returns a new coroutine each time.
|
|
76
|
-
max_retries: Maximum number of retry attempts.
|
|
77
|
-
base_delay: Base delay in seconds (doubles each retry).
|
|
78
|
-
|
|
79
|
-
Returns:
|
|
80
|
-
The result of the coroutine if successful.
|
|
81
|
-
|
|
82
|
-
Raises:
|
|
83
|
-
The last exception if all retries fail.
|
|
84
|
-
"""
|
|
85
|
-
last_error = None
|
|
86
|
-
for attempt in range(max_retries):
|
|
87
|
-
try:
|
|
88
|
-
return await coro_factory()
|
|
89
|
-
except Exception as exc:
|
|
90
|
-
last_error = exc
|
|
91
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
92
|
-
wait_time = base_delay * (2**attempt)
|
|
93
|
-
await asyncio.sleep(wait_time)
|
|
94
|
-
continue
|
|
95
|
-
raise
|
|
96
|
-
raise last_error
|
|
97
|
-
|
|
98
|
-
|
|
99
71
|
def _timestamp_rate_to_apy(rate: float) -> float:
|
|
100
72
|
"""Convert a per-second rate to APY."""
|
|
101
73
|
return (1 + rate) ** SECONDS_PER_YEAR - 1
|
|
@@ -109,20 +81,17 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
109
81
|
def __init__(
|
|
110
82
|
self,
|
|
111
83
|
config: dict[str, Any] | None = None,
|
|
112
|
-
web3_service: Web3Service | None = None,
|
|
113
84
|
token_client: TokenClient | None = None,
|
|
114
85
|
simulation: bool = False,
|
|
86
|
+
strategy_wallet_signing_callback=None,
|
|
115
87
|
) -> None:
|
|
116
88
|
super().__init__("moonwell_adapter", config)
|
|
117
89
|
cfg = config or {}
|
|
118
90
|
adapter_cfg = cfg.get("moonwell_adapter") or {}
|
|
119
91
|
|
|
120
|
-
self.web3 = web3_service
|
|
121
92
|
self.simulation = simulation
|
|
122
93
|
self.token_client = token_client
|
|
123
|
-
self.
|
|
124
|
-
web3_service.token_transactions if web3_service else None
|
|
125
|
-
)
|
|
94
|
+
self.strategy_wallet_signing_callback = strategy_wallet_signing_callback
|
|
126
95
|
|
|
127
96
|
self.strategy_wallet = cfg.get("strategy_wallet") or {}
|
|
128
97
|
self.chain_id = adapter_cfg.get("chain_id", BASE_CHAIN_ID)
|
|
@@ -197,7 +166,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
197
166
|
args=[amount],
|
|
198
167
|
from_address=strategy,
|
|
199
168
|
)
|
|
200
|
-
return await self.
|
|
169
|
+
return await self._send_tx(tx)
|
|
201
170
|
|
|
202
171
|
async def unlend(
|
|
203
172
|
self,
|
|
@@ -221,7 +190,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
221
190
|
args=[amount],
|
|
222
191
|
from_address=strategy,
|
|
223
192
|
)
|
|
224
|
-
return await self.
|
|
193
|
+
return await self._send_tx(tx)
|
|
225
194
|
|
|
226
195
|
# ------------------------------------------------------------------ #
|
|
227
196
|
# Public API - Borrowing Operations #
|
|
@@ -251,19 +220,18 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
251
220
|
|
|
252
221
|
# Get borrow balance before the transaction for verification
|
|
253
222
|
borrow_before = 0
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
223
|
+
try:
|
|
224
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
257
225
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
258
226
|
|
|
259
227
|
borrow_before = await mtoken_contract.functions.borrowBalanceStored(
|
|
260
228
|
strategy
|
|
261
|
-
).call()
|
|
229
|
+
).call(block_identifier="pending")
|
|
262
230
|
|
|
263
231
|
# Simulate borrow to check for errors before submitting
|
|
264
232
|
try:
|
|
265
233
|
borrow_return = await mtoken_contract.functions.borrow(amount).call(
|
|
266
|
-
{"from": strategy}
|
|
234
|
+
{"from": strategy}, block_identifier="pending"
|
|
267
235
|
)
|
|
268
236
|
if borrow_return != 0:
|
|
269
237
|
logger.warning(
|
|
@@ -273,9 +241,8 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
273
241
|
)
|
|
274
242
|
except Exception as call_err:
|
|
275
243
|
logger.debug(f"Borrow simulation failed: {call_err}")
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
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}")
|
|
279
246
|
|
|
280
247
|
tx = await self._encode_call(
|
|
281
248
|
target=mtoken,
|
|
@@ -284,19 +251,18 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
284
251
|
args=[amount],
|
|
285
252
|
from_address=strategy,
|
|
286
253
|
)
|
|
287
|
-
result = await self.
|
|
254
|
+
result = await self._send_tx(tx)
|
|
288
255
|
|
|
289
256
|
if not result[0]:
|
|
290
257
|
return result
|
|
291
258
|
|
|
292
259
|
# Verify the borrow actually succeeded by checking balance increased
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
260
|
+
try:
|
|
261
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
296
262
|
mtoken_contract = web3.eth.contract(address=mtoken, abi=MTOKEN_ABI)
|
|
297
263
|
borrow_after = await mtoken_contract.functions.borrowBalanceStored(
|
|
298
264
|
strategy
|
|
299
|
-
).call()
|
|
265
|
+
).call(block_identifier="pending")
|
|
300
266
|
|
|
301
267
|
# Borrow balance should have increased by approximately the amount
|
|
302
268
|
# Allow for some interest accrual
|
|
@@ -317,11 +283,11 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
317
283
|
f"Borrow failed: balance did not increase as expected. "
|
|
318
284
|
f"Before: {borrow_before}, After: {borrow_after}, Expected: +{amount}",
|
|
319
285
|
)
|
|
320
|
-
|
|
321
|
-
|
|
286
|
+
except Exception as e:
|
|
287
|
+
from loguru import logger
|
|
322
288
|
|
|
323
|
-
|
|
324
|
-
|
|
289
|
+
logger.warning(f"Could not verify borrow balance: {e}")
|
|
290
|
+
# Continue with the original result if verification fails
|
|
325
291
|
|
|
326
292
|
return result
|
|
327
293
|
|
|
@@ -370,7 +336,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
370
336
|
args=[repay_amount],
|
|
371
337
|
from_address=strategy,
|
|
372
338
|
)
|
|
373
|
-
return await self.
|
|
339
|
+
return await self._send_tx(tx)
|
|
374
340
|
|
|
375
341
|
# ------------------------------------------------------------------ #
|
|
376
342
|
# Public API - Collateral Management #
|
|
@@ -396,21 +362,20 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
396
362
|
args=[[mtoken]],
|
|
397
363
|
from_address=strategy,
|
|
398
364
|
)
|
|
399
|
-
result = await self.
|
|
365
|
+
result = await self._send_tx(tx)
|
|
400
366
|
|
|
401
367
|
if not result[0]:
|
|
402
368
|
return result
|
|
403
369
|
|
|
404
370
|
# Verify the market was actually entered
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
web3 = self.web3.get_web3(self.chain_id)
|
|
371
|
+
try:
|
|
372
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
408
373
|
comptroller = web3.eth.contract(
|
|
409
374
|
address=self.comptroller_address, abi=COMPTROLLER_ABI
|
|
410
375
|
)
|
|
411
376
|
is_member = await comptroller.functions.checkMembership(
|
|
412
377
|
strategy, mtoken
|
|
413
|
-
).call()
|
|
378
|
+
).call(block_identifier="pending")
|
|
414
379
|
|
|
415
380
|
if not is_member:
|
|
416
381
|
from loguru import logger
|
|
@@ -423,13 +388,38 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
423
388
|
False,
|
|
424
389
|
f"enterMarkets succeeded but account is not a member of market {mtoken}",
|
|
425
390
|
)
|
|
426
|
-
|
|
427
|
-
|
|
391
|
+
except Exception as e:
|
|
392
|
+
from loguru import logger
|
|
428
393
|
|
|
429
|
-
|
|
394
|
+
logger.warning(f"Could not verify market membership: {e}")
|
|
430
395
|
|
|
431
396
|
return result
|
|
432
397
|
|
|
398
|
+
async def is_market_entered(
|
|
399
|
+
self,
|
|
400
|
+
*,
|
|
401
|
+
mtoken: str,
|
|
402
|
+
account: str | None = None,
|
|
403
|
+
) -> tuple[bool, bool | str]:
|
|
404
|
+
"""Check whether an account has entered a given market (as collateral / borrowing market)."""
|
|
405
|
+
if self.simulation:
|
|
406
|
+
return True, True
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
acct = self._checksum(account) if account else self._strategy_address()
|
|
410
|
+
mtoken = self._checksum(mtoken)
|
|
411
|
+
|
|
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)
|
|
420
|
+
except Exception as exc:
|
|
421
|
+
return False, str(exc)
|
|
422
|
+
|
|
433
423
|
async def remove_collateral(
|
|
434
424
|
self,
|
|
435
425
|
*,
|
|
@@ -446,7 +436,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
446
436
|
args=[mtoken],
|
|
447
437
|
from_address=strategy,
|
|
448
438
|
)
|
|
449
|
-
return await self.
|
|
439
|
+
return await self._send_tx(tx)
|
|
450
440
|
|
|
451
441
|
# ------------------------------------------------------------------ #
|
|
452
442
|
# Public API - Rewards #
|
|
@@ -481,7 +471,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
481
471
|
args=[strategy],
|
|
482
472
|
from_address=strategy,
|
|
483
473
|
)
|
|
484
|
-
result = await self.
|
|
474
|
+
result = await self._send_tx(tx)
|
|
485
475
|
if not result[0]:
|
|
486
476
|
return result
|
|
487
477
|
|
|
@@ -489,33 +479,30 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
489
479
|
|
|
490
480
|
async def _get_outstanding_rewards(self, account: str) -> dict[str, int]:
|
|
491
481
|
"""Get outstanding rewards for an account across all markets."""
|
|
492
|
-
if not self.web3:
|
|
493
|
-
return {}
|
|
494
|
-
|
|
495
482
|
try:
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
+
)
|
|
500
487
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
519
506
|
except Exception:
|
|
520
507
|
return {}
|
|
521
508
|
|
|
@@ -559,45 +546,45 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
559
546
|
block_identifier: Block to query at. Can be:
|
|
560
547
|
- int: specific block number (for pinning to tx block)
|
|
561
548
|
- "safe": OP Stack safe block (data posted to L1)
|
|
562
|
-
- None/"
|
|
549
|
+
- None/"pending": current head (default)
|
|
563
550
|
|
|
564
551
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
565
552
|
"""
|
|
566
|
-
if not self.web3:
|
|
567
|
-
return False, "web3 service not configured"
|
|
568
|
-
|
|
569
553
|
mtoken = self._checksum(mtoken)
|
|
570
554
|
account = self._checksum(account) if account else self._strategy_address()
|
|
571
|
-
block_id = block_identifier if block_identifier is not None else "
|
|
555
|
+
block_id = block_identifier if block_identifier is not None else "pending"
|
|
572
556
|
|
|
573
557
|
bal = exch = borrow = underlying = rewards = None
|
|
574
558
|
last_error = ""
|
|
575
559
|
|
|
576
560
|
for attempt in range(max_retries):
|
|
577
561
|
try:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
)
|
|
583
568
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
+
)
|
|
601
588
|
break # Success, exit retry loop
|
|
602
589
|
except Exception as exc:
|
|
603
590
|
last_error = str(exc)
|
|
@@ -702,9 +689,6 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
702
689
|
Uses a 1-hour cache since collateral factors rarely change (governance controlled).
|
|
703
690
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
704
691
|
"""
|
|
705
|
-
if not self.web3:
|
|
706
|
-
return False, "web3 service not configured"
|
|
707
|
-
|
|
708
692
|
mtoken = self._checksum(mtoken)
|
|
709
693
|
|
|
710
694
|
# Check cache first
|
|
@@ -717,25 +701,27 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
717
701
|
last_error = ""
|
|
718
702
|
for attempt in range(max_retries):
|
|
719
703
|
try:
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
+
)
|
|
724
708
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
|
728
714
|
|
|
729
|
-
|
|
730
|
-
|
|
715
|
+
if not is_listed:
|
|
716
|
+
return False, f"Market {mtoken} is not listed"
|
|
731
717
|
|
|
732
|
-
|
|
733
|
-
|
|
718
|
+
# Convert from mantissa to decimal
|
|
719
|
+
collateral_factor = collateral_factor_mantissa / MANTISSA
|
|
734
720
|
|
|
735
|
-
|
|
736
|
-
|
|
721
|
+
# Cache the result
|
|
722
|
+
self._cf_cache[mtoken] = (collateral_factor, now)
|
|
737
723
|
|
|
738
|
-
|
|
724
|
+
return True, collateral_factor
|
|
739
725
|
except Exception as exc:
|
|
740
726
|
last_error = str(exc)
|
|
741
727
|
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
@@ -758,50 +744,60 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
758
744
|
|
|
759
745
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
760
746
|
"""
|
|
761
|
-
if not self.web3:
|
|
762
|
-
return False, "web3 service not configured"
|
|
763
|
-
|
|
764
747
|
mtoken = self._checksum(mtoken)
|
|
765
748
|
|
|
766
749
|
last_error = ""
|
|
767
750
|
for attempt in range(max_retries):
|
|
768
751
|
try:
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
# Get base rate (sequential to avoid rate limits)
|
|
776
|
-
if apy_type == "supply":
|
|
777
|
-
rate_per_timestamp = (
|
|
778
|
-
await mtoken_contract.functions.supplyRatePerTimestamp().call()
|
|
779
|
-
)
|
|
780
|
-
mkt_config = await reward_distributor.functions.getAllMarketConfigs(
|
|
781
|
-
mtoken
|
|
782
|
-
).call()
|
|
783
|
-
total_value = await mtoken_contract.functions.totalSupply().call()
|
|
784
|
-
else:
|
|
785
|
-
rate_per_timestamp = (
|
|
786
|
-
await mtoken_contract.functions.borrowRatePerTimestamp().call()
|
|
787
|
-
)
|
|
788
|
-
mkt_config = await reward_distributor.functions.getAllMarketConfigs(
|
|
789
|
-
mtoken
|
|
790
|
-
).call()
|
|
791
|
-
total_value = await mtoken_contract.functions.totalBorrows().call()
|
|
792
|
-
|
|
793
|
-
# Convert rate per second to APY
|
|
794
|
-
rate = rate_per_timestamp / MANTISSA
|
|
795
|
-
apy = _timestamp_rate_to_apy(rate)
|
|
796
|
-
|
|
797
|
-
# Add WELL rewards APY if requested and token_client available
|
|
798
|
-
if include_rewards and self.token_client and total_value > 0:
|
|
799
|
-
rewards_apr = await self._calculate_rewards_apr(
|
|
800
|
-
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,
|
|
801
757
|
)
|
|
802
|
-
apy += rewards_apr
|
|
803
758
|
|
|
804
|
-
|
|
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
|
|
805
801
|
except Exception as exc:
|
|
806
802
|
last_error = str(exc)
|
|
807
803
|
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
@@ -848,9 +844,11 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
848
844
|
return 0.0
|
|
849
845
|
|
|
850
846
|
# Get underlying token for decimals
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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
|
+
)
|
|
854
852
|
|
|
855
853
|
# Get prices
|
|
856
854
|
well_key = f"{self.chain_name}_{self.well_token}"
|
|
@@ -906,33 +904,32 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
906
904
|
|
|
907
905
|
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
908
906
|
"""
|
|
909
|
-
if not self.web3:
|
|
910
|
-
return False, "web3 service not configured"
|
|
911
|
-
|
|
912
907
|
account = self._checksum(account) if account else self._strategy_address()
|
|
913
908
|
|
|
914
909
|
last_error = ""
|
|
915
910
|
for attempt in range(max_retries):
|
|
916
911
|
try:
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
+
)
|
|
921
916
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
+
)
|
|
928
925
|
|
|
929
|
-
|
|
930
|
-
|
|
926
|
+
if error != 0:
|
|
927
|
+
return False, f"Comptroller error: {error}"
|
|
931
928
|
|
|
932
|
-
|
|
933
|
-
|
|
929
|
+
if shortfall > 0:
|
|
930
|
+
return False, f"Account has shortfall: {shortfall}"
|
|
934
931
|
|
|
935
|
-
|
|
932
|
+
return True, liquidity
|
|
936
933
|
except Exception as exc:
|
|
937
934
|
last_error = str(exc)
|
|
938
935
|
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
@@ -950,93 +947,102 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
950
947
|
account: str | None = None,
|
|
951
948
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
952
949
|
"""Calculate max mTokens withdrawable without liquidation using binary search."""
|
|
953
|
-
if not self.web3:
|
|
954
|
-
return False, "web3 service not configured"
|
|
955
|
-
|
|
956
950
|
mtoken = self._checksum(mtoken)
|
|
957
951
|
account = self._checksum(account) if account else self._strategy_address()
|
|
958
952
|
|
|
959
953
|
try:
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
|
974
1029
|
|
|
975
|
-
if bal_raw == 0 or exch_raw == 0:
|
|
976
1030
|
return True, {
|
|
977
|
-
"cTokens_raw":
|
|
978
|
-
"cTokens":
|
|
979
|
-
"underlying_raw":
|
|
980
|
-
"underlying":
|
|
981
|
-
"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
|
+
},
|
|
982
1039
|
"exchangeRate_raw": int(exch_raw),
|
|
983
1040
|
"mToken_decimals": int(m_dec),
|
|
984
|
-
"underlying_decimals":
|
|
1041
|
+
"underlying_decimals": int(u_dec),
|
|
1042
|
+
"conversion_factor": redeem_c_raw / under_raw
|
|
1043
|
+
if under_raw > 0
|
|
1044
|
+
else 0,
|
|
985
1045
|
}
|
|
986
|
-
|
|
987
|
-
# Get underlying decimals
|
|
988
|
-
u_dec = 18 # Default
|
|
989
|
-
if self.token_client:
|
|
990
|
-
try:
|
|
991
|
-
u_key = f"{self.chain_name}_{u_addr}"
|
|
992
|
-
u_data = await self.token_client.get_token_details(u_key)
|
|
993
|
-
if u_data:
|
|
994
|
-
u_dec = u_data.get("decimals", 18)
|
|
995
|
-
except Exception:
|
|
996
|
-
pass
|
|
997
|
-
|
|
998
|
-
# Binary search: largest cTokens you can redeem without shortfall
|
|
999
|
-
lo, hi = 0, int(bal_raw)
|
|
1000
|
-
while lo < hi:
|
|
1001
|
-
mid = (lo + hi + 1) // 2
|
|
1002
|
-
(
|
|
1003
|
-
err,
|
|
1004
|
-
_liq,
|
|
1005
|
-
short,
|
|
1006
|
-
) = await comptroller.functions.getHypotheticalAccountLiquidity(
|
|
1007
|
-
account, mtoken, mid, 0
|
|
1008
|
-
).call()
|
|
1009
|
-
if err != 0:
|
|
1010
|
-
return False, f"Comptroller error {err}"
|
|
1011
|
-
if short == 0:
|
|
1012
|
-
lo = mid # Safe, try more
|
|
1013
|
-
else:
|
|
1014
|
-
hi = mid - 1
|
|
1015
|
-
|
|
1016
|
-
c_by_collateral = lo
|
|
1017
|
-
|
|
1018
|
-
# Pool cash bound (convert underlying cash -> cToken capacity)
|
|
1019
|
-
c_by_cash = (int(cash_raw) * MANTISSA) // int(exch_raw)
|
|
1020
|
-
|
|
1021
|
-
redeem_c_raw = min(c_by_collateral, int(c_by_cash))
|
|
1022
|
-
|
|
1023
|
-
# Final underlying you actually receive (mirror Solidity floor)
|
|
1024
|
-
under_raw = (redeem_c_raw * int(exch_raw)) // MANTISSA
|
|
1025
|
-
|
|
1026
|
-
return True, {
|
|
1027
|
-
"cTokens_raw": int(redeem_c_raw),
|
|
1028
|
-
"cTokens": redeem_c_raw / (10 ** int(m_dec)),
|
|
1029
|
-
"underlying_raw": int(under_raw),
|
|
1030
|
-
"underlying": under_raw / (10 ** int(u_dec)),
|
|
1031
|
-
"bounds_raw": {
|
|
1032
|
-
"collateral_cTokens": int(c_by_collateral),
|
|
1033
|
-
"cash_cTokens": int(c_by_cash),
|
|
1034
|
-
},
|
|
1035
|
-
"exchangeRate_raw": int(exch_raw),
|
|
1036
|
-
"mToken_decimals": int(m_dec),
|
|
1037
|
-
"underlying_decimals": int(u_dec),
|
|
1038
|
-
"conversion_factor": redeem_c_raw / under_raw if under_raw > 0 else 0,
|
|
1039
|
-
}
|
|
1040
1046
|
except Exception as exc:
|
|
1041
1047
|
return False, str(exc)
|
|
1042
1048
|
|
|
@@ -1063,7 +1069,7 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1063
1069
|
from_address=strategy,
|
|
1064
1070
|
value=amount,
|
|
1065
1071
|
)
|
|
1066
|
-
return await self.
|
|
1072
|
+
return await self._send_tx(tx)
|
|
1067
1073
|
|
|
1068
1074
|
# ------------------------------------------------------------------ #
|
|
1069
1075
|
# Helpers #
|
|
@@ -1072,6 +1078,13 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1072
1078
|
# Max uint256 for unlimited approvals
|
|
1073
1079
|
MAX_UINT256 = 2**256 - 1
|
|
1074
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
|
+
|
|
1075
1088
|
async def _ensure_allowance(
|
|
1076
1089
|
self,
|
|
1077
1090
|
*,
|
|
@@ -1083,90 +1096,42 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1083
1096
|
"""Ensure token allowance is sufficient, approving if needed.
|
|
1084
1097
|
|
|
1085
1098
|
Approves for max uint256 to avoid precision issues with exact amounts.
|
|
1099
|
+
In simulation mode, skips the allowance check and assumes approval needed.
|
|
1086
1100
|
"""
|
|
1087
|
-
if
|
|
1088
|
-
return
|
|
1101
|
+
if self.simulation:
|
|
1102
|
+
return True, {} # Skip allowance check in simulation
|
|
1089
1103
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
chain, token_address, owner, spender
|
|
1104
|
+
allowance = await get_token_allowance(
|
|
1105
|
+
token_address, self.chain_id, owner, spender
|
|
1093
1106
|
)
|
|
1094
|
-
if allowance
|
|
1107
|
+
if allowance >= amount:
|
|
1095
1108
|
return True, {}
|
|
1096
1109
|
|
|
1097
1110
|
# Approve for max uint256 to avoid precision/timing issues
|
|
1098
|
-
|
|
1111
|
+
approve_tx = await build_approve_transaction(
|
|
1112
|
+
from_address=owner,
|
|
1099
1113
|
chain_id=self.chain_id,
|
|
1100
1114
|
token_address=token_address,
|
|
1101
|
-
|
|
1102
|
-
spender=spender,
|
|
1115
|
+
spender_address=spender,
|
|
1103
1116
|
amount=self.MAX_UINT256,
|
|
1104
1117
|
)
|
|
1105
|
-
if not build_success:
|
|
1106
|
-
return False, approve_tx
|
|
1107
1118
|
|
|
1108
|
-
result = await self.
|
|
1119
|
+
result = await self._send_tx(approve_tx)
|
|
1109
1120
|
|
|
1110
|
-
# Small delay after approval to ensure state is propagated
|
|
1121
|
+
# Small delay after approval to ensure state is propagated on providers/chains
|
|
1122
|
+
# where we don't wait for additional confirmations by default.
|
|
1111
1123
|
if result[0]:
|
|
1112
|
-
|
|
1124
|
+
confirmations = 0
|
|
1125
|
+
if isinstance(result[1], dict):
|
|
1126
|
+
try:
|
|
1127
|
+
confirmations = int(result[1].get("confirmations") or 0)
|
|
1128
|
+
except (TypeError, ValueError):
|
|
1129
|
+
confirmations = 0
|
|
1130
|
+
if confirmations == 0:
|
|
1131
|
+
await asyncio.sleep(1.0)
|
|
1113
1132
|
|
|
1114
1133
|
return result
|
|
1115
1134
|
|
|
1116
|
-
async def _execute(
|
|
1117
|
-
self, tx: dict[str, Any], max_retries: int = DEFAULT_MAX_RETRIES
|
|
1118
|
-
) -> tuple[bool, Any]:
|
|
1119
|
-
"""Execute a transaction (or return simulation data).
|
|
1120
|
-
|
|
1121
|
-
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1122
|
-
"""
|
|
1123
|
-
if self.simulation:
|
|
1124
|
-
return True, {"simulation": tx}
|
|
1125
|
-
if not self.web3:
|
|
1126
|
-
return False, "web3 service not configured"
|
|
1127
|
-
|
|
1128
|
-
last_error = None
|
|
1129
|
-
for attempt in range(max_retries):
|
|
1130
|
-
try:
|
|
1131
|
-
return await self.web3.broadcast_transaction(
|
|
1132
|
-
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
1133
|
-
)
|
|
1134
|
-
except Exception as exc:
|
|
1135
|
-
last_error = exc
|
|
1136
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
1137
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
1138
|
-
await asyncio.sleep(wait_time)
|
|
1139
|
-
continue
|
|
1140
|
-
return False, str(exc)
|
|
1141
|
-
|
|
1142
|
-
return False, str(last_error) if last_error else "Max retries exceeded"
|
|
1143
|
-
|
|
1144
|
-
async def _broadcast_transaction(
|
|
1145
|
-
self, tx: dict[str, Any], max_retries: int = DEFAULT_MAX_RETRIES
|
|
1146
|
-
) -> tuple[bool, Any]:
|
|
1147
|
-
"""Broadcast a pre-built transaction.
|
|
1148
|
-
|
|
1149
|
-
Includes retry logic with exponential backoff for rate-limited RPCs.
|
|
1150
|
-
"""
|
|
1151
|
-
if not self.web3:
|
|
1152
|
-
return False, "web3 service not configured"
|
|
1153
|
-
|
|
1154
|
-
last_error = None
|
|
1155
|
-
for attempt in range(max_retries):
|
|
1156
|
-
try:
|
|
1157
|
-
return await self.web3.evm_transactions.broadcast_transaction(
|
|
1158
|
-
tx, wait_for_receipt=True, timeout=DEFAULT_TRANSACTION_TIMEOUT
|
|
1159
|
-
)
|
|
1160
|
-
except Exception as exc:
|
|
1161
|
-
last_error = exc
|
|
1162
|
-
if _is_rate_limit_error(exc) and attempt < max_retries - 1:
|
|
1163
|
-
wait_time = DEFAULT_BASE_DELAY * (2**attempt)
|
|
1164
|
-
await asyncio.sleep(wait_time)
|
|
1165
|
-
continue
|
|
1166
|
-
return False, str(exc)
|
|
1167
|
-
|
|
1168
|
-
return False, str(last_error) if last_error else "Max retries exceeded"
|
|
1169
|
-
|
|
1170
1135
|
async def _encode_call(
|
|
1171
1136
|
self,
|
|
1172
1137
|
*,
|
|
@@ -1178,28 +1143,25 @@ class MoonwellAdapter(BaseAdapter):
|
|
|
1178
1143
|
value: int = 0,
|
|
1179
1144
|
) -> dict[str, Any]:
|
|
1180
1145
|
"""Encode a contract call without touching the network."""
|
|
1181
|
-
|
|
1182
|
-
|
|
1146
|
+
async with web3_from_chain_id(self.chain_id) as web3:
|
|
1147
|
+
contract = web3.eth.contract(address=target, abi=abi)
|
|
1183
1148
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
"value": int(value),
|
|
1201
|
-
}
|
|
1202
|
-
return tx
|
|
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
|
|
1203
1165
|
|
|
1204
1166
|
def _strategy_address(self) -> str:
|
|
1205
1167
|
"""Get the strategy wallet address."""
|