wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wayfinder-paths might be problematic. Click here for more details.

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