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

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

Potentially problematic release.


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

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